mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-08 05:29:47 +00:00
f0d00d6161
* 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
229 lines
5.7 KiB
TypeScript
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()
|
|
}
|
|
})
|
|
})
|