728 lines
25 KiB
JavaScript
728 lines
25 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import ts from 'typescript'
|
|
import * as vue from 'vue'
|
|
import { compileScript, parse } from '@vue/compiler-sfc'
|
|
|
|
function evaluate(source, imports = {}) {
|
|
const compiled = ts.transpileModule(source, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
|
}).outputText
|
|
const module = { exports: {} }
|
|
new Function('require', 'module', 'exports', compiled)(
|
|
(name) => {
|
|
assert.ok(imports[name], `Unexpected dependency: ${name}`)
|
|
return imports[name]
|
|
},
|
|
module,
|
|
module.exports
|
|
)
|
|
return module.exports
|
|
}
|
|
const source = (path) => readFileSync(path, 'utf8')
|
|
const format = evaluate(source('src/utils/business/format.ts'))
|
|
const utils = evaluate(source('src/utils/business/gateway.ts'), { './format': format })
|
|
const constants = evaluate(source('src/config/constants/gateway.ts'))
|
|
const permissions = constants.GATEWAY_PERMISSIONS
|
|
const rule = {
|
|
category: 'business',
|
|
keyword: 'failed',
|
|
match_type: 'contains',
|
|
priority: 10,
|
|
status: 1,
|
|
operations: ['stop']
|
|
}
|
|
const group = {
|
|
group_code: 'default',
|
|
group_name: '默认组',
|
|
max_qps: 20,
|
|
reserved_qps: 5,
|
|
operations: ['stop', 'start'],
|
|
high_priority_operations: ['stop'],
|
|
status: 1
|
|
}
|
|
const option = (value) => ({ value, label: `动态-${value}` })
|
|
const options = {
|
|
error_categories: ['business', 'terminal'].map(option),
|
|
exempt_sources: ['manual'].map(option),
|
|
gateway_operations: ['stop', 'start', 'query_flow', 'query_status', 'query_realname'].map(option),
|
|
match_types: ['contains', 'prefix', 'regex'].map(option),
|
|
rate_limit_modes: ['default'].map(option),
|
|
stop_resume_actions: ['stop', 'start'].map(option),
|
|
stop_resume_results: ['success', 'failed', 'unknown', 'already_target', 'skipped'].map(option)
|
|
}
|
|
const success = (data) => ({ code: 0, data, msg: 'ok' })
|
|
const page = (items) => success({ items, total: items.length, page: 1, size: 20 })
|
|
const deferred = () => {
|
|
let resolve
|
|
const promise = new Promise((done) => {
|
|
resolve = done
|
|
})
|
|
return { promise, resolve }
|
|
}
|
|
async function flush() {
|
|
await new Promise((done) => setImmediate(done))
|
|
await vue.nextTick()
|
|
}
|
|
|
|
test('all 17 gateway service contracts preserve methods, paths, params and response', async () => {
|
|
const calls = []
|
|
const response = success({ marker: true })
|
|
const http = Object.fromEntries(
|
|
['get', 'post', 'put', 'del'].map((method) => [
|
|
method,
|
|
(args) => {
|
|
calls.push({ method, ...args })
|
|
return Promise.resolve(response)
|
|
}
|
|
])
|
|
)
|
|
const base = evaluate(source('src/api/BaseService.ts'), { '@/utils/http': { default: http } })
|
|
const { GatewayService: service } = evaluate(source('src/api/modules/gateway.ts'), {
|
|
'../BaseService': base
|
|
})
|
|
const recordsQuery = {
|
|
page: 2,
|
|
page_size: 100,
|
|
carrier_id: 0,
|
|
start_time: '2026-10-01T00:00:00Z',
|
|
end_time: '2026-10-24T00:00:00Z'
|
|
}
|
|
const contracts = [
|
|
['getRecords', [recordsQuery], 'get', '/card-stop-resume-records', 'params', recordsQuery],
|
|
[
|
|
'getCardRecords',
|
|
[42, { page: 1 }],
|
|
'get',
|
|
'/iot-cards/42/stop-resume-records',
|
|
'params',
|
|
{ page: 1 }
|
|
],
|
|
['getRules', [{ status: 0 }], 'get', '/gateway-error-classifications', 'params', { status: 0 }],
|
|
['createRule', [rule], 'post', '/gateway-error-classifications', 'data', rule],
|
|
['updateRule', [7, rule], 'put', '/gateway-error-classifications/7', 'data', rule],
|
|
[
|
|
'setRuleStatus',
|
|
[7, 0],
|
|
'put',
|
|
'/gateway-error-classifications/7/status',
|
|
'data',
|
|
{ status: 0 }
|
|
],
|
|
['deleteRule', [7], 'del', '/gateway-error-classifications/7', 'params', undefined],
|
|
[
|
|
'testRule',
|
|
[{ operation: 'stop', message: 'failed', draft: rule }],
|
|
'post',
|
|
'/gateway-error-classifications/test',
|
|
'data',
|
|
{ operation: 'stop', message: 'failed', draft: rule }
|
|
],
|
|
['getOptions', [], 'get', '/polling-options', 'params', undefined],
|
|
[
|
|
'getGroups',
|
|
[{ only_global: false, carrier_id: 0, status: 0 }],
|
|
'get',
|
|
'/polling-rate-limit-groups',
|
|
'params',
|
|
{ only_global: false, carrier_id: 0, status: 0 }
|
|
],
|
|
['createGroup', [group], 'post', '/polling-rate-limit-groups', 'data', group],
|
|
['getGroup', [4], 'get', '/polling-rate-limit-groups/4', 'params', undefined],
|
|
['updateGroup', [4, group], 'put', '/polling-rate-limit-groups/4', 'data', group],
|
|
['setGroupStatus', [4, 1], 'put', '/polling-rate-limit-groups/4/status', 'data', { status: 1 }],
|
|
['deleteGroup', [4], 'del', '/polling-rate-limit-groups/4', 'params', undefined],
|
|
['getCarrierStats', [60], 'get', '/polling-stats/carriers', 'params', { minutes: 60 }],
|
|
[
|
|
'getCarrierTrend',
|
|
[0, { minutes: 10080, bucket_minutes: 60 }],
|
|
'get',
|
|
'/polling-stats/carriers/0/trend',
|
|
'params',
|
|
{ minutes: 10080, bucket_minutes: 60 }
|
|
]
|
|
]
|
|
for (const [method, args, verb, url, field, payload] of contracts) {
|
|
assert.equal(await service[method](...args), response)
|
|
assert.deepEqual(calls.at(-1), { method: verb, url: `/api/admin${url}`, [field]: payload })
|
|
}
|
|
assert.equal(calls.length, 17)
|
|
})
|
|
|
|
test('record dates are RFC3339, ordered and bounded to 31 days, optional only for cards', () => {
|
|
assert.deepEqual(utils.gatewayTimeRange(['2026-10-01T08:00:00+08:00', '2026-11-01T00:00:00Z']), {
|
|
start_time: '2026-10-01T00:00:00.000Z',
|
|
end_time: '2026-11-01T00:00:00.000Z'
|
|
})
|
|
assert.deepEqual(utils.gatewayTimeRange(null, false), {})
|
|
for (const range of [
|
|
null,
|
|
[],
|
|
[''],
|
|
['invalid', 'invalid'],
|
|
['2026-10-02', '2026-10-01'],
|
|
['2026-10-01', '2026-11-01T00:00:00.001Z']
|
|
])
|
|
assert.throws(() => utils.gatewayTimeRange(range))
|
|
const [start, end] = utils.recentGatewayRange()
|
|
assert.equal(end - start, 7 * 86400000)
|
|
})
|
|
|
|
test('limit groups validate QPS, operation subset, carrier zero and text limits', () => {
|
|
assert.doesNotThrow(() => utils.validateGatewayGroup({ ...group, carrier_id: 0 }))
|
|
assert.doesNotThrow(() =>
|
|
utils.validateGatewayGroup({ ...group, reserved_qps: 0, high_priority_operations: [] })
|
|
)
|
|
for (const patch of [
|
|
{ max_qps: 0 },
|
|
{ max_qps: 10001 },
|
|
{ max_qps: 1.5 },
|
|
{ reserved_qps: -1 },
|
|
{ reserved_qps: 20 },
|
|
{ reserved_qps: 0.5 },
|
|
{ operations: [] },
|
|
{ high_priority_operations: [] },
|
|
{ high_priority_operations: ['query_flow'] },
|
|
{ group_code: ' ' },
|
|
{ group_name: 'x'.repeat(101) },
|
|
{ carrier_id: -1 }
|
|
])
|
|
assert.throws(() => utils.validateGatewayGroup({ ...group, ...patch }))
|
|
})
|
|
|
|
test('rules validate required fields, priority and lengths, including draft rules', () => {
|
|
assert.doesNotThrow(() => utils.validateGatewayRule(rule))
|
|
for (const patch of [
|
|
{ keyword: ' ' },
|
|
{ keyword: 'x'.repeat(201) },
|
|
{ category: '' },
|
|
{ match_type: '' },
|
|
{ priority: 0 },
|
|
{ priority: 10001 },
|
|
{ priority: 1.2 },
|
|
{ remark: 'x'.repeat(501) }
|
|
])
|
|
assert.throws(() => utils.validateGatewayRule({ ...rule, ...patch }))
|
|
})
|
|
|
|
test('unknown enums and statistic fields remain visible, without fabricated zeroes', () => {
|
|
assert.equal(utils.gatewayLabel(options.gateway_operations, 'stop'), '动态-stop')
|
|
assert.equal(
|
|
utils.gatewayLabel(options.gateway_operations, 'future_operation'),
|
|
'future_operation'
|
|
)
|
|
assert.equal(utils.gatewayValue(0), '0')
|
|
assert.equal(utils.gatewayValue(false), 'false')
|
|
assert.equal(utils.gatewayValue(null), '-')
|
|
assert.deepEqual(utils.gatewayRows([{ future_metric: 12 }, 0, null]), [
|
|
{ future_metric: 12 },
|
|
{ value: 0 },
|
|
{ value: null }
|
|
])
|
|
assert.deepEqual(
|
|
utils.compactGatewayQuery({
|
|
carrier_id: 0,
|
|
status: 0,
|
|
only_global: false,
|
|
blank: '',
|
|
missing: undefined
|
|
}),
|
|
{ carrier_id: 0, status: 0, only_global: false }
|
|
)
|
|
assert.throws(() => utils.gatewayData({ code: 403, msg: '没有权限' }), /没有权限/)
|
|
})
|
|
|
|
function mountComponent(
|
|
t,
|
|
path,
|
|
{ superAdmin = false, perms = [], overrides = {}, props = {} } = {}
|
|
) {
|
|
const calls = []
|
|
const user = vue.reactive({ isSuperAdmin: superAdmin, perms })
|
|
const service = Object.fromEntries(
|
|
['getRecords', 'getCardRecords', 'getRules', 'getGroups'].map((name) => [
|
|
name,
|
|
async (...args) => {
|
|
calls.push({ name, args })
|
|
return page([])
|
|
}
|
|
])
|
|
)
|
|
Object.assign(service, { getOptions: async () => success(options) })
|
|
for (const name of [
|
|
'createRule',
|
|
'updateRule',
|
|
'setRuleStatus',
|
|
'deleteRule',
|
|
'testRule',
|
|
'createGroup',
|
|
'updateGroup',
|
|
'setGroupStatus',
|
|
'deleteGroup',
|
|
'getGroup',
|
|
'getCarrierStats',
|
|
'getCarrierTrend'
|
|
])
|
|
service[name] = async (...args) => {
|
|
calls.push({ name, args })
|
|
return success({
|
|
...group,
|
|
id: 4,
|
|
matched: true,
|
|
rule_id: 0,
|
|
category: 'business',
|
|
category_name: '业务错误',
|
|
keyword: 'failed',
|
|
maybe_accepted: false,
|
|
carriers: [],
|
|
points: [],
|
|
carrier_id: args[0]
|
|
})
|
|
}
|
|
for (const [name, handler] of Object.entries(overrides))
|
|
service[name] = async (...args) => {
|
|
calls.push({ name, args })
|
|
return handler(...args)
|
|
}
|
|
const imports = {
|
|
vue,
|
|
'vue-router': {
|
|
useRouter: () => ({
|
|
push: async (url) => {
|
|
calls.push({ name: 'navigate', args: [url] })
|
|
},
|
|
back() {}
|
|
}),
|
|
useRoute: () => ({ params: { id: '4' } })
|
|
},
|
|
'@vueuse/core': { useWindowSize: () => ({ width: vue.ref(1200) }) },
|
|
'@element-plus/icons-vue': { ArrowLeft: {} },
|
|
'@/router/routesAlias': {
|
|
RoutesAlias: { GatewayRateLimitGroupDetail: '/polling-management/rate-limit-groups/detail' }
|
|
},
|
|
'@/utils/business/format': format,
|
|
'@/api/modules/carrier': {
|
|
CarrierService: {
|
|
getCarriers: async (...args) => {
|
|
calls.push({ name: 'getCarriers', args })
|
|
if (overrides.getCarriers) return overrides.getCarriers(...args)
|
|
return page([{ id: 8, carrier_name: '模拟运营商' }])
|
|
}
|
|
}
|
|
},
|
|
'element-plus': {
|
|
ElMessage: { success() {}, error() {} },
|
|
ElMessageBox: { confirm: async () => {} }
|
|
},
|
|
'@/api/modules/gateway': { GatewayService: service },
|
|
'@/store/modules/user': { useUserStore: () => user },
|
|
'@/composables/useAuth': {
|
|
useAuth: () => ({ hasAuth: (perm) => user.isSuperAdmin || user.perms.includes(perm) })
|
|
},
|
|
'@/config/constants/gateway': constants,
|
|
'@/utils/business/gateway': utils,
|
|
'@/components/business/gateway/GatewayOptionsNotice.vue': { default: {} },
|
|
'@/components/business/gateway/GatewayRuleFields.vue': { default: {} },
|
|
'@/components/business/gateway/GatewayDynamicData.vue': { default: {} },
|
|
'@/components/business/gateway/GatewayOperationTable.vue': { default: {} },
|
|
'./GatewayOptionsNotice.vue': { default: {} }
|
|
}
|
|
imports['@/composables/useGateway'] = evaluate(source('src/composables/useGateway.ts'), imports)
|
|
const { descriptor } = parse(source(path))
|
|
const component = evaluate(
|
|
compileScript(descriptor, { id: 'gateway-test' }).content,
|
|
imports
|
|
).default
|
|
let state
|
|
const reactiveProps = vue.reactive(props)
|
|
const renderer = vue.createRenderer({
|
|
createComment: () => ({}),
|
|
insert() {},
|
|
remove() {},
|
|
parentNode: () => null,
|
|
nextSibling: () => null
|
|
})
|
|
const app = renderer.createApp({
|
|
setup() {
|
|
state = component.setup(reactiveProps, { expose() {}, emit() {} })
|
|
return () => null
|
|
}
|
|
})
|
|
app.mount({})
|
|
t.after(() => app.unmount())
|
|
return { state, calls, user, props: reactiveProps, service }
|
|
}
|
|
const rulePage = 'src/views/polling-management/error-classifications/index.vue'
|
|
const groupPage = 'src/views/polling-management/rate-limit-groups/index.vue'
|
|
const statsPage = 'src/views/polling-management/gateway-stats/index.vue'
|
|
const recordComponent = 'src/components/business/gateway/StopResumeRecords.vue'
|
|
|
|
test('ordinary users cannot maintain either rules or groups, even if handlers are called', async (t) => {
|
|
for (const [path, area, save, mutate, input] of [
|
|
[rulePage, 'rules', 'saveRule', 'changeRule', rule],
|
|
[groupPage, 'groups', 'saveGroup', 'changeGroup', group]
|
|
]) {
|
|
const { state, calls } = mountComponent(t, path, { perms: [permissions[area].page] })
|
|
await flush()
|
|
state.form.value = input
|
|
await state[save]()
|
|
await state[mutate]({ ...input, id: 1 }, true)
|
|
assert.equal(
|
|
calls.filter((call) => /create|update|delete|set.*Status/.test(call.name)).length,
|
|
0
|
|
)
|
|
assert.match(state.editorError.value, /无权/)
|
|
}
|
|
})
|
|
|
|
test('superadmin saves use write DTOs without record ids or timestamps', async (t) => {
|
|
for (const [path, save, input, method] of [
|
|
[rulePage, 'saveRule', rule, 'createRule'],
|
|
[groupPage, 'saveGroup', group, 'createGroup']
|
|
]) {
|
|
const { state, calls } = mountComponent(t, path, { superAdmin: true })
|
|
await flush()
|
|
state.form.value = { ...input }
|
|
await state[save]()
|
|
const call = calls.find((call) => call.name === method)
|
|
assert.ok(call)
|
|
assert.equal(call.args[0].id, undefined)
|
|
if (method === 'createGroup') assert.equal(call.args[0].carrier_id, null)
|
|
assert.equal(state.editorError.value, '')
|
|
}
|
|
})
|
|
|
|
test('ordinary rule tests require page + button, validate drafts, and never write rules', async (t) => {
|
|
const { state, calls, user } = mountComponent(t, rulePage, {
|
|
perms: [permissions.rules.page, permissions.rules.test]
|
|
})
|
|
await flush()
|
|
state.openTest()
|
|
state.testForm.operation = 'stop'
|
|
state.testForm.message = 'failed'
|
|
state.includeDraft.value = true
|
|
await state.runTest()
|
|
assert.equal(calls.filter((call) => call.name === 'testRule').length, 0)
|
|
state.draft.value = { ...rule }
|
|
await state.runTest()
|
|
assert.deepEqual(calls.find((call) => call.name === 'testRule').args[0], {
|
|
operation: 'stop',
|
|
message: 'failed',
|
|
draft: rule
|
|
})
|
|
assert.equal(state.testResult.value.matched, true)
|
|
user.perms = [permissions.rules.test]
|
|
await state.runTest()
|
|
assert.match(state.testError.value, /无权/)
|
|
assert.equal(calls.filter((call) => call.name === 'testRule').length, 1)
|
|
})
|
|
|
|
test('test close/reopen discards a pending result', async (t) => {
|
|
const pending = deferred()
|
|
const { state } = mountComponent(t, rulePage, {
|
|
superAdmin: true,
|
|
overrides: { testRule: () => pending.promise }
|
|
})
|
|
await flush()
|
|
state.openTest()
|
|
Object.assign(state.testForm, { operation: 'stop', message: 'failed' })
|
|
const running = state.runTest()
|
|
state.testVisible.value = false
|
|
await vue.nextTick()
|
|
state.openTest()
|
|
pending.resolve(success({ matched: true }))
|
|
await running
|
|
assert.equal(state.testResult.value, null)
|
|
})
|
|
|
|
test('enum failure is visible, blocks editing and can be retried with dynamic labels', async (t) => {
|
|
let failed = true
|
|
const { state } = mountComponent(t, rulePage, {
|
|
superAdmin: true,
|
|
overrides: {
|
|
getOptions: () => (failed ? Promise.reject(new Error('枚举不可用')) : success(options))
|
|
}
|
|
})
|
|
await flush()
|
|
assert.equal(state.optionsReady.value, false)
|
|
assert.match(state.optionsError.value, /枚举不可用/)
|
|
state.openEditor()
|
|
assert.equal(state.editorVisible.value, false)
|
|
failed = false
|
|
await state.loadOptions()
|
|
assert.equal(state.optionsReady.value, true)
|
|
assert.equal(state.label('gateway_operations', 'stop'), '动态-stop')
|
|
})
|
|
|
|
test('record queries validate time, preserve zero filters and isolate cards by real ID', async (t) => {
|
|
const { state, calls, props } = mountComponent(t, recordComponent, {
|
|
perms: [permissions.cardHistory],
|
|
props: { cardId: 42, permission: permissions.cardHistory }
|
|
})
|
|
await flush()
|
|
assert.equal(calls[0].name, 'getCardRecords')
|
|
assert.equal(calls[0].args[0], 42)
|
|
state.range.value = null
|
|
await state.search()
|
|
assert.deepEqual(calls.at(-1).args, [42, { page: 1, page_size: 20 }])
|
|
props.cardId = 43
|
|
await flush()
|
|
assert.equal(calls.at(-1).args[0], 43)
|
|
props.cardId = undefined
|
|
props.permission = permissions.records.page
|
|
await flush()
|
|
assert.match(state.error.value, /无权/)
|
|
})
|
|
|
|
test('invalid global date ranges never request records; zero carrier survives', async (t) => {
|
|
const { state, calls } = mountComponent(t, recordComponent, {
|
|
perms: [permissions.records.page],
|
|
props: { permission: permissions.records.page }
|
|
})
|
|
await flush()
|
|
calls.length = 0
|
|
state.range.value = null
|
|
await state.search()
|
|
assert.equal(calls.length, 0)
|
|
state.range.value = [new Date('2026-10-01'), new Date('2026-10-24')]
|
|
state.filters.carrier_id = 0
|
|
await state.search()
|
|
assert.equal(calls[0].args[0].carrier_id, 0)
|
|
})
|
|
|
|
test('list latest request wins and errors clear previous records', async (t) => {
|
|
const first = deferred()
|
|
const second = deferred()
|
|
let count = 0
|
|
const { state } = mountComponent(t, groupPage, {
|
|
superAdmin: true,
|
|
overrides: { getGroups: () => (++count === 1 ? first.promise : second.promise) }
|
|
})
|
|
const latest = state.load()
|
|
second.resolve(page([{ ...group, id: 2 }]))
|
|
await latest
|
|
first.resolve(page([{ ...group, id: 1 }]))
|
|
await flush()
|
|
assert.equal(state.rows.value[0].id, 2)
|
|
})
|
|
|
|
test('group details require permissions before navigation to shop-style detail page', async (t) => {
|
|
const { state, calls, user } = mountComponent(t, groupPage, { perms: [permissions.groups.page] })
|
|
await flush()
|
|
await state.openDetail(4)
|
|
assert.equal(
|
|
calls.some((call) => call.name === 'navigate'),
|
|
false
|
|
)
|
|
user.perms.push(permissions.groups.detail)
|
|
await state.openDetail(4)
|
|
assert.equal(
|
|
calls.find((call) => call.name === 'navigate').args[0],
|
|
'/polling-management/rate-limit-groups/detail/4'
|
|
)
|
|
})
|
|
|
|
test('statistics support account 0, reject bad bounds and retain raw unknown metrics', async (t) => {
|
|
const { state, calls } = mountComponent(t, statsPage, {
|
|
perms: [permissions.stats.page, permissions.stats.trend],
|
|
overrides: {
|
|
getCarrierTrend: (id) =>
|
|
success({ carrier_id: id, points: [{ new_metric: 9, future_time: '2026-10-24' }] })
|
|
}
|
|
})
|
|
await flush()
|
|
state.carrierId.value = 0
|
|
await state.loadTrend()
|
|
assert.equal(state.trend.value.carrier_id, 0)
|
|
assert.equal(state.trend.value.points[0].new_metric, 9)
|
|
state.trendMinutes.value = 10081
|
|
await state.loadTrend()
|
|
assert.equal(calls.filter((call) => call.name === 'getCarrierTrend').length, 1)
|
|
state.summaryMinutes.value = 61
|
|
await state.loadSummary()
|
|
assert.equal(calls.filter((call) => call.name === 'getCarrierStats').length, 1)
|
|
assert.equal(state.trend.value, null)
|
|
})
|
|
|
|
test('route aliases match all four pages and ordinary permissions use colon codes', () => {
|
|
const routes = source('src/router/routes/asyncRoutes.ts')
|
|
const aliases = source('src/router/routesAlias.ts')
|
|
for (const [name, segment] of [
|
|
['GatewayStopResumeRecords', 'stop-resume-records'],
|
|
['GatewayErrorClassifications', 'error-classifications'],
|
|
['GatewayRateLimitGroups', 'rate-limit-groups'],
|
|
['GatewayCallStats', 'gateway-stats']
|
|
]) {
|
|
assert.ok(routes.includes(`name: '${name}'`))
|
|
assert.ok(aliases.includes(`'/polling-management/${segment}'`))
|
|
assert.ok(source(`src/views/polling-management/${segment}/index.vue`))
|
|
}
|
|
const flattened = Object.values(permissions).flatMap((value) =>
|
|
typeof value === 'string' ? [value] : Object.values(value)
|
|
)
|
|
assert.equal(flattened.length, 10)
|
|
for (const code of flattened) assert.match(code, /^[a-z_]+:[a-z_]+$/)
|
|
})
|
|
|
|
test('detail page checks permission and formats dates using the shop formatter', async (t) => {
|
|
const path = 'src/views/polling-management/rate-limit-groups/detail.vue'
|
|
const denied = mountComponent(t, path)
|
|
await flush()
|
|
assert.equal(
|
|
denied.calls.some((call) => call.name === 'getGroup'),
|
|
false
|
|
)
|
|
const allowed = mountComponent(t, path, { perms: [permissions.groups.detailPage] })
|
|
await flush()
|
|
assert.equal(allowed.state.detail.value.id, 4)
|
|
assert.equal(allowed.state.formatDateTime('2026-10-24T10:00:00+08:00'), '2026-10-24 10:00:00')
|
|
})
|
|
|
|
test('gateway search expand toggles follow the >=4 rule', () => {
|
|
for (const path of [recordComponent, rulePage, groupPage, statsPage]) {
|
|
const matches = [...source(path).matchAll(/:show-expand="([^"]+)"/g)]
|
|
assert.ok(matches.length)
|
|
for (const match of matches) assert.match(match[1], /\.length >= 4$/)
|
|
}
|
|
})
|
|
|
|
test('record time picker matches expiring assets estimated-expiry range configuration', async (t) => {
|
|
const { descriptor } = parse(source('src/views/asset-management/expiring-assets/index.vue'))
|
|
const script = ts.createSourceFile(
|
|
'expiring-assets.ts',
|
|
descriptor.scriptSetup.content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TS
|
|
)
|
|
let reference
|
|
function visit(node) {
|
|
if (
|
|
ts.isObjectLiteralExpression(node) &&
|
|
node.properties.some(
|
|
(property) =>
|
|
ts.isPropertyAssignment(property) &&
|
|
property.name.getText(script) === 'label' &&
|
|
ts.isStringLiteral(property.initializer) &&
|
|
property.initializer.text === '预计到期起止'
|
|
)
|
|
) {
|
|
reference = evaluate(`export const item = ${node.getText(script)}`).item
|
|
}
|
|
ts.forEachChild(node, visit)
|
|
}
|
|
visit(script)
|
|
assert.ok(reference)
|
|
const { state } = mountComponent(t, recordComponent, {
|
|
perms: [permissions.records.page],
|
|
props: { permission: permissions.records.page }
|
|
})
|
|
await flush()
|
|
const item = state.searchItems.value.find((item) => item.prop === 'range')
|
|
assert.equal(item.type, reference.type)
|
|
assert.equal(item.elColSpan, reference.elColSpan)
|
|
assert.deepEqual(item.config, reference.config)
|
|
assert.equal(item.label, '记录时间')
|
|
})
|
|
|
|
test('business timestamps match shop list formatting and IDs retain zero', () => {
|
|
const timestamp = '2026-10-24T10:00:00+08:00'
|
|
assert.equal(utils.gatewayDisplayValue(timestamp, 'created_at'), format.formatDateTime(timestamp))
|
|
assert.equal(
|
|
utils.gatewayDisplayValue({ created_at: timestamp }),
|
|
'{"created_at":"2026-10-24 10:00:00"}'
|
|
)
|
|
assert.equal(
|
|
utils.gatewayDisplayValue(1792807200, 'timestamp'),
|
|
format.formatDateTime(1792807200)
|
|
)
|
|
assert.equal(utils.gatewayDisplayValue(0, 'duration_ms'), '0')
|
|
assert.equal(utils.gatewayOptionalId('0'), 0)
|
|
assert.equal(utils.gatewayOptionalId(''), undefined)
|
|
assert.throws(() => utils.gatewayOptionalId('-1'))
|
|
})
|
|
|
|
test('carrier name search uses exact backend parameters and remembers edit labels', async (t) => {
|
|
const { state, calls } = mountComponent(t, groupPage, { superAdmin: true })
|
|
await flush()
|
|
await state.searchCarriers('运营商名称')
|
|
assert.deepEqual(calls.at(-1), {
|
|
name: 'getCarriers',
|
|
args: [{ page: 1, page_size: 20, carrier_name: '运营商名称' }]
|
|
})
|
|
state.openEditor({ ...group, id: 7, carrier_id: 99, carrier_name: '原账户' })
|
|
assert.equal(state.carrierOptions.value.find((option) => option.value === 99).label, '原账户')
|
|
assert.equal(state.form.value.carrier_id, 99)
|
|
assert.equal(state.searchItems.value[0].label, '运营商账户')
|
|
assert.equal(state.searchItems.value[0].type, 'select')
|
|
})
|
|
|
|
test('carrier search ignores a late old response and retains selected account labels', async (t) => {
|
|
const older = deferred()
|
|
const { state } = mountComponent(t, groupPage, {
|
|
superAdmin: true,
|
|
overrides: {
|
|
getCarriers: ({ carrier_name }) =>
|
|
carrier_name === 'old' ? older.promise : page([{ id: 8, carrier_name: '新账户' }])
|
|
}
|
|
})
|
|
await flush()
|
|
const first = state.searchCarriers('old')
|
|
await state.searchCarriers('new')
|
|
state.selectCarrier(8)
|
|
older.resolve(page([{ id: 9, carrier_name: '旧账户' }]))
|
|
await first
|
|
assert.equal(
|
|
state.carrierOptions.value.some((option) => option.value === 9),
|
|
false
|
|
)
|
|
assert.equal(state.carrierOptions.value.find((option) => option.value === 8).label, '新账户')
|
|
})
|
|
|
|
test('duplicate maintenance submits cannot send a second write while saving', async (t) => {
|
|
const pending = deferred()
|
|
const { state, calls } = mountComponent(t, rulePage, {
|
|
superAdmin: true,
|
|
overrides: { createRule: () => pending.promise }
|
|
})
|
|
await flush()
|
|
state.form.value = { ...rule }
|
|
const first = state.saveRule()
|
|
await state.saveRule()
|
|
assert.equal(calls.filter((call) => call.name === 'createRule').length, 1)
|
|
pending.resolve(success(rule))
|
|
await first
|
|
})
|
|
|
|
test('single-card history entry rejects missing IDs, other assets and denied permissions', () => {
|
|
const text = source('src/views/asset-management/asset-information/index.vue')
|
|
const functionText = text.match(/ {2}function showStopResumeRecords[\s\S]*?\n {2}}/)[0]
|
|
const { create } = evaluate(
|
|
`export const create = (cardInfo, hasAuth, stopResumePermission, stopResumeTarget, stopResumeVisible) => { ${functionText}; return showStopResumeRecords }`
|
|
)
|
|
const asset = vue.ref({
|
|
asset_type: 'device',
|
|
asset_id: 100,
|
|
cards: [{ card_id: 42, iccid: 'CARD' }]
|
|
})
|
|
const target = vue.ref(null)
|
|
const visible = vue.ref(false)
|
|
let allowed = false
|
|
const open = create(asset, () => allowed, vue.ref(permissions.deviceCardHistory), target, visible)
|
|
open({ cardId: 42, iccid: 'CARD' })
|
|
assert.equal(visible.value, false)
|
|
allowed = true
|
|
open({ cardId: 100, iccid: 'CARD' })
|
|
assert.equal(visible.value, false)
|
|
open({ cardId: 42, iccid: 'CARD' })
|
|
assert.equal(target.value.cardId, 42)
|
|
assert.equal(visible.value, true)
|
|
visible.value = false
|
|
asset.value = { asset_type: 'card', asset_id: 7, iccid: 'CARD-7' }
|
|
open({ cardId: 42, iccid: 'CARD' })
|
|
assert.equal(visible.value, false)
|
|
open({ cardId: 7, iccid: 'CARD-7' })
|
|
assert.equal(target.value.cardId, 7)
|
|
})
|