Files
Easytier/easytier-web/frontend-lib/tests/remote-management-config.spec.ts
T
KKRainbow f0d00d6161 refactor(web): use generated proto network types (#2373)
* refactor(web): use generated proto network types
* fix(core): preserve dumped config flags
* test(web): cover config flag save paths
* fix(ci): use system protoc before frontend codegen
* fix(ci): serialize frontend-lib builds
2026-06-27 13:09:28 +08:00

229 lines
5.7 KiB
TypeScript

import { flushPromises, mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import RemoteManagement from '../src/components/RemoteManagement.vue'
import {
DEFAULT_NETWORK_CONFIG,
type NetworkConfig,
} from '../src/types/network'
const BOOLEAN_CONFIG_FIELDS = [
'dhcp',
'enable_vpn_portal',
'advanced_settings',
'latency_first',
'use_smoltcp',
'disable_ipv6',
'enable_kcp_proxy',
'disable_kcp_input',
'disable_p2p',
'bind_device',
'no_tun',
'enable_exit_node',
'relay_all_peer_rpc',
'multi_thread',
'enable_relay_network_whitelist',
'enable_manual_routes',
'proxy_forward_by_system',
'disable_encryption',
'enable_socks5',
'disable_udp_hole_punching',
'enable_magic_dns',
'enable_private_mode',
'enable_quic_proxy',
'disable_quic_input',
'disable_sym_hole_punching',
'p2p_only',
'lazy_p2p',
'need_p2p',
'disable_upnp',
'ipv6_public_addr_provider',
'ipv6_public_addr_auto',
'disable_relay_data',
'enable_udp_broadcast_relay',
'disable_tcp_hole_punching',
] as const satisfies readonly (keyof NetworkConfig)[]
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string) => key,
}),
}))
vi.mock('primevue', async () => {
const { defineComponent, h } = await import('vue')
const PassThrough = defineComponent({
name: 'PassThrough',
props: {
label: String,
value: String,
},
setup(props, { slots }) {
return () => h('div', {
'data-label': props.label,
'data-value': props.value,
'data-stub': 'pass-through',
}, slots.default?.())
},
})
const ButtonStub = defineComponent({
name: 'Button',
props: {
label: String,
icon: String,
disabled: Boolean,
},
emits: ['click'],
setup(props, { slots, emit }) {
return () => h('button', {
type: 'button',
disabled: props.disabled,
'data-label': props.label ?? props.icon,
onClick: (event: MouseEvent) => emit('click', event),
}, slots.default?.() ?? props.label ?? props.icon)
},
})
const SelectStub = defineComponent({
name: 'Select',
props: {
modelValue: Object,
options: Array,
},
emits: ['update:modelValue'],
setup(props, { slots }) {
return () => h('div', { 'data-stub': 'select' }, [
slots.value?.({ value: props.modelValue, placeholder: '' }),
])
},
})
const MenuStub = defineComponent({
name: 'Menu',
setup(_, { expose }) {
expose({ toggle: vi.fn() })
return () => h('div', { 'data-stub': 'menu' })
},
})
return {
Button: ButtonStub,
ConfirmPopup: PassThrough,
Divider: PassThrough,
IftaLabel: PassThrough,
Menu: MenuStub,
Message: PassThrough,
Select: SelectStub,
Tag: PassThrough,
useConfirm: () => ({ require: vi.fn() }),
useToast: () => ({ add: vi.fn() }),
}
})
const INSTANCE_ID = '00000000-0000-0000-0000-000000000001'
const INSTANCE_UUID = {
part1: 0,
part2: 0,
part3: 0,
part4: 1,
}
function makeFlagConfig(): NetworkConfig {
const config = {
...DEFAULT_NETWORK_CONFIG(),
instance_id: INSTANCE_ID,
network_name: 'mesh-save',
}
BOOLEAN_CONFIG_FIELDS.forEach((field, index) => {
config[field] = index % 2 === 0
})
return config
}
function cloneConfig(config: NetworkConfig): NetworkConfig {
return JSON.parse(JSON.stringify(config)) as NetworkConfig
}
function snapshotBooleanConfigFields(config: NetworkConfig): Record<string, unknown> {
return Object.fromEntries(
BOOLEAN_CONFIG_FIELDS.map((field) => [field, config[field]]),
)
}
async function settleRemoteManagement() {
for (let i = 0; i < 3; i++) {
await new Promise((resolve) => setTimeout(resolve, 0))
await flushPromises()
await nextTick()
}
}
describe('RemoteManagement config save', () => {
it('saves the current network config without dropping boolean fields', async () => {
const config = makeFlagConfig()
const expectedFlags = snapshotBooleanConfigFields(config)
const api = {
delete_network: vi.fn(),
generate_config: vi.fn(),
get_network_config: vi.fn(async () => cloneConfig(config)),
get_network_info: vi.fn(),
get_network_metas: vi.fn(async (instanceIds: string[]) => ({
metas: Object.fromEntries(instanceIds.map((id) => [id, {
config_permission: 0xffffffff,
inst_id: INSTANCE_UUID,
instance_name: 'mesh-save',
network_name: 'mesh-save',
source: 2,
}])),
})),
list_network_instance_ids: vi.fn(async () => ({
disabled_inst_ids: [INSTANCE_UUID],
running_inst_ids: [],
})),
parse_config: vi.fn(),
run_network: vi.fn(),
save_config: vi.fn(async () => undefined),
update_network_instance_state: vi.fn(),
validate_config: vi.fn(),
}
const wrapper = mount(RemoteManagement, {
props: {
api,
instanceId: INSTANCE_ID,
},
global: {
stubs: {
Config: true,
ConfigEditDialog: true,
Status: true,
},
},
})
try {
await settleRemoteManagement()
const saveButton = wrapper.find('button[data-label="web.device_management.save_config"]')
expect(saveButton.exists()).toBe(true)
expect(saveButton.attributes('disabled')).toBeUndefined()
await saveButton.trigger('click')
await flushPromises()
expect(api.save_config).toHaveBeenCalledOnce()
const savedConfig = api.save_config.mock.calls[0][0] as NetworkConfig
for (const field of BOOLEAN_CONFIG_FIELDS) {
expect(savedConfig[field], `${field} should be saved`).toBe(expectedFlags[field])
}
} finally {
wrapper.unmount()
}
})
})