Files
i/server/test/routes.notifications.test.js
wsh5485 65b0bb04f8 feat: import CarLog v2.8 code + dev plan
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline:
- server/src/ (13 个路由 + middleware + services + config)
- server/migrations/ (0001~0018 共 18 个迁移 + mysql)
- server/test/ (12 文件 101 测试)
- client/src/ (20 个 view + components + stores + api + composables)
- client/public/ + client/scripts/
- 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql)
- .husky/pre-commit (git hooks)
- docs/install/ (宝塔部署文档)

不含:
- node_modules/ (本地 npm install)
- .env (敏感, 走 .env.example)
- *.zip / *.log / *.sqlite / .DS_Store

新增文档 docs/DEV-PLAN.md:
- Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view)
- Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单)
- Phase 3: 验证 (测试 + E2E + DB 完整性)
- 交付清单 + commit 模板 + 给 Mavis review 的材料

后续 Trae 实施, 提交后我 code review + 跑测试。
2026-06-20 22:30:19 +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);
});
});