@goplusvn/core 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/features/README.md +11 -1
- package/features/audit-logs/README.md +18 -0
- package/features/audit-logs/migrations/0001_init.sql +33 -0
- package/features/audit-logs/schema.prisma +20 -0
- package/features/system-jobs/README.md +40 -0
- package/features/system-jobs/migrations/0001_init.sql +47 -0
- package/features/system-jobs/schema.prisma +42 -0
- package/package.json +5 -1
- package/src/audit/__tests__/audit-context.test.ts +174 -0
- package/src/audit/__tests__/prisma-audit-extension.test.ts +426 -0
- package/src/audit/audit-actor.ts +42 -0
- package/src/audit/audit-context.ts +47 -0
- package/src/audit/entity-audit.ts +71 -0
- package/src/audit/index.ts +29 -10
- package/src/audit/prisma-audit-extension.ts +572 -0
- package/src/cron/__tests__/db-cron-manager.test.ts +316 -0
- package/src/cron/db-cron-manager.ts +459 -0
- package/src/cron/index.ts +24 -0
- package/src/system/pages/__tests__/system-audit-page.test.tsx +140 -0
- package/src/system/pages/__tests__/system-jobs-page.test.tsx +92 -0
- package/src/system/pages/system-audit-page.tsx +532 -0
- package/src/system/pages/system-jobs-page.tsx +571 -0
- package/src/ui/management/audit-log-page.tsx +12 -207
- package/src/audit/audit-manager.ts +0 -139
- package/src/audit/memory-audit-logger.ts +0 -86
- package/src/audit/types.ts +0 -50
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
configureCronManager,
|
|
5
|
+
DbCronJobManager,
|
|
6
|
+
getJobExecutionContext,
|
|
7
|
+
} from "../db-cron-manager";
|
|
8
|
+
import type { CronDb } from "../db-cron-manager";
|
|
9
|
+
|
|
10
|
+
interface JobRow {
|
|
11
|
+
name: string;
|
|
12
|
+
cronTime: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
status?: string;
|
|
15
|
+
lastRun?: Date | null;
|
|
16
|
+
nextRun?: Date | null;
|
|
17
|
+
error?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface LogRow {
|
|
21
|
+
id: string;
|
|
22
|
+
jobName: string;
|
|
23
|
+
status: string;
|
|
24
|
+
actions?: unknown;
|
|
25
|
+
durationMs?: number | null;
|
|
26
|
+
error?: string | null;
|
|
27
|
+
summary?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Fake Prisma tối thiểu — đủ để quan sát side-effect DB của manager. */
|
|
31
|
+
function makeDb(seed: JobRow[] = []) {
|
|
32
|
+
const jobs = new Map(seed.map((j) => [j.name, { ...j }]));
|
|
33
|
+
const logs: LogRow[] = [];
|
|
34
|
+
let seq = 0;
|
|
35
|
+
|
|
36
|
+
const db = {
|
|
37
|
+
systemJob: {
|
|
38
|
+
count: vi.fn(async () => jobs.size),
|
|
39
|
+
findUnique: vi.fn(
|
|
40
|
+
async ({ where }: { where: { name: string } }) =>
|
|
41
|
+
jobs.get(where.name) ?? null,
|
|
42
|
+
),
|
|
43
|
+
create: vi.fn(async ({ data }: { data: JobRow }) => {
|
|
44
|
+
jobs.set(data.name, { ...data });
|
|
45
|
+
return jobs.get(data.name);
|
|
46
|
+
}),
|
|
47
|
+
update: vi.fn(
|
|
48
|
+
async ({
|
|
49
|
+
where,
|
|
50
|
+
data,
|
|
51
|
+
}: {
|
|
52
|
+
where: { name: string };
|
|
53
|
+
data: Partial<JobRow>;
|
|
54
|
+
}) => {
|
|
55
|
+
const row = jobs.get(where.name);
|
|
56
|
+
if (!row) throw new Error(`no job ${where.name}`);
|
|
57
|
+
Object.assign(row, data);
|
|
58
|
+
return row;
|
|
59
|
+
},
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
jobExecutionLog: {
|
|
63
|
+
create: vi.fn(async ({ data }: { data: Omit<LogRow, "id"> }) => {
|
|
64
|
+
const row = { id: `log${++seq}`, ...data };
|
|
65
|
+
logs.push(row);
|
|
66
|
+
return row;
|
|
67
|
+
}),
|
|
68
|
+
update: vi.fn(
|
|
69
|
+
async ({
|
|
70
|
+
where,
|
|
71
|
+
data,
|
|
72
|
+
}: {
|
|
73
|
+
where: { id: string };
|
|
74
|
+
data: Partial<LogRow>;
|
|
75
|
+
}) => {
|
|
76
|
+
const row = logs.find((l) => l.id === where.id);
|
|
77
|
+
if (!row) throw new Error(`no log ${where.id}`);
|
|
78
|
+
Object.assign(row, data);
|
|
79
|
+
return row;
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return { db: db as unknown as CronDb, jobs, logs };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const silentLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
89
|
+
|
|
90
|
+
/** Chờ các promise nền (syncJobWithDb chạy fire-and-forget trong addJob). */
|
|
91
|
+
const flush = () => new Promise((r) => setTimeout(r, 0));
|
|
92
|
+
|
|
93
|
+
describe("DbCronJobManager — đăng ký job", () => {
|
|
94
|
+
let manager: DbCronJobManager;
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
manager = new DbCronJobManager();
|
|
98
|
+
});
|
|
99
|
+
afterEach(() => manager.stopAll());
|
|
100
|
+
|
|
101
|
+
it("chưa có row DB → tạo mới và chạy theo cờ start", async () => {
|
|
102
|
+
const { db, jobs } = makeDb();
|
|
103
|
+
configureCronManager({ db, logger: silentLogger });
|
|
104
|
+
|
|
105
|
+
manager.addJob({
|
|
106
|
+
name: "sync",
|
|
107
|
+
cronTime: "5m",
|
|
108
|
+
onTick: vi.fn(),
|
|
109
|
+
start: true,
|
|
110
|
+
});
|
|
111
|
+
await flush();
|
|
112
|
+
|
|
113
|
+
expect(jobs.get("sync")).toMatchObject({ cronTime: "5m", enabled: true });
|
|
114
|
+
expect(manager.getJob("sync")?.isRunning()).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("DB enabled=false THẮNG start:true — admin tắt thì deploy mới không tự bật", async () => {
|
|
118
|
+
const { db } = makeDb([{ name: "sync", cronTime: "5m", enabled: false }]);
|
|
119
|
+
configureCronManager({ db, logger: silentLogger });
|
|
120
|
+
|
|
121
|
+
manager.addJob({
|
|
122
|
+
name: "sync",
|
|
123
|
+
cronTime: "5m",
|
|
124
|
+
onTick: vi.fn(),
|
|
125
|
+
start: true,
|
|
126
|
+
});
|
|
127
|
+
await flush();
|
|
128
|
+
|
|
129
|
+
expect(manager.getJob("sync")?.isRunning()).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("lịch trong CODE đổi → ghi ngược lại DB", async () => {
|
|
133
|
+
const { db, jobs } = makeDb([
|
|
134
|
+
{ name: "sync", cronTime: "1h", enabled: false },
|
|
135
|
+
]);
|
|
136
|
+
configureCronManager({ db, logger: silentLogger });
|
|
137
|
+
|
|
138
|
+
manager.addJob({ name: "sync", cronTime: "15m", onTick: vi.fn() });
|
|
139
|
+
await flush();
|
|
140
|
+
|
|
141
|
+
expect(jobs.get("sync")?.cronTime).toBe("15m");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("app chưa sync feature (thiếu model) → memory-only, KHÔNG nổ", async () => {
|
|
145
|
+
configureCronManager({ db: {} as CronDb, logger: silentLogger });
|
|
146
|
+
|
|
147
|
+
manager.addJob({
|
|
148
|
+
name: "sync",
|
|
149
|
+
cronTime: "5m",
|
|
150
|
+
onTick: vi.fn(),
|
|
151
|
+
start: true,
|
|
152
|
+
});
|
|
153
|
+
await manager.init();
|
|
154
|
+
await flush();
|
|
155
|
+
|
|
156
|
+
expect(manager.getJob("sync")?.isRunning()).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("DbCronJobManager — lịch sử thực thi", () => {
|
|
161
|
+
let manager: DbCronJobManager;
|
|
162
|
+
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
manager = new DbCronJobManager();
|
|
165
|
+
});
|
|
166
|
+
afterEach(() => manager.stopAll());
|
|
167
|
+
|
|
168
|
+
it("chạy thành công: log running → success kèm actions của job", async () => {
|
|
169
|
+
const { db, jobs, logs } = makeDb([
|
|
170
|
+
{ name: "sync", cronTime: "5m", enabled: true },
|
|
171
|
+
]);
|
|
172
|
+
configureCronManager({ db, logger: silentLogger });
|
|
173
|
+
|
|
174
|
+
manager.addJob({
|
|
175
|
+
name: "sync",
|
|
176
|
+
cronTime: "5m",
|
|
177
|
+
onTick: async () => {
|
|
178
|
+
getJobExecutionContext()?.log("Đã tải giá", "12 mã");
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
await flush();
|
|
182
|
+
await manager.runJobNow("sync");
|
|
183
|
+
await flush();
|
|
184
|
+
|
|
185
|
+
expect(logs).toHaveLength(1);
|
|
186
|
+
expect(logs[0]).toMatchObject({ jobName: "sync", status: "success" });
|
|
187
|
+
expect(logs[0].actions).toEqual([
|
|
188
|
+
expect.objectContaining({ action: "Đã tải giá", details: "12 mã" }),
|
|
189
|
+
]);
|
|
190
|
+
expect(jobs.get("sync")).toMatchObject({ status: "idle", error: null });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("chạy lỗi: log failed + job.status failed + giữ nguyên message", async () => {
|
|
194
|
+
const { db, jobs, logs } = makeDb([
|
|
195
|
+
{ name: "sync", cronTime: "5m", enabled: true },
|
|
196
|
+
]);
|
|
197
|
+
configureCronManager({ db, logger: silentLogger });
|
|
198
|
+
|
|
199
|
+
manager.addJob({
|
|
200
|
+
name: "sync",
|
|
201
|
+
cronTime: "5m",
|
|
202
|
+
onTick: async () => {
|
|
203
|
+
throw new Error("upstream 503");
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
await flush();
|
|
207
|
+
await manager.runJobNow("sync"); // fire-and-forget: lỗi được nuốt ở đây
|
|
208
|
+
await flush();
|
|
209
|
+
|
|
210
|
+
expect(logs[0]).toMatchObject({ status: "failed" });
|
|
211
|
+
expect(String(logs[0].error)).toContain("upstream 503");
|
|
212
|
+
expect(jobs.get("sync")?.status).toBe("failed");
|
|
213
|
+
expect(String(jobs.get("sync")?.error)).toContain("upstream 503");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("ngữ cảnh được dọn sau khi chạy — không rò sang lần sau", async () => {
|
|
217
|
+
const { db } = makeDb([{ name: "sync", cronTime: "5m", enabled: true }]);
|
|
218
|
+
configureCronManager({ db, logger: silentLogger });
|
|
219
|
+
|
|
220
|
+
manager.addJob({ name: "sync", cronTime: "5m", onTick: vi.fn() });
|
|
221
|
+
await flush();
|
|
222
|
+
await manager.runJobNow("sync");
|
|
223
|
+
await flush();
|
|
224
|
+
|
|
225
|
+
expect(getJobExecutionContext()).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("đổi lịch xong VẪN ghi lịch sử (bản cũ mất log sau updateJob)", async () => {
|
|
229
|
+
const { db, jobs, logs } = makeDb([
|
|
230
|
+
{ name: "sync", cronTime: "5m", enabled: true },
|
|
231
|
+
]);
|
|
232
|
+
configureCronManager({ db, logger: silentLogger });
|
|
233
|
+
|
|
234
|
+
const onTick = vi.fn();
|
|
235
|
+
manager.addJob({ name: "sync", cronTime: "5m", onTick });
|
|
236
|
+
await flush();
|
|
237
|
+
|
|
238
|
+
expect(await manager.updateJob("sync", "30m", false)).toBe(true);
|
|
239
|
+
expect(jobs.get("sync")?.cronTime).toBe("30m");
|
|
240
|
+
expect(manager.getJob("sync")?.isRunning()).toBe(false);
|
|
241
|
+
|
|
242
|
+
await manager.runJobNow("sync");
|
|
243
|
+
await flush();
|
|
244
|
+
|
|
245
|
+
expect(onTick).toHaveBeenCalledTimes(1);
|
|
246
|
+
expect(logs).toHaveLength(1);
|
|
247
|
+
expect(logs[0].status).toBe("success");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("updateJob job lạ → false, không đụng DB", async () => {
|
|
251
|
+
const { db } = makeDb();
|
|
252
|
+
configureCronManager({ db, logger: silentLogger });
|
|
253
|
+
expect(await manager.updateJob("khong-co", "5m")).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("DbCronJobManager — điều khiển", () => {
|
|
258
|
+
let manager: DbCronJobManager;
|
|
259
|
+
|
|
260
|
+
beforeEach(() => {
|
|
261
|
+
manager = new DbCronJobManager();
|
|
262
|
+
});
|
|
263
|
+
afterEach(() => manager.stopAll());
|
|
264
|
+
|
|
265
|
+
it("toggleJob ghi cờ enabled xuống DB", async () => {
|
|
266
|
+
const { db, jobs } = makeDb([
|
|
267
|
+
{ name: "sync", cronTime: "5m", enabled: true },
|
|
268
|
+
]);
|
|
269
|
+
configureCronManager({ db, logger: silentLogger });
|
|
270
|
+
|
|
271
|
+
manager.addJob({ name: "sync", cronTime: "5m", onTick: vi.fn() });
|
|
272
|
+
await flush();
|
|
273
|
+
expect(manager.getJob("sync")?.isRunning()).toBe(true);
|
|
274
|
+
|
|
275
|
+
await manager.toggleJob("sync");
|
|
276
|
+
expect(manager.getJob("sync")?.isRunning()).toBe(false);
|
|
277
|
+
expect(jobs.get("sync")?.enabled).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("runJobNow/toggleJob với job chưa đăng ký → false", async () => {
|
|
281
|
+
const { db } = makeDb();
|
|
282
|
+
configureCronManager({ db, logger: silentLogger });
|
|
283
|
+
expect(await manager.runJobNow("khong-co")).toBe(false);
|
|
284
|
+
expect(await manager.toggleJob("khong-co")).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("removeJob dừng timer và quên callback", async () => {
|
|
288
|
+
const { db } = makeDb([{ name: "sync", cronTime: "5m", enabled: true }]);
|
|
289
|
+
configureCronManager({ db, logger: silentLogger });
|
|
290
|
+
|
|
291
|
+
manager.addJob({ name: "sync", cronTime: "5m", onTick: vi.fn() });
|
|
292
|
+
await flush();
|
|
293
|
+
manager.removeJob("sync");
|
|
294
|
+
|
|
295
|
+
expect(manager.getJob("sync")).toBeUndefined();
|
|
296
|
+
expect(manager.getAllJobs()).toHaveLength(0);
|
|
297
|
+
expect(await manager.runJobNow("sync")).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("getJobsInfo trả trạng thái runtime cho trang admin", async () => {
|
|
301
|
+
const { db } = makeDb([{ name: "sync", cronTime: "5m", enabled: true }]);
|
|
302
|
+
configureCronManager({ db, logger: silentLogger });
|
|
303
|
+
|
|
304
|
+
manager.addJob({ name: "sync", cronTime: "5m", onTick: vi.fn() });
|
|
305
|
+
await flush();
|
|
306
|
+
|
|
307
|
+
const info = manager.getJobsInfo();
|
|
308
|
+
expect(info).toHaveLength(1);
|
|
309
|
+
expect(info[0]).toMatchObject({
|
|
310
|
+
name: "sync",
|
|
311
|
+
cronTime: "5m",
|
|
312
|
+
isRunning: true,
|
|
313
|
+
});
|
|
314
|
+
expect(info[0].nextDate).toBeInstanceOf(Date);
|
|
315
|
+
});
|
|
316
|
+
});
|