Files
one-pipe-system/scripts/tests/gateway.test.mjs
luo 049f8257fb
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 56s
feat: 新增网关限流
2026-10-24 16:22:28 +08:00

727 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: {} },
'./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)
})