fe17886ac4
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 CRUD + 软删 - AI 截图识别(5 类型 OCR schema):OpenAI 兼容 + MiniMax M3 - 化学品 / Grocy 实例对接 + 库存镜像同步 - 仪表盘:30 天频次 + 健康度 + 同比环比 + 油价趋势 + 年均养护 - 月度报表:Excel 6 sheet + PDF - PWA:manifest / SW / 离线缓存 / iOS 引导 - 安全:bcrypt + CSRF + 登录锁定(IP/用户/全局三级)+ 401 自动跳登录 + 表单草稿 - 高 ROI 8 功能:里程/提醒/成本/搜索/标签/通知/同比/成就 - 3 个新 migration(0016/0017/0018)+ 18 个迁移全幂等 - 101/101 测试通过(含 ipRateLimit / CSRF / retry / stats / tags / notifications) - 部署:宝塔面板文档 + PM2 + Nginx
112 lines
4.3 KiB
JavaScript
112 lines
4.3 KiB
JavaScript
// server/test/routes.notifications.test.js — 站内通知测试
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
notifications: [],
|
|
nextId: 1,
|
|
}));
|
|
|
|
vi.mock('../src/db.js', () => ({
|
|
db: () => ({
|
|
all: vi.fn(async (sql) => {
|
|
if (/FROM notifications/.test(sql)) {
|
|
if (/is_read = 0/.test(sql)) return mocks.notifications.filter(n => !n.is_read);
|
|
return mocks.notifications.slice().reverse();
|
|
}
|
|
return [];
|
|
}),
|
|
get: vi.fn(async (sql) => {
|
|
if (/COUNT.*FROM notifications WHERE is_read = 0/.test(sql)) {
|
|
return { n: mocks.notifications.filter(n => !n.is_read).length };
|
|
}
|
|
return null;
|
|
}),
|
|
run: vi.fn(async (sql, params = []) => {
|
|
if (/INSERT INTO notifications/.test(sql)) {
|
|
const [type, title, body, link, severity] = params;
|
|
const id = mocks.nextId++;
|
|
mocks.notifications.push({
|
|
id, type, title, body, link, severity, is_read: 0,
|
|
created_at: '2026-06-20 01:00:00',
|
|
});
|
|
return { lastInsertRowid: id };
|
|
}
|
|
if (/UPDATE notifications SET is_read = 1 WHERE id/.test(sql)) {
|
|
const id = params[0];
|
|
const n = mocks.notifications.find(x => x.id === id);
|
|
if (n) n.is_read = 1;
|
|
return { changes: 1 };
|
|
}
|
|
if (/UPDATE notifications SET is_read = 1/.test(sql)) {
|
|
let count = 0;
|
|
for (const n of mocks.notifications) {
|
|
if (!n.is_read) { n.is_read = 1; count++; }
|
|
}
|
|
return { changes: count };
|
|
}
|
|
return { changes: 0 };
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
import notifRouter from '../src/routes/notifications.js';
|
|
|
|
describe('Notifications', () => {
|
|
let app;
|
|
beforeEach(() => {
|
|
mocks.notifications = [];
|
|
mocks.nextId = 1;
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use('/api', notifRouter);
|
|
});
|
|
|
|
it('GET /api/notifications 返包装', async () => {
|
|
const r = await request(app).get('/api/notifications');
|
|
expect(r.status).toBe(200);
|
|
expect(r.body.ok).toBe(true);
|
|
expect(Array.isArray(r.body.data.items)).toBe(true);
|
|
expect(r.body.data.unread).toBe(0);
|
|
});
|
|
|
|
it('POST 创建 + id 是 number', async () => {
|
|
const r = await request(app).post('/api/notifications').send({ title: '测试', type: 'ocr_done' });
|
|
expect(r.status).toBe(200);
|
|
expect(typeof r.body.data.id).toBe('number');
|
|
});
|
|
|
|
it('POST 缺 title 400', async () => {
|
|
const r = await request(app).post('/api/notifications').send({});
|
|
expect(r.status).toBe(400);
|
|
});
|
|
|
|
it('GET unread=1 只返未读', async () => {
|
|
await request(app).post('/api/notifications').send({ title: 'a' });
|
|
await request(app).post('/api/notifications').send({ title: 'b' });
|
|
await request(app).post('/api/notifications/read').send({ all: true });
|
|
const r = await request(app).get('/api/notifications?unread=1');
|
|
expect(r.body.data.items).toHaveLength(0);
|
|
expect(r.body.data.unread).toBe(0);
|
|
});
|
|
|
|
it('POST /notifications/read 单条标已读', async () => {
|
|
await request(app).post('/api/notifications').send({ title: 'a' });
|
|
const list = await request(app).get('/api/notifications');
|
|
const id = list.body.data.items[0].id;
|
|
const r = await request(app).post('/api/notifications/read').send({ id });
|
|
expect(r.status).toBe(200);
|
|
const list2 = await request(app).get('/api/notifications');
|
|
expect(list2.body.data.unread).toBe(0);
|
|
});
|
|
|
|
it('POST /notifications/read {all:true} 清空所有未读', async () => {
|
|
await request(app).post('/api/notifications').send({ title: 'a' });
|
|
await request(app).post('/api/notifications').send({ title: 'b' });
|
|
const r = await request(app).post('/api/notifications/read').send({ all: true });
|
|
expect(r.status).toBe(200);
|
|
const list = await request(app).get('/api/notifications');
|
|
expect(list.body.data.unread).toBe(0);
|
|
});
|
|
}); |