// Pure UTS boundary tests. No network, database, real session or runtime installation. const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const ts = require(process.env.AIRZ_TYPESCRIPT_PATH || 'D:/Develop/HBuilderX/plugins/uniapp-uts-v1/node_modules/@dcloudio/uni-uts-v1/lib/typescript/lib/typescript.js'); const root = path.resolve(__dirname, '..'); class JsonObject { constructor(value = {}) { Object.assign(this, value); } getString(key) { return typeof this[key] === 'string' ? this[key] : null; } getNumber(key) { return typeof this[key] === 'number' ? this[key] : null; } getBoolean(key) { return typeof this[key] === 'boolean' ? this[key] : null; } } const json = value => Array.isArray(value) ? value.map(json) : value && typeof value === 'object' ? new JsonObject(Object.fromEntries(Object.entries(value).map(([k, v]) => [k, json(v)]))) : value; let session = json({ token: 'synthetic-token-a', userId: '1' }); let calls = []; let reply; class ApiError extends Error { constructor(message, status, errorCode) { super(message); this.status = status; this.errorCode = errorCode; } } const api = { ApiRequestError: ApiError, getSession: () => session, requestJson: async (url, method, payload) => { calls.push({ url, method, payload: JSON.parse(JSON.stringify(payload)) }); return json(await reply(url, method, payload)); } }; const modules = {}; const pages = []; const redirects = []; const styles = { pageStyle: () => '', contentStyle: () => '', statusStyle: () => '', bottomStyle: () => '' }; let pageShow, pageHide, pageLoad; const pageUni = { showToast: () => {}, showModal: options => options.success?.({ confirm: true }), redirectTo: ({ url }) => redirects.push(url), navigateTo: ({ url }) => pages.push(url) }; api.saveSession = (token, phone, userId, expiresAt) => { session = json({ token, phone, userId, expiresAt }); }; api.API_BASE_URL = 'https://synthetic.invalid/api/v1'; function load(relative) { if (modules[relative]) return modules[relative]; const file = path.join(root, relative); if (!fs.existsSync(file)) return {}; const output = ts.transpileModule(fs.readFileSync(file, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText; const exports = {}; modules[relative] = exports; vm.runInNewContext(output, { exports, UTSJSONObject: JsonObject, Date, Math, Number, Error, require: name => name.endsWith('/api.uts') || name === './api.uts' ? api : load(path.posix.normalize(path.posix.join(path.posix.dirname(relative), name))) }, { filename: relative }); return exports; } function componentPage(relative, bindings) { pageShow = null; pageHide = null; pageLoad = null; const source = fs.readFileSync(path.join(root, relative), 'utf8').match(/', contentHash: hash, effectiveAt: '2026-10-01T00:00:00Z' }; const consent = { consentId: '9', scope: 'account-terms', agreementCode: 'terms', agreementVersion: '2026.10-v1', contentHash: hash, grantedAt: '2026-10-12T00:00:00Z', revokedAt: null, canRevoke: false }; (async () => { await test('unknown agreement code cannot produce a route', () => { assert.equal(typeof route.agreementRoute, 'function', 'agreement route validation is not implemented'); for (const code of ['', 'unknown', 'terms?x=1', 'https://outside.invalid']) assert.equal(route.agreementRoute(code), ''); }); await test('versioned agreement links preserve the immutable version', () => { assert.equal(route.agreementRoute('terms', '2026.10-v1'), '/pages/agreement/agreement?code=terms&version=2026.10-v1'); assert.equal(route.agreementRoute('privacy'), '/pages/agreement/agreement?code=privacy'); for (const version of ['x&code=terms', '\n', 'x'.repeat(65), '../draft']) assert.equal(route.agreementRoute('terms', version), ''); }); await test('scope mapping separates account confirmations from optional grants', () => { assert.equal(route.agreementScope('terms'), 'account-terms'); assert.equal(route.agreementScope('privacy'), 'account-privacy'); assert.equal(route.agreementScope('document-use'), 'documents'); assert.equal(route.agreementScope('data-authorization'), 'data'); assert.equal(route.optionalConsentScope('account-privacy'), false); assert.equal(route.optionalConsentScope('documents'), true); }); await test('BIGINT agreement ids stay as strings and reject overflow', () => { for (const value of ['1', '9223372036854775807']) assert.equal(route.agreementRecordIdValid(value), true); for (const value of ['0', '01', '-1', '9223372036854775808', '99999999999999999999']) assert.equal(route.agreementRecordIdValid(value), false); }); await test('account guard rejects another account, new token and anonymous state', () => { assert.equal(route.sameConsentSession('a', '1', 'a', '1'), true); for (const args of [['a', '1', 'b', '1'], ['a', '1', 'a', '2'], ['', '', '', ''], ['a', '1', '', '']]) assert.equal(route.sameConsentSession(...args), false); }); await test('public fetch emits only a validated code and requested version', async () => { calls = []; reply = () => terms; const result = await service.fetchAgreement('terms', '2026.10-v1'); assert.equal(result.content, terms.content); assert.deepEqual(calls, [{ url: '/agreements/terms', method: 'GET', payload: { version: '2026.10-v1' } }]); await assert.rejects(service.fetchAgreement('unknown')); assert.equal(calls.length, 1); }); await test('wrong version, scope, hash and invalid timestamp cannot become seen agreement', async () => { for (const patch of [{ version: 'old' }, { code: 'privacy' }, { scope: 'data' }, { contentHash: 'bad' }, { content: '' }, { effectiveAt: 'bad' }]) { reply = () => ({ ...terms, ...patch }); await assert.rejects(service.fetchAgreement('terms', '2026.10-v1')); } }); await test('not-published detection does not hide a network or server failure', () => { assert.equal(service.agreementUnavailable(new ApiError('未发布', 404, 'AGREEMENT_NOT_AVAILABLE')), true); for (const error of [new Error('network'), new ApiError('server', 503, 'AGREEMENT_NOT_AVAILABLE'), new ApiError('not found', 404, 'OTHER')]) assert.equal(service.agreementUnavailable(error), false); }); await test('grant sends the exact seen version and stable request key', async () => { calls = []; reply = () => consent; const result = await service.grantConsent(terms, 'consent_request_123'); assert.equal(result.consentId, '9'); assert.deepEqual(calls, [{ url: '/consents', method: 'POST', payload: { scope: 'account-terms', agreementCode: 'terms', agreementVersion: '2026.10-v1', requestId: 'consent_request_123' } }]); }); await test('anonymous or malformed consent cannot reach the network', async () => { calls = []; session = null; await assert.rejects(service.grantConsent(terms, 'consent_request_123')); session = json({ token: 'synthetic-token-a', userId: '1' }); await assert.rejects(service.grantConsent(terms, 'bad')); await assert.rejects(service.grantConsent({ ...terms, scope: 'data' }, 'consent_request_123')); assert.equal(calls.length, 0); }); await test('consent request keys require the server minimum of sixteen characters', async () => { calls = []; reply = () => consent; for (const value of ['abcdefgh', 'abcdefghijklmno', 'a'.repeat(65), 'request key spaces']) await assert.rejects(service.grantConsent(terms, value)); assert.equal(calls.length, 0); await service.grantConsent(terms, 'abcdefghijklmnop'); assert.equal(calls.length, 1); }); await test('a response from the previous session cannot confirm consent', async () => { reply = () => { session = json({ token: 'synthetic-token-b', userId: '1' }); return consent; }; await assert.rejects(service.grantConsent(terms, 'consent_request_123')); session = json({ token: 'synthetic-token-a', userId: '1' }); }); await test('receipt must match seen hash, scope and version', async () => { for (const patch of [{ agreementVersion: 'old' }, { contentHash: 'b'.repeat(64) }, { scope: 'data' }, { agreementCode: 'privacy' }, { revokedAt: '2026-10-12T01:00:00Z' }]) { reply = () => ({ ...consent, ...patch }); await assert.rejects(service.grantConsent(terms, 'consent_request_123')); } }); await test('a revoked optional grant replay is a valid receipt of the original command', async () => { session = json({ token: 'synthetic-token-a', userId: '1' }); const agreement = { ...terms, code: 'document-use', scope: 'documents' }; reply = () => ({ ...consent, scope: 'documents', agreementCode: 'document-use', revokedAt: '2026-10-12T01:00:00Z', canRevoke: false }); const result = await service.grantConsent(agreement, 'original_request_123'); assert.equal(result.revokedAt, '2026-10-12T01:00:00Z'); assert.equal(result.canRevoke, false); }); await test('account confirmations cannot be revoked even with an incorrect permission flag', async () => { calls = []; await assert.rejects(service.revokeConsent({ ...consent, canRevoke: true }, 'revoke_request_123')); assert.equal(calls.length, 0); }); await test('optional revocation preserves the original receipt and only sends a request key', async () => { const item = { ...consent, scope: 'documents', agreementCode: 'document-use', canRevoke: true, revokedAt: '' }; calls = []; reply = () => ({ ...item, canRevoke: false, revokedAt: '2026-10-12T01:00:00Z' }); assert.equal((await service.revokeConsent(item, 'revoke_request_123')).revokedAt, '2026-10-12T01:00:00Z'); assert.deepEqual(calls, [{ url: '/consents/9/revoke', method: 'POST', payload: { requestId: 'revoke_request_123' } }]); }); await test('list validates scope and pagination before querying', async () => { calls = []; reply = () => ({ items: [consent], page: 1, pageSize: 20, total: 1, hasMore: false }); const result = await service.fetchConsents('account-terms', 1); assert.equal(result.items.length, 1); assert.deepEqual(calls[0], { url: '/consents', method: 'GET', payload: { scope: 'account-terms', page: '1', pageSize: '20' } }); for (const args of [['unknown', 1], ['', 0], ['', 1.5], ['', 1, 51]]) await assert.rejects(service.fetchConsents(...args)); assert.equal(calls.length, 1); }); await test('wrong-page and wrong-scope list responses cannot replace user records', async () => { for (const patch of [{ page: 2 }, { pageSize: 10 }, { total: -1 }, { hasMore: null }, { items: [{ ...consent, scope: 'data', agreementCode: 'data-authorization' }] }]) { reply = () => ({ items: [consent], page: 1, pageSize: 20, total: 1, hasMore: false, ...patch }); await assert.rejects(service.fetchConsents('account-terms', 1)); } }); await test('failed post-login consent retains authentication and retries the same command without logging in again', async () => { session = null; calls = []; redirects.length = 0; let fail = true; reply = (url, method, payload) => { if (url === '/agreements/terms') return terms; if (url === '/agreements/privacy') return { ...terms, revisionId: '2', code: 'privacy', scope: 'account-privacy' }; if (url === '/auth/password-login') return { token: 'synthetic-login-token', userId: '1', expiresAt: '2099-01-01T00:00:00Z' }; if (url === '/consents') { if (fail) { fail = false; throw new Error('synthetic lost receipt'); } return { ...consent, agreementCode: payload.agreementCode, scope: payload.scope, agreementVersion: payload.agreementVersion }; } throw Error('Unexpected login request ' + url); }; const page = loginPage(); if (pageShow) await pageShow(); if (page.loadAgreements) await page.loadAgreements(); page.phone.value = '13800000000'; page.password.value = 'synthetic-password'; if (page.toggleAgreement) page.toggleAgreement(); else page.agreed.value = true; await page.login(); assert.equal(session.getString('token'), 'synthetic-login-token'); assert.equal(redirects.length, 0, 'login must wait for consent receipts'); assert.ok(page.errorMessage.value.length > 0, 'failed consent remains visible for manual retry'); await page.login(); const authCalls = calls.filter(call => call.url === '/auth/password-login'); const consentCalls = calls.filter(call => call.url === '/consents'); assert.equal(authCalls.length, 1, 'manual retry must not authenticate again'); assert.equal(consentCalls.length, 3, 'one failed grant, its retry and privacy confirmation'); assert.equal(consentCalls[0].payload.requestId, consentCalls[1].payload.requestId, 'unknown result must use the original key'); assert.notEqual(consentCalls[1].payload.requestId, consentCalls[2].payload.requestId); assert.deepEqual(redirects, ['/pages/home/home']); }); await test('unpublished agreements allow existing password login without manufacturing consent records', async () => { session = null; calls = []; redirects.length = 0; reply = url => { if (url.startsWith('/agreements/')) throw new ApiError('not published', 404, 'AGREEMENT_NOT_AVAILABLE'); if (url === '/auth/password-login') return { token: 'synthetic-existing-token', userId: '1', expiresAt: '2099-01-01T00:00:00Z' }; throw Error('No consent write is allowed before publication: ' + url); }; const page = loginPage(); await pageShow(); page.phone.value = '13800000000'; page.password.value = 'synthetic-password'; await page.login(); assert.equal(calls.filter(call => call.url === '/auth/password-login').length, 1); assert.equal(calls.filter(call => call.url === '/consents').length, 0); assert.deepEqual(redirects, ['/pages/home/home']); }); await test('network failure loading agreements is never treated as not published', async () => { session = null; calls = []; redirects.length = 0; reply = () => { throw new Error('synthetic network failure'); }; const page = loginPage(); await pageShow(); page.phone.value = '13800000000'; page.password.value = 'synthetic-password'; page.agreed.value = true; await page.login(); assert.equal(calls.filter(call => call.url.startsWith('/auth/')).length, 0); assert.equal(redirects.length, 0); assert.ok(page.errorMessage.value.length > 0); }); await test('an account change while confirmation is pending never grants the previous choice to the new account', async () => { session = null; calls = []; redirects.length = 0; reply = (url, method, payload) => { if (url === '/agreements/terms') return terms; if (url === '/agreements/privacy') return { ...terms, revisionId: '2', code: 'privacy', scope: 'account-privacy' }; if (url === '/auth/password-login') return { token: 'synthetic-login-token', userId: '1', expiresAt: '2099-01-01T00:00:00Z' }; if (url === '/consents') throw new Error('synthetic lost receipt'); throw Error('Unexpected account-change request ' + url); }; const page = loginPage(); await pageShow(); page.phone.value = '13800000000'; page.password.value = 'synthetic-password'; page.toggleAgreement(); await page.login(); const writes = calls.filter(call => call.url === '/consents').length; session = json({ token: 'synthetic-other-token', userId: '2' }); await page.login(); assert.equal(calls.filter(call => call.url === '/consents').length, writes); assert.equal(page.agreed.value, false, 'old account checkbox must be cleared'); assert.equal(redirects.length, 0); }); await test('late public agreement responses cannot replace a newer loaded version', async () => { session = null; calls = []; let release; reply = () => new Promise(resolve => { release = resolve; }); const page = componentPage('pages/agreement/agreement.uvue', 'reload, agreement, available, loading, error'); pageLoad({ code: 'terms' }); pageShow(); await flush(); assert.equal(typeof release, 'function'); reply = () => ({ ...terms, revisionId: '3', version: '2026.10-v2', content: '新合成正文' }); await page.reload(); release(terms); await flush(); assert.equal(page.agreement.value.version, '2026.10-v2'); assert.equal(page.agreement.value.content, '新合成正文'); assert.equal(page.available.value, true); }); await test('an agreement response arriving after leaving cannot fill the hidden page', async () => { session = null; let release; reply = () => new Promise(resolve => { release = resolve; }); const page = componentPage('pages/agreement/agreement.uvue', 'agreement, available, error'); pageLoad({ code: 'terms' }); pageShow(); await flush(); pageHide(); release(terms); await flush(); assert.equal(page.available.value, false); assert.equal(page.agreement.value.content, ''); assert.equal(page.error.value, ''); }); await test('optional revocation retries an unknown receipt with the original request key', async () => { session = json({ token: 'synthetic-token-a', userId: '1' }); calls = []; const item = { ...consent, scope: 'documents', agreementCode: 'document-use', canRevoke: true, revokedAt: null }; let failed = false; reply = (url, method) => { if (url === '/consents' && method === 'GET') return { items: [item], page: 1, pageSize: 20, total: 1, hasMore: false }; if (url === '/consents/9/revoke') { if (!failed) { failed = true; throw Error('synthetic lost revoke receipt'); } return { ...item, revokedAt: '2026-10-12T01:00:00Z', canRevoke: false }; } throw Error('Unexpected revocation request ' + url); }; const page = componentPage('packages/account/consents/consents.uvue', 'records, pending, actionError, confirmRevoke, retryRevoke'); pageShow(); await flush(); assert.equal(page.records.value.length, 1); page.confirmRevoke(page.records.value[0]); await flush(); assert.ok(page.pending.value); assert.ok(page.actionError.value.length > 0); await page.retryRevoke(); const writes = calls.filter(call => call.url === '/consents/9/revoke'); assert.equal(writes.length, 2); assert.equal(writes[0].payload.requestId, writes[1].payload.requestId); assert.equal(page.records.value[0].revokedAt, '2026-10-12T01:00:00Z'); assert.equal(page.pending.value, null); }); await test('late consent records from a former account never enter the new account view', async () => { session = json({ token: 'synthetic-token-a', userId: '1' }); redirects.length = 0; let release; reply = () => new Promise(resolve => { release = resolve; }); const page = componentPage('packages/account/consents/consents.uvue', 'records, error'); pageShow(); await flush(); session = json({ token: 'synthetic-token-b', userId: '2' }); release({ items: [consent], page: 1, pageSize: 20, total: 1, hasMore: false }); await flush(); assert.equal(page.records.value.length, 0); assert.equal(page.error.value, ''); assert.deepEqual(redirects, ['/packages/account/profile/profile']); }); await test('pagination never renders duplicate consent ids after records shift between requests', async () => { session = json({ token: 'synthetic-token-a', userId: '1' }); const first = Array.from({ length: 20 }, (_, index) => ({ ...consent, consentId: String(index + 1) })); reply = (url, method, payload) => payload.page === '1' ? { items: first, page: 1, pageSize: 20, total: 21, hasMore: true } : { items: [{ ...consent, consentId: '20' }, { ...consent, consentId: '21' }], page: 2, pageSize: 20, total: 21, hasMore: false }; const page = componentPage('packages/account/consents/consents.uvue', 'records, loadMore'); pageShow(); await flush(); page.loadMore(); await flush(); assert.equal(page.records.value.length, 21); assert.equal(new Set(page.records.value.map(item => item.consentId)).size, 21); }); await test('a list started before revocation cannot restore the revoked consent after its receipt', async () => { session = json({ token: 'synthetic-token-a', userId: '1' }); const item = { ...consent, scope: 'documents', agreementCode: 'document-use', canRevoke: true, revokedAt: null }; let listCount = 0, release, releaseRevoke, revoked = false; reply = (url, method) => { if (url === '/consents' && method === 'GET') { listCount++; const currentItem = revoked ? { ...item, canRevoke: false, revokedAt: '2026-10-12T01:00:00Z' } : { ...item }; const result = { items: [currentItem], page: 1, pageSize: 20, total: 1, hasMore: false }; if (listCount === 2) return new Promise(resolve => { release = () => resolve(result); }); return result; } if (url === '/consents/9/revoke') return new Promise(resolve => { releaseRevoke = () => { revoked = true; resolve({ ...item, canRevoke: false, revokedAt: '2026-10-12T01:00:00Z' }); }; }); throw Error('Unexpected revoke/list race request ' + url); }; const page = componentPage('packages/account/consents/consents.uvue', 'records, loading, reload, confirmRevoke'); pageShow(); await flush(); page.confirmRevoke(page.records.value[0]); await flush(); page.reload(); await flush(); assert.equal(page.loading.value, true); releaseRevoke(); await flush(); release(); await flush(); const visible = page.records.value.find(row => row.consentId === '9'); assert.ok(visible, 'receipt should retain the record while invalidating its in-flight refresh'); assert.equal(visible.revokedAt, '2026-10-12T01:00:00Z'); assert.equal(visible.canRevoke, false); assert.equal(page.loading.value, false); }); await test('returning to an optional page settles a revoked original grant and waits for a new explicit click', async () => { session = json({ token: 'synthetic-token-a', userId: '1' }); calls = []; const agreement = { ...terms, code: 'document-use', scope: 'documents' }; const original = { ...consent, agreementCode: 'document-use', scope: 'documents', revokedAt: null, canRevoke: true }; let originalKey = '', revoked = false; reply = (url, method, payload) => { if (url === '/agreements/document-use') return agreement; if (url === '/consents' && method === 'POST') { if (!originalKey) { originalKey = payload.requestId; throw Error('synthetic accepted but lost receipt'); } if (payload.requestId === originalKey) return { ...original, revokedAt: revoked ? '2026-10-12T01:00:00Z' : null, canRevoke: !revoked }; return { ...original, consentId: '10' }; } throw Error('Unexpected optional replay request ' + url); }; const page = componentPage('pages/agreement/agreement.uvue', 'confirmConsent, pendingId, granted, actionMessage'); pageLoad({ code: 'document-use' }); pageShow(); await flush(); await page.confirmConsent(); assert.ok(page.pendingId.value); pageHide(); revoked = true; pageShow(); await flush(); assert.equal(calls.filter(call => call.url === '/consents').length, 1, 'return must not automatically submit another grant'); await page.confirmConsent(); assert.equal(page.pendingId.value, '', 'revoked replay settles the pending command'); assert.equal(page.granted.value, false, 'revoked original receipt must not show currently granted'); assert.match(page.actionMessage.value, /此前同意已撤销/); await page.confirmConsent(); const writes = calls.filter(call => call.url === '/consents'); assert.equal(writes.length, 3); assert.equal(writes[0].payload.requestId, writes[1].payload.requestId); assert.notEqual(writes[1].payload.requestId, writes[2].payload.requestId); assert.equal(page.granted.value, true); }); await test('returning after revoking a successful optional grant restores an explicit fresh consent action', async () => { session = json({ token: 'synthetic-token-a', userId: '1' }); calls = []; const agreement = { ...terms, code: 'document-use', scope: 'documents' }; reply = (url, method) => { if (url === '/agreements/document-use') return agreement; if (url === '/consents' && method === 'POST') return { ...consent, agreementCode: 'document-use', scope: 'documents', revokedAt: null, canRevoke: true }; throw Error('Unexpected successful-grant return request ' + url); }; const page = componentPage('pages/agreement/agreement.uvue', 'confirmConsent, pendingId, granted'); pageLoad({ code: 'document-use' }); pageShow(); await flush(); await page.confirmConsent(); assert.equal(page.granted.value, true); assert.equal(page.pendingId.value, ''); pageHide(); pageShow(); await flush(); assert.equal(page.granted.value, false, 'return must expose an explicit consent action instead of retaining old success state'); assert.equal(calls.filter(call => call.url === '/consents').length, 1, 'return must not automatically regrant'); await page.confirmConsent(); const writes = calls.filter(call => call.url === '/consents'); assert.equal(writes.length, 2); assert.notEqual(writes[0].payload.requestId, writes[1].payload.requestId); }); console.log(checks + ' checks passed; no network calls.'); })().catch(error => { console.error(error); process.exitCode = 1; });