65b0bb04f8
把 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 + 跑测试。
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);
|
|
});
|
|
}); |