Просмотр исходного кода

fix(mobile): restore wordbook gateway status filtering

彭峰 8 часов назад
Родитель
Сommit
648b78fac0
2 измененных файлов с 82 добавлено и 1 удалено
  1. 1 1
      scripts/deploy-admin-functions.mjs
  2. 81 0
      scripts/tests/wordbook-gateway.test.mjs

+ 1 - 1
scripts/deploy-admin-functions.mjs

@@ -2615,7 +2615,7 @@ async function recordDetailData(input, current) {
 async function newWordPage(input, current) {
   const uid = await authorizeRequestedUser(current, input.uid);
   const paging = largePageInput(input);
-  const status = text(input.learningStatus);
+  const status = String(input.learningStatus ?? '').trim();
   if (status && status !== 'pending' && status !== 'completed') fail(400, '生词学习状态无效');
   const values = [DEFAULT_COMPANY_ID, String(uid)];
   const from = ' FROM "PracticeRecord" p JOIN "CommonModel" c ON c."company" = $1 AND CAST(c."modelId" AS text) = \'52\' AND CAST(c."generalId" AS text) = CAST(p."scid" AS text)';

+ 81 - 0
scripts/tests/wordbook-gateway.test.mjs

@@ -0,0 +1,81 @@
+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);
+});