mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
510 lines
19 KiB
JavaScript
510 lines
19 KiB
JavaScript
import { mount } from "@vue/test-utils";
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import AddInterfacePage from "../../meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue";
|
|
|
|
const mockAxios = {
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
};
|
|
window.api = mockAxios;
|
|
|
|
vi.mock("../../meshchatx/src/frontend/js/DialogUtils", () => ({
|
|
default: {
|
|
alert: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
|
|
default: {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const mountPage = () =>
|
|
mount(AddInterfacePage, {
|
|
global: {
|
|
mocks: {
|
|
$route: { query: {} },
|
|
$router: { push: vi.fn() },
|
|
$t: (msg) => msg,
|
|
},
|
|
stubs: ["RouterLink", "MaterialDesignIcon", "Toggle", "ExpandingSection", "FormLabel", "FormSubLabel"],
|
|
},
|
|
});
|
|
|
|
describe("AddInterfacePage.vue interface options", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockAxios.get.mockImplementation(async (url) => {
|
|
if (String(url).includes("/api/v1/config")) {
|
|
return { data: { config: { is_transport_enabled: true } } };
|
|
}
|
|
if (String(url).includes("/api/v1/reticulum/instance")) {
|
|
return { data: { instance: { enable_transport: true } } };
|
|
}
|
|
if (String(url).includes("/api/v1/reticulum/interface-modules")) {
|
|
return {
|
|
data: {
|
|
interfacepath: "/tmp/meshchatx/reticulum/interfaces",
|
|
modules: [{ type: "ExampleInterface", filename: "ExampleInterface.py", size: 12 }],
|
|
},
|
|
};
|
|
}
|
|
if (String(url).includes("/api/v1/reticulum/interfaces")) {
|
|
return { data: { interfaces: {} } };
|
|
}
|
|
return { data: {} };
|
|
});
|
|
mockAxios.post.mockResolvedValue({ data: { message: "ok" } });
|
|
});
|
|
|
|
it("loads installed interface modules for custom external type", async () => {
|
|
const wrapper = mountPage();
|
|
wrapper.vm.newInterfaceType = "__external__";
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.loadInstalledInterfaceModules();
|
|
expect(mockAxios.get).toHaveBeenCalledWith("/api/v1/reticulum/interface-modules");
|
|
expect(wrapper.vm.interfaceModulesPath).toContain("interfaces");
|
|
expect(wrapper.vm.installedInterfaceModules).toHaveLength(1);
|
|
expect(wrapper.text()).toContain("interfaces.custom_external_install_button");
|
|
});
|
|
|
|
it("sends AutoInterface group/discovery/data port settings", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "AutoLAN";
|
|
wrapper.vm.newInterfaceType = "AutoInterface";
|
|
wrapper.vm.newInterfaceGroupID = "homelab";
|
|
wrapper.vm.newInterfaceDiscoveryScope = "site";
|
|
wrapper.vm.newInterfaceDiscoveryPort = 35000;
|
|
wrapper.vm.newInterfaceDataPort = 35001;
|
|
wrapper.vm.newInterfaceMulticastAddressType = "permanent";
|
|
wrapper.vm.newInterfaceDevices = "eth0,wlan0";
|
|
wrapper.vm.newInterfaceIgnoredDevices = "tun0";
|
|
wrapper.vm.newInterfaceConfiguredBitrate = 5000000;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
type: "AutoInterface",
|
|
group_id: "homelab",
|
|
discovery_scope: "site",
|
|
discovery_port: 35000,
|
|
data_port: 35001,
|
|
multicast_address_type: "permanent",
|
|
devices: "eth0,wlan0",
|
|
ignored_devices: "tun0",
|
|
configured_bitrate: 5000000,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends TCP client advanced options (kiss/i2p/timeout/mtu)", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "TCPC";
|
|
wrapper.vm.newInterfaceType = "TCPClientInterface";
|
|
wrapper.vm.newInterfaceTargetHost = "example.com";
|
|
wrapper.vm.newInterfaceTargetPort = 4242;
|
|
wrapper.vm.newInterfaceKISSFramingEnabled = true;
|
|
wrapper.vm.newInterfaceI2PTunnelingEnabled = true;
|
|
wrapper.vm.newInterfaceConnectTimeout = 12;
|
|
wrapper.vm.newInterfaceMaxReconnectTries = 7;
|
|
wrapper.vm.newInterfaceFixedMTU = 512;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
kiss_framing: true,
|
|
i2p_tunneled: true,
|
|
connect_timeout: 12,
|
|
max_reconnect_tries: 7,
|
|
fixed_mtu: 512,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("blocks TCP client save when fixed_mtu is below Reticulum minimum", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "TCPC";
|
|
wrapper.vm.newInterfaceType = "TCPClientInterface";
|
|
wrapper.vm.newInterfaceTargetHost = "example.com";
|
|
wrapper.vm.newInterfaceTargetPort = 4242;
|
|
wrapper.vm.newInterfaceFixedMTU = 485;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("sends TCP server device/prefer_ipv6/i2p_tunneled options", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "TCPS";
|
|
wrapper.vm.newInterfaceType = "TCPServerInterface";
|
|
wrapper.vm.newInterfaceListenIp = "0.0.0.0";
|
|
wrapper.vm.newInterfaceListenPort = 4242;
|
|
wrapper.vm.newInterfaceNetworkDevice = "eth0";
|
|
wrapper.vm.newInterfacePreferIPV6 = true;
|
|
wrapper.vm.newInterfaceI2PTunnelingEnabled = true;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
device: "eth0",
|
|
prefer_ipv6: true,
|
|
i2p_tunneled: true,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends BackboneInterface listener payload when listener mode is on", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "Backbone";
|
|
wrapper.vm.newInterfaceType = "BackboneInterface";
|
|
wrapper.vm.newInterfaceBackboneListenMode = true;
|
|
wrapper.vm.newInterfaceBackboneListenIp = "0.0.0.0";
|
|
wrapper.vm.newInterfaceBackboneListenPort = 5151;
|
|
wrapper.vm.newInterfaceBackboneListenDevice = "eth1";
|
|
wrapper.vm.newInterfacePreferIPV6 = true;
|
|
wrapper.vm.newInterfaceTargetHost = "leaked.example";
|
|
wrapper.vm.newInterfaceTargetPort = 9999;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
const payload = mockAxios.post.mock.calls[0][1];
|
|
expect(payload.listen_ip).toBe("0.0.0.0");
|
|
expect(payload.listen_port).toBe(5151);
|
|
expect(payload.device).toBe("eth1");
|
|
expect(payload.prefer_ipv6).toBe(true);
|
|
expect(payload.target_host).toBe(null);
|
|
expect(payload.target_port).toBe(null);
|
|
});
|
|
|
|
it("sends RNode flow_control + id_callsign + id_interval", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "Radio";
|
|
wrapper.vm.newInterfaceType = "RNodeInterface";
|
|
wrapper.vm.newInterfacePort = "/dev/ttyUSB0";
|
|
wrapper.vm.RNodeMHzValue = 868;
|
|
wrapper.vm.newInterfaceBandwidth = 125000;
|
|
wrapper.vm.newInterfaceTxpower = 7;
|
|
wrapper.vm.newInterfaceSpreadingFactor = 8;
|
|
wrapper.vm.newInterfaceCodingRate = 5;
|
|
wrapper.vm.newInterfaceFlowControl = true;
|
|
wrapper.vm.newInterfaceIDCallsign = "NOCALL";
|
|
wrapper.vm.newInterfaceIDInterval = 600;
|
|
wrapper.vm.newInterfaceAirtimeLimitLong = 1.5;
|
|
wrapper.vm.newInterfaceAirtimeLimitShort = 33.0;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
flow_control: true,
|
|
id_callsign: "NOCALL",
|
|
id_interval: 600,
|
|
airtime_limit_long: 1.5,
|
|
airtime_limit_short: 33.0,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends KISS serial + framing options", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "KISS";
|
|
wrapper.vm.newInterfaceType = "KISSInterface";
|
|
wrapper.vm.newInterfacePort = "/dev/ttyACM0";
|
|
wrapper.vm.newInterfaceSpeed = 19200;
|
|
wrapper.vm.newInterfaceDatabits = 8;
|
|
wrapper.vm.newInterfaceParity = "N";
|
|
wrapper.vm.newInterfaceStopbits = 1;
|
|
wrapper.vm.newInterfacePreamble = 200;
|
|
wrapper.vm.newInterfaceTXTail = 30;
|
|
wrapper.vm.newInterfacePersistence = 128;
|
|
wrapper.vm.newInterfaceSlotTime = 25;
|
|
wrapper.vm.newInterfaceFlowControl = true;
|
|
wrapper.vm.newInterfaceIDCallsign = "BEACON";
|
|
wrapper.vm.newInterfaceIDInterval = 1200;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
speed: 19200,
|
|
databits: 8,
|
|
parity: "N",
|
|
stopbits: 1,
|
|
preamble: 200,
|
|
txtail: 30,
|
|
persistence: 128,
|
|
slottime: 25,
|
|
flow_control: true,
|
|
id_callsign: "BEACON",
|
|
id_interval: 1200,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends AX25KISS callsign + ssid", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "AX25";
|
|
wrapper.vm.newInterfaceType = "AX25KISSInterface";
|
|
wrapper.vm.newInterfacePort = "/dev/ttyACM1";
|
|
wrapper.vm.newInterfaceCallsign = "N0CALL";
|
|
wrapper.vm.newInterfaceSSID = 7;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
callsign: "N0CALL",
|
|
ssid: 7,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("defaults I2P connectable off and requires a peer", async () => {
|
|
const wrapper = mountPage();
|
|
const ToastUtils = (await import("../../meshchatx/src/frontend/js/ToastUtils")).default;
|
|
|
|
expect(wrapper.vm.newInterfaceConnectable).toBe(false);
|
|
|
|
wrapper.vm.newInterfaceName = "I2P";
|
|
wrapper.vm.newInterfaceType = "I2PInterface";
|
|
wrapper.vm.I2PSettings.newInterfacePeers = [" "];
|
|
wrapper.vm.config = { ...(wrapper.vm.config || {}), is_transport_enabled: true };
|
|
wrapper.vm.reticulumInstance = {
|
|
...(wrapper.vm.reticulumInstance || {}),
|
|
enable_transport: true,
|
|
};
|
|
wrapper.vm.existingInterfaces = {};
|
|
|
|
await wrapper.vm.saveInterface();
|
|
expect(ToastUtils.error).toHaveBeenCalledWith("interfaces.i2p_peers_required");
|
|
expect(mockAxios.post).not.toHaveBeenCalled();
|
|
|
|
wrapper.vm.I2PSettings.newInterfacePeers = ["abcdef.b32.i2p"];
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
type: "I2PInterface",
|
|
peers: ["abcdef.b32.i2p"],
|
|
connectable: false,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("shows I2P SAM copy and a connectable warning when inbound is on", async () => {
|
|
const wrapper = mountPage();
|
|
wrapper.vm.newInterfaceType = "I2PInterface";
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.text()).toContain("interfaces.i2p_sam_required");
|
|
expect(wrapper.text()).toContain("interfaces.i2p_connectable_hint");
|
|
expect(wrapper.text()).not.toContain("interfaces.i2p_connectable_warning");
|
|
|
|
wrapper.vm.newInterfaceConnectable = true;
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.text()).toContain("interfaces.i2p_connectable_warning");
|
|
expect(wrapper.text()).toContain("interfaces.i2p_peers_add");
|
|
});
|
|
|
|
it("sends I2P connectable=false when peer disables it", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "I2P";
|
|
wrapper.vm.newInterfaceType = "I2PInterface";
|
|
wrapper.vm.I2PSettings.newInterfacePeers = ["abcdef.b32.i2p"];
|
|
wrapper.vm.newInterfaceConnectable = false;
|
|
wrapper.vm.config = { ...(wrapper.vm.config || {}), is_transport_enabled: true };
|
|
wrapper.vm.reticulumInstance = {
|
|
...(wrapper.vm.reticulumInstance || {}),
|
|
enable_transport: true,
|
|
};
|
|
wrapper.vm.existingInterfaces = {};
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
type: "I2PInterface",
|
|
peers: ["abcdef.b32.i2p"],
|
|
connectable: false,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("surfaces backend port-in-use errors to the user", async () => {
|
|
const wrapper = mountPage();
|
|
const ToastUtils = (await import("../../meshchatx/src/frontend/js/ToastUtils")).default;
|
|
|
|
mockAxios.post.mockRejectedValueOnce({
|
|
response: {
|
|
status: 409,
|
|
data: { message: "The TCP port 4242 on 0.0.0.0 is already in use" },
|
|
},
|
|
});
|
|
|
|
wrapper.vm.newInterfaceName = "TCPS";
|
|
wrapper.vm.newInterfaceType = "TCPServerInterface";
|
|
wrapper.vm.newInterfaceListenIp = "0.0.0.0";
|
|
wrapper.vm.newInterfaceListenPort = 4242;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(ToastUtils.error).toHaveBeenCalledWith(expect.stringContaining("already in use"));
|
|
});
|
|
|
|
it("sends internal mode and recursive_prs / announces_from_internal", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "InternalLAN";
|
|
wrapper.vm.newInterfaceType = "TCPServerInterface";
|
|
wrapper.vm.newInterfaceListenIp = "127.0.0.1";
|
|
wrapper.vm.newInterfaceListenPort = 4242;
|
|
wrapper.vm.sharedInterfaceSettings.mode = "internal";
|
|
wrapper.vm.sharedInterfaceSettings.recursive_prs = true;
|
|
wrapper.vm.sharedInterfaceSettings.announces_from_internal = false;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
mode: "internal",
|
|
recursive_prs: true,
|
|
announces_from_internal: false,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends gravity and announces_to_internal", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "GravityLAN";
|
|
wrapper.vm.newInterfaceType = "TCPServerInterface";
|
|
wrapper.vm.newInterfaceListenIp = "127.0.0.1";
|
|
wrapper.vm.newInterfaceListenPort = 4242;
|
|
wrapper.vm.sharedInterfaceSettings.gravity = 2;
|
|
wrapper.vm.sharedInterfaceSettings.announces_to_internal = true;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
gravity: 2,
|
|
announces_to_internal: true,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends BackboneInterface fast-flapping options in listener mode", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "Backbone";
|
|
wrapper.vm.newInterfaceType = "BackboneInterface";
|
|
wrapper.vm.newInterfaceBackboneListenMode = true;
|
|
wrapper.vm.newInterfaceBackboneListenIp = "0.0.0.0";
|
|
wrapper.vm.newInterfaceBackboneListenPort = 5151;
|
|
wrapper.vm.newInterfaceBlockFastFlapping = false;
|
|
wrapper.vm.newInterfaceFastFlappingBlockTime = 30;
|
|
wrapper.vm.newInterfaceFastFlappingThreshold = 10;
|
|
wrapper.vm.newInterfaceFastFlappingGrace = 2;
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
block_fast_flapping: false,
|
|
fast_flapping_block_time: 30,
|
|
fast_flapping_threshold: 10,
|
|
fast_flapping_grace: 2,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends discovery location_cmd when discoverable", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "TCPS";
|
|
wrapper.vm.newInterfaceType = "TCPServerInterface";
|
|
wrapper.vm.newInterfaceListenIp = "0.0.0.0";
|
|
wrapper.vm.newInterfaceListenPort = 4242;
|
|
wrapper.vm.discovery.discoverable = true;
|
|
wrapper.vm.discovery.discovery_name = "Public";
|
|
wrapper.vm.discovery.location_cmd = "/usr/local/bin/gps-loc";
|
|
wrapper.vm.sharedInterfaceSettings.mode = "gateway";
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
discoverable: "yes",
|
|
location_cmd: "/usr/local/bin/gps-loc",
|
|
mode: "gateway",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("sends HTTPInterface client tunnel payload", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "HttpClient";
|
|
wrapper.vm.newInterfaceType = "HTTPInterface";
|
|
wrapper.vm.newInterfaceHttpTunnelMode = "client";
|
|
wrapper.vm.newInterfaceHttpServerUrl = "https://example.com:8080/";
|
|
wrapper.vm.newInterfaceHttpPollInterval = 0.2;
|
|
wrapper.vm.newInterfaceHttpMtu = 2048;
|
|
wrapper.vm.newInterfaceHttpVersion = 1;
|
|
wrapper.vm.newInterfaceHttpUserAgent = "RNS-HTTP-Tunnel/1.0";
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).toHaveBeenCalledWith(
|
|
"/api/v1/reticulum/interfaces/add",
|
|
expect.objectContaining({
|
|
type: "HTTPInterface",
|
|
mode: "client",
|
|
server_url: "https://example.com:8080/",
|
|
poll_interval: 0.2,
|
|
mtu: 2048,
|
|
http_version: 1,
|
|
user_agent: "RNS-HTTP-Tunnel/1.0",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("blocks HTTPInterface client save without server_url", async () => {
|
|
const wrapper = mountPage();
|
|
|
|
wrapper.vm.newInterfaceName = "HttpClient";
|
|
wrapper.vm.newInterfaceType = "HTTPInterface";
|
|
wrapper.vm.newInterfaceHttpTunnelMode = "client";
|
|
wrapper.vm.newInterfaceHttpServerUrl = " ";
|
|
|
|
await wrapper.vm.saveInterface();
|
|
|
|
expect(mockAxios.post).not.toHaveBeenCalled();
|
|
});
|
|
});
|