Files
CarLog/server/test/routes.notifications.test.js
T
wsh5485 fe17886ac4 feat: 洗车管理系统 v2.8 — 个人 detailer 单用户全栈应用
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 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
2026-06-20 21:11:54 +08:00

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);
});
});