93033e94db
新增 src/pages/shop/shop-dto.ts 纯逻辑 DTO:ShopKeyItem/ShopSummary/ ShopCredential/SensitiveString 等类型边界与分页归一/查询序列化(店铺接口 camelCase);凭证明文与紫鸟令牌按敏感字段建模,禁止进入通用日志。 TDD: task-81.test.ts 8 用例先 RED 后 GREEN。
119 lines
5.2 KiB
TypeScript
119 lines
5.2 KiB
TypeScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readSource } from './helpers.ts'
|
|
import {
|
|
SHOP_PAGE_DEFAULT_SIZE,
|
|
SHOP_PAGE_MAX_SIZE,
|
|
SHOP_PAGE_MIN_PAGE,
|
|
emptyShopKeyPageResult,
|
|
emptyShopPageResult,
|
|
normalizeKeyListParams,
|
|
normalizeShopListParams,
|
|
toKeyPageQuery,
|
|
toShopManagePageQuery,
|
|
} from '../src/pages/shop/shop-dto.ts'
|
|
|
|
test('test_task_081_shop_dto_normal_primary_path', () => {
|
|
// 正常主路径:完整店铺筛选/分页归一后序列化为 Java camelCase 查询参数(page/pageSize/groupId/shopName)。
|
|
const params = normalizeShopListParams({
|
|
page: 2,
|
|
pageSize: 20,
|
|
groupId: 3,
|
|
shopName: ' 主 店 ',
|
|
})
|
|
assert.equal(params.page, 2)
|
|
assert.equal(params.pageSize, 20)
|
|
assert.equal(params.groupId, 3)
|
|
assert.equal(params.shopName, '主 店')
|
|
const query = toShopManagePageQuery(params)
|
|
assert.equal(query.page, 2)
|
|
assert.equal(query.pageSize, 20)
|
|
assert.equal(query.groupId, 3)
|
|
assert.equal(query.shopName, '主 店')
|
|
})
|
|
|
|
test('test_task_081_shop_dto_normal_variant_input', () => {
|
|
// 正常变体:店铺密钥列表只允许 page/pageSize,不携带分组/店铺名维度。
|
|
const params = normalizeKeyListParams({ page: 3, pageSize: 30 })
|
|
const query = toKeyPageQuery(params)
|
|
assert.deepEqual(Object.keys(query).sort(), ['page', 'pageSize'])
|
|
assert.equal(query.page, 3)
|
|
assert.equal(query.pageSize, 30)
|
|
// 店铺列表可选条件缺省时不下发对应字段。
|
|
const manageQuery = toShopManagePageQuery(normalizeShopListParams({ page: 1, pageSize: 15 }))
|
|
assert.deepEqual(Object.keys(manageQuery).sort(), ['page', 'pageSize'])
|
|
})
|
|
|
|
test('test_task_081_shop_dto_normal_repeated_operation_is_idempotent', () => {
|
|
// 正常重复:归一不改输入、结果稳定;空结果默认值相互独立。
|
|
const input = { page: 2, pageSize: 25, groupId: 7, shopName: 'A店' }
|
|
assert.deepEqual(normalizeShopListParams(input), normalizeShopListParams(input))
|
|
assert.equal(input.shopName, 'A店')
|
|
const first = emptyShopPageResult()
|
|
const second = emptyShopPageResult()
|
|
first.items.push({} as never)
|
|
assert.equal(second.items.length, 0, '每次空结果返回独立列表,避免污染')
|
|
})
|
|
|
|
test('test_task_081_shop_dto_boundary_empty_input', () => {
|
|
// 边界空值:空入参回默认分页(第 1 页/15 条),无可选筛选。
|
|
const params = normalizeShopListParams({})
|
|
assert.equal(params.page, SHOP_PAGE_MIN_PAGE)
|
|
assert.equal(params.pageSize, SHOP_PAGE_DEFAULT_SIZE)
|
|
assert.equal(params.groupId, null)
|
|
assert.equal(params.shopName, undefined)
|
|
const query = toShopManagePageQuery(params)
|
|
assert.deepEqual(Object.keys(query).sort(), ['page', 'pageSize'])
|
|
})
|
|
|
|
test('test_task_081_shop_dto_boundary_single_item', () => {
|
|
// 边界单元素:页大小 1 保留为最小单行页;单行空结果 DTO 结构完整。
|
|
assert.equal(normalizeShopListParams({ page: 1, pageSize: 1 }).pageSize, 1)
|
|
const key = emptyShopKeyPageResult()
|
|
assert.deepEqual(key.items, [])
|
|
assert.equal(key.total, 0)
|
|
assert.equal(key.page, SHOP_PAGE_MIN_PAGE)
|
|
assert.equal(key.pageSize, SHOP_PAGE_DEFAULT_SIZE)
|
|
const shop = emptyShopPageResult()
|
|
assert.deepEqual(shop.items, [])
|
|
assert.equal(shop.total, 0)
|
|
})
|
|
|
|
test('test_task_081_shop_dto_boundary_limit_or_missing_field', () => {
|
|
// 边界上限:页码 <1 归 1;页大小超上限/非正被夹取;负分组 id 视为无筛选。
|
|
assert.equal(normalizeShopListParams({ page: 0, pageSize: 999 }).page, 1)
|
|
assert.equal(normalizeShopListParams({ page: 0, pageSize: 999 }).pageSize, SHOP_PAGE_MAX_SIZE)
|
|
assert.equal(normalizeShopListParams({ page: 0, pageSize: 0 }).pageSize, SHOP_PAGE_DEFAULT_SIZE)
|
|
assert.equal(normalizeShopListParams({ page: 1, pageSize: 15, groupId: -5 }).groupId, null)
|
|
const params = normalizeShopListParams({ page: 1, pageSize: 15, groupId: undefined })
|
|
assert.equal(params.shopName, undefined)
|
|
})
|
|
|
|
test('test_task_081_shop_dto_invalid_input_rejected', () => {
|
|
// 异常输入:非数字/NaN/空白筛选取缺省;字符串分组 id 不接受。
|
|
const params = normalizeShopListParams({
|
|
page: 'x' as never,
|
|
pageSize: Number.NaN,
|
|
groupId: '3' as never,
|
|
shopName: ' ',
|
|
})
|
|
assert.equal(params.page, SHOP_PAGE_MIN_PAGE)
|
|
assert.equal(params.pageSize, SHOP_PAGE_DEFAULT_SIZE)
|
|
assert.equal(params.groupId, null)
|
|
assert.equal(params.shopName, undefined)
|
|
})
|
|
|
|
test('test_task_081_shop_dto_dependency_failure_returns_actionable_message', () => {
|
|
// 依赖失败/可操作:DTO 为纯逻辑、无框架/http;敏感字段与店铺/密钥边界有明确类型。
|
|
const mod = readSource('src/pages/shop/shop-dto.ts')
|
|
assert.equal(/axios|http\.|from '\.\/http'|vue/.test(mod), false, 'DTO 模块保持纯逻辑')
|
|
assert.match(mod, /export interface ShopKeyItem/)
|
|
assert.match(mod, /export interface ShopSummary/)
|
|
assert.match(mod, /export interface ShopCredential/)
|
|
assert.match(mod, /export type SensitiveString/)
|
|
assert.match(mod, /ziniaoToken\s*:/, '紫鸟令牌以敏感字段建模')
|
|
assert.match(mod, /password\s*:\s*SensitiveString/, '凭证明文必须落在 SensitiveString')
|
|
assert.match(mod, /passwordMasked\s*\?/, '列表行只有掩码密码')
|
|
assert.equal(/group_id|created_by_id/.test(mod), false, '店铺接口为 camelCase,不应出现 snake 字段名')
|
|
})
|