import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; import { PGlite } from '@electric-sql/pglite'; const source = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8'); const gateway = source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)?.[1]; assert.ok(gateway, 'app gateway source must be available'); async function setup(t) { const db = new PGlite(); t.after(() => db.close()); await db.exec(` CREATE TABLE "PracticeRecord" (id int, company text, yhid text, scid text, kcid text, xxcs text, jrscb text, "updatedAt" timestamptz); CREATE TABLE "CommonModel" (company text, "modelId" int, "generalId" int, "itemId" int, title text); CREATE TABLE "VocabularyWord" (company text, id int, sy text); INSERT INTO "CommonModel" VALUES ('7pIbDBJmKx',52,101,1,'apple'), ('7pIbDBJmKx',52,102,2,'banana'), ('7pIbDBJmKx',52,103,3,'orange'), ('7pIbDBJmKx',53,901,11,'practice'), ('other',52,101,1,'foreign'); INSERT INTO "VocabularyWord" VALUES ('7pIbDBJmKx',1,'苹果'); INSERT INTO "PracticeRecord" VALUES (11,'7pIbDBJmKx','42','101','100',NULL,'1','2026-10-14'), (12,'7pIbDBJmKx','42','102','100','1','true','2026-10-13'), (13,'7pIbDBJmKx','42','103','100','2.0','1','2026-10-12'), (14,'7pIbDBJmKx','42','103','100','0','0','2026-10-11'), (15,'7pIbDBJmKx','99','101','100','0','1','2026-10-14'), (16,'other','42','101','100','0','1','2026-10-14'), (17,'7pIbDBJmKx','42','999','100','0','1','2026-10-14'); `); let queries = 0; const context = vm.createContext({ Psql: { one: async (sql, values) => { queries++; return (await db.query(sql, values)).rows[0]; }, query: async (sql, values) => { queries++; return (await db.query(sql, values)).rows; }, }, }); // Execute the actual app scope without injecting helpers from the admin gateway. vm.runInContext(gateway, context); context.authorizeRequestedUser = async (_current, uid) => { if (uid !== 42) throw new Error('无权访问该学员'); return uid; }; return { read: input => context.newWordPage({ uid: 42, ...input }, {}), queryCount: () => queries }; } test('生词本未学分页保留全量状态计数,并隔离其他学员和帐套', async t => { const { read } = await setup(t); const first = await read({ learningStatus: ' pending ', psize: 1, cpage: 1 }); assert.deepEqual(JSON.parse(JSON.stringify(first.page)), { itemCount: 2, pageCount: 2, pageIndex: 1, pageSize: 1, pendingCount: 2, completedCount: 1 }); assert.equal(first.rows[0].Title, 'apple'); assert.equal(first.rows[0].GeneralID, 901); assert.equal(first.rows[0].detail[0].sy, '苹果'); const second = await read({ learningStatus: 'pending', psize: 1, cpage: 2 }); assert.equal(second.rows[0].Title, 'banana'); }); test('生词本已学及旧版混合列表均可读取', async t => { const { read } = await setup(t); const completed = await read({ learningStatus: 'completed' }); assert.equal(completed.page.itemCount, 1); assert.equal(completed.rows[0].Title, 'orange'); const mixed = await read({}); assert.equal(mixed.page.itemCount, 3); assert.equal(mixed.rows.length, 3); }); test('生词本越界页返回空列表并保留正确计数', async t => { const { read } = await setup(t); const result = await read({ learningStatus: 'pending', psize: 30, cpage: 2 }); assert.equal(result.rows.length, 0); assert.equal(result.page.itemCount, 2); }); test('生词本拒绝非法状态和越权访问,不执行查询', async t => { const { read, queryCount } = await setup(t); await assert.rejects(read({ learningStatus: 'invalid' }), /生词学习状态无效/); await assert.rejects(read({ uid: 99 }), /无权访问/); assert.equal(queryCount(), 0); });