Files
crawler-plugin/admin-frontend-vue/tests/task-81.test.ts
T
huangzd1997 93033e94db task-81(店铺中心): 定义店铺和店铺密钥 DTO
新增 src/pages/shop/shop-dto.ts 纯逻辑 DTO:ShopKeyItem/ShopSummary/
ShopCredential/SensitiveString 等类型边界与分页归一/查询序列化(店铺接口
camelCase);凭证明文与紫鸟令牌按敏感字段建模,禁止进入通用日志。

TDD: task-81.test.ts 8 用例先 RED 后 GREEN。
2026-09-05 16:32:08 +08:00

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 字段名')
})