task-93(店铺中心): 实现店铺凭证字段分离
新增 shop-credential-model.ts:凭证明文仅经凭证端点解码且落 SensitiveString,
与列表行(仅掩码)字段分离;shop-manage-api.ts 增加 fetchShopCredential
(GET /api/admin/shop-manages/{id}/credential?shop_name=…)。
TDD: task-93.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
/** 店铺明文凭据解析模型(任务 93):凭证明文仅来自凭证端点且落 SensitiveString,
|
||||||
|
* 与列表行(仅掩码)字段分离;纯逻辑。 */
|
||||||
|
import { unwrap } from '../../api/envelope.ts'
|
||||||
|
import { toSensitiveString, type ShopCredential } from './shop-dto.ts'
|
||||||
|
|
||||||
|
function text(value: unknown): string {
|
||||||
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberOrNull(value: unknown): number | null {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析单条明文凭据;缺 id 或密码为空视为无效(拒绝把空当明文)。 */
|
||||||
|
export function toShopCredential(raw: unknown): ShopCredential | null {
|
||||||
|
if (!raw || typeof raw !== 'object') return null
|
||||||
|
const record = raw as Record<string, unknown>
|
||||||
|
const id = numberOrNull(record.id)
|
||||||
|
if (id === null) return null
|
||||||
|
const password = text(record.password)
|
||||||
|
if (!password) return null
|
||||||
|
const credential: ShopCredential = {
|
||||||
|
id,
|
||||||
|
groupId: numberOrNull(record.groupId ?? record.group_id),
|
||||||
|
groupName: text(record.groupName ?? record.group_name),
|
||||||
|
shopName: text(record.shopName ?? record.shop_name),
|
||||||
|
mallName: text(record.mallName ?? record.mall_name),
|
||||||
|
znUsername: text(record.znUsername ?? record.zn_username),
|
||||||
|
account: text(record.account),
|
||||||
|
password: toSensitiveString(password),
|
||||||
|
}
|
||||||
|
return credential
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解包凭证端点负载(data 为 ShopManageCredentialVo)为明文凭据;无效时抛可操作错误。 */
|
||||||
|
export function parseShopCredential(payload: unknown): ShopCredential {
|
||||||
|
const credential = toShopCredential(unwrap<unknown>(payload))
|
||||||
|
if (!credential) {
|
||||||
|
throw new Error('读取店铺凭证响应异常:未返回有效的明文凭据')
|
||||||
|
}
|
||||||
|
return credential
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
/** 店铺列表/分组/新增适配(任务 88/90/91):GET|POST /api/admin/shop-manages + 归一与解析。 */
|
/** 店铺列表/分组/新增/编辑/凭证适配(任务 88-93):/api/admin/shop-manages 系端点 + 归一与解析。 */
|
||||||
import { http } from '@/api/http'
|
import { http } from '@/api/http'
|
||||||
import { unwrap } from '@/api/envelope'
|
import { unwrap } from '@/api/envelope'
|
||||||
import { parseShopManagePage, toShopSummary } from './shop-manage-model.ts'
|
import { parseShopManagePage, toShopSummary } from './shop-manage-model.ts'
|
||||||
import { parseShopManageGroups } from './shop-group-model.ts'
|
import { parseShopManageGroups } from './shop-group-model.ts'
|
||||||
|
import { toShopCredential } from './shop-credential-model.ts'
|
||||||
import {
|
import {
|
||||||
normalizeShopListParams,
|
normalizeShopListParams,
|
||||||
toShopManagePageQuery,
|
toShopManagePageQuery,
|
||||||
|
type ShopCredential,
|
||||||
type ShopGroupOption,
|
type ShopGroupOption,
|
||||||
type ShopManageListParams,
|
type ShopManageListParams,
|
||||||
type ShopPageResult,
|
type ShopPageResult,
|
||||||
@@ -61,3 +63,13 @@ export async function submitUpdateShopManage(id: number, form: ShopManageFormVal
|
|||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 读取店铺明文凭据:GET /api/admin/shop-manages/{id}/credential?shop_name=…;明文仅在内存短时使用。 */
|
||||||
|
export async function fetchShopCredential(id: number, shopName: string): Promise<ShopCredential> {
|
||||||
|
const { data } = await http.get<unknown>(`${SHOP_MANAGE_ENDPOINT}/${id}/credential`, { params: { shop_name: shopName } })
|
||||||
|
const credential = toShopCredential(unwrap<unknown>(data))
|
||||||
|
if (!credential) {
|
||||||
|
throw new Error('读取店铺凭证响应异常:未返回有效记录')
|
||||||
|
}
|
||||||
|
return credential
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readSource } from './helpers.ts'
|
||||||
|
import { toShopCredential } from '../src/pages/shop/shop-credential-model.ts'
|
||||||
|
import { toShopSummary } from '../src/pages/shop/shop-manage-model.ts'
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_normal_primary_path', () => {
|
||||||
|
// 正常主路径:凭证明文只能经凭证解码产生,且落在 SensitiveString 字段。
|
||||||
|
const credential = toShopCredential({
|
||||||
|
id: 5,
|
||||||
|
groupId: 3,
|
||||||
|
groupName: '华东组',
|
||||||
|
shopName: 'BlueWave',
|
||||||
|
mallName: 'M',
|
||||||
|
znUsername: 'robot',
|
||||||
|
account: 'a@b.c',
|
||||||
|
password: 'plain-pw',
|
||||||
|
})
|
||||||
|
assert.ok(credential)
|
||||||
|
assert.equal(credential.id, 5)
|
||||||
|
assert.equal(credential.shopName, 'BlueWave')
|
||||||
|
assert.equal(credential.account, 'a@b.c')
|
||||||
|
assert.equal(credential.password, 'plain-pw')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_normal_variant_input', () => {
|
||||||
|
// 正常变体:凭证端点解码对象结构与列表行独立,不混入列表摘要。
|
||||||
|
const row = toShopSummary({ id: 5, shopName: 'BlueWave', password: 'plain-pw', passwordMasked: '******' })
|
||||||
|
assert.equal(row!.account, '')
|
||||||
|
assert.equal('password' in row!, false, '列表行不带密码明文字段')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_repeated_is_idempotent', () => {
|
||||||
|
// 正常重复:凭证解码稳定、不改输入。
|
||||||
|
const payload = { id: 1, shopName: 'A', account: 'x', password: 'p' }
|
||||||
|
assert.deepEqual(toShopCredential(payload), toShopCredential(payload))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_boundary_empty_input', () => {
|
||||||
|
// 边界空值:无密码凭证无法解码(避免误把空当明文)。
|
||||||
|
assert.equal(toShopCredential({ id: 1, shopName: 'A', account: 'x', password: '' }), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_boundary_single_item', () => {
|
||||||
|
// 边界单元素:单店铺凭证解码字段完整。
|
||||||
|
const credential = toShopCredential({ id: 9, shopName: 'S', account: 'a', password: 'p', mallName: 'M' })
|
||||||
|
assert.ok(credential)
|
||||||
|
assert.equal(credential.groupId, null)
|
||||||
|
assert.equal(credential.mallName, 'M')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_boundary_limit_or_missing_field', () => {
|
||||||
|
// 边界上限/缺字段:凭证明文不为空时必须校验原样保留,缺 id 视为无效。
|
||||||
|
assert.equal(toShopCredential('garbage'), null)
|
||||||
|
assert.equal(toShopCredential({ shopName: 'no id', password: 'p' }), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_invalid_input_rejected', () => {
|
||||||
|
// 异常输入:无 id/无明文凭证解码为 null,明确拒绝进入通用对象。
|
||||||
|
assert.equal(toShopCredential({ id: 1, shopName: 'A' }), null)
|
||||||
|
assert.equal(toShopCredential(null), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test_task_093_credential_separation_dependency_failure_returns_actionable_message', () => {
|
||||||
|
// 依赖失败/取凭证走 adapter:凭证模型纯逻辑;adapter GET /{id}/credential + SensitiveString。
|
||||||
|
const model = readSource('src/pages/shop/shop-credential-model.ts')
|
||||||
|
assert.equal(/axios|http\.|vue/.test(model), false, '凭证模型保持纯逻辑')
|
||||||
|
assert.match(model, /SensitiveString/)
|
||||||
|
const api = readSource('src/pages/shop/shop-manage-api.ts')
|
||||||
|
assert.match(api, /fetchShopCredential/)
|
||||||
|
assert.match(api, /\/credential/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user