From 0a1e778ef634a1507b3d3d7c7a0c22885a04a71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 16:49:25 +0800 Subject: [PATCH] =?UTF-8?q?task-94(=E5=BA=97=E9=93=BA=E4=B8=AD=E5=BF=83):?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E5=BA=97=E9=93=BA=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E6=98=8E=E6=96=87=E7=9F=AD=E6=97=B6=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-credential-reveal.ts:复用 secret-mask 会话,店铺密码读取后默认 5 秒短时展示,读取失败/离开即清空,明文不写持久化/日志。 TDD: task-94.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/shop/shop-credential-reveal.ts | 38 ++++++++++ admin-frontend-vue/tests/task-94.test.ts | 72 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 admin-frontend-vue/src/pages/shop/shop-credential-reveal.ts create mode 100644 admin-frontend-vue/tests/task-94.test.ts diff --git a/admin-frontend-vue/src/pages/shop/shop-credential-reveal.ts b/admin-frontend-vue/src/pages/shop/shop-credential-reveal.ts new file mode 100644 index 00000000..0bec3573 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-credential-reveal.ts @@ -0,0 +1,38 @@ +/** 店铺密码短时揭示会话(任务 94):读取明文后只在内存按短窗口(默认 5 秒)展示, + * 读取失败或离开页面即清空;纯逻辑,明文不写持久化/日志。 */ +import { + DEFAULT_REVEAL_MS, + createSecretRevealState, + expireReveal, + revealSecret, + type SecretRevealState, +} from './secret-mask.ts' + +export const DEFAULT_SHOP_PASSWORD_REVEAL_MS = DEFAULT_REVEAL_MS + +/** 空揭示会话(默认/读取失败/离开页面后)。 */ +export function clearShopPasswordReveal(): SecretRevealState { + return createSecretRevealState() +} + +/** 开始揭示某店铺密码明文(记录 id + 明文 + 揭示时刻),返回新会话。 */ +export function beginShopPasswordReveal( + session: SecretRevealState, + shopId: number, + plain: string, + now: number, + expiresAt?: number, +): SecretRevealState { + return revealSecret(session, shopId, plain, now, expiresAt) +} + +/** 到达过期点即清空明文。 */ +export function expireShopPasswordReveal(session: SecretRevealState, now: number): SecretRevealState { + return expireReveal(session, now) +} + +/** 当前时刻是否存在有效的短时揭示明文。 */ +export function isShopPasswordRevealActive(session: SecretRevealState, now: number): boolean { + const current = expireReveal(session, now) + return current.keyId !== null && current.token !== undefined && current.token !== '' +} diff --git a/admin-frontend-vue/tests/task-94.test.ts b/admin-frontend-vue/tests/task-94.test.ts new file mode 100644 index 00000000..f844d1e6 --- /dev/null +++ b/admin-frontend-vue/tests/task-94.test.ts @@ -0,0 +1,72 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + DEFAULT_SHOP_PASSWORD_REVEAL_MS, + beginShopPasswordReveal, + clearShopPasswordReveal, + expireShopPasswordReveal, + isShopPasswordRevealActive, +} from '../src/pages/shop/shop-credential-reveal.ts' + +test('test_task_094_credential_reveal_normal_primary_path', () => { + // 正常主路径:主动揭示后短时(默认 5 秒)拿到明文,超时自动清空。 + const session = beginShopPasswordReveal(clearShopPasswordReveal(), 5, 'plain-pw', 1000) + assert.equal(isShopPasswordRevealActive(session, 1000), true) + assert.equal(session.token, 'plain-pw') + const expired = expireShopPasswordReveal(session, 1000 + DEFAULT_SHOP_PASSWORD_REVEAL_MS + 1) + assert.equal(expired.token, undefined, '超时明文被清空') + assert.equal(isShopPasswordRevealActive(expired, 7000), false) +}) + +test('test_task_094_credential_reveal_normal_variant_input', () => { + // 正常变体:多个店铺各自揭示互不串扰,切换记录即替换明文。 + const first = beginShopPasswordReveal(clearShopPasswordReveal(), 1, 'pw1', 0) + const second = beginShopPasswordReveal(first, 2, 'pw2', 0) + assert.equal(second.keyId, 2) + assert.equal(second.token, 'pw2') +}) + +test('test_task_094_credential_reveal_repeated_is_idempotent', () => { + // 正常重复:同一时刻重复揭示结果稳定。 + const a = beginShopPasswordReveal(clearShopPasswordReveal(), 3, 'pw', 2000) + assert.deepEqual(beginShopPasswordReveal(a, 3, 'pw', 2000), a) +}) + +test('test_task_094_credential_reveal_boundary_empty_input', () => { + // 边界空值:清空后无活动揭示、无明文残留。 + const session = clearShopPasswordReveal() + assert.equal(session.keyId, null) + assert.equal(isShopPasswordRevealActive(session, 0), false) +}) + +test('test_task_094_credential_reveal_boundary_single_item', () => { + // 边界单元素:单次揭示在窗口内有效,离开(调用清空)立即释放。 + const session = beginShopPasswordReveal(clearShopPasswordReveal(), 9, 'pw9', 0, 0 + DEFAULT_SHOP_PASSWORD_REVEAL_MS) + assert.equal(isShopPasswordRevealActive(session, DEFAULT_SHOP_PASSWORD_REVEAL_MS - 1), true) + const gone = clearShopPasswordReveal() + assert.equal(isShopPasswordRevealActive(gone, 0), false) +}) + +test('test_task_094_credential_reveal_boundary_limit_or_missing_field', () => { + // 边界上限:跨记录的揭示不会在离开/失败后残留在其它状态里。 + const session = beginShopPasswordReveal(clearShopPasswordReveal(), 4, 'x'.repeat(600), 0) + assert.equal(session.token!.length, 600) + assert.equal(clearShopPasswordReveal().token, undefined) +}) + +test('test_task_094_credential_reveal_invalid_input_rejected', () => { + // 异常输入:读取失败路径调用清空后不残留明文。 + const session = beginShopPasswordReveal(clearShopPasswordReveal(), 7, 'pw7', 0) + const afterFailure = clearShopPasswordReveal() + assert.equal(isShopPasswordRevealActive(afterFailure, 0), false) + assert.equal(afterFailure.token, undefined, '失败后无明文残留') + assert.equal(isShopPasswordRevealActive(session, 0), true, '原会话不受影响(每次新会话)') +}) + +test('test_task_094_credential_reveal_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:揭示会话纯逻辑复用短时语义;不写持久化/日志。 + const mod = readSource('src/pages/shop/shop-credential-reveal.ts') + assert.equal(/axios|http\.|vue|console\./.test(mod), false, '揭示会话保持纯逻辑') + assert.match(mod, /DEFAULT_SHOP_PASSWORD_REVEAL_MS/) +})