Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
275 lines
8.4 KiB
TypeScript
275 lines
8.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import type { Host } from "./models";
|
|
import {
|
|
buildTelnetDeepLinkConnectionHost,
|
|
buildTelnetDeepLinkEphemeralHostFromSaved,
|
|
buildTelnetDeepLinkHostDraft,
|
|
buildTelnetDeepLinkOpenHost,
|
|
findTelnetDeepLinkHost,
|
|
materializeTelnetDeepLinkMatchHost,
|
|
parseTelnetDeepLink,
|
|
shouldHandleTelnetDeepLink,
|
|
} from "./telnetDeepLink";
|
|
|
|
const host = (overrides: Partial<Host>): Host => ({
|
|
id: overrides.id || "host-1",
|
|
label: overrides.label || "Example",
|
|
hostname: overrides.hostname || "example.com",
|
|
username: overrides.username ?? "",
|
|
port: overrides.port,
|
|
group: "",
|
|
tags: [],
|
|
os: "linux",
|
|
protocol: overrides.protocol ?? "telnet",
|
|
...overrides,
|
|
});
|
|
|
|
test("parseTelnetDeepLink accepts host and port", () => {
|
|
assert.deepEqual(parseTelnetDeepLink("telnet://router.example.com:2001"), {
|
|
rawUrl: "telnet://router.example.com:2001",
|
|
hostname: "router.example.com",
|
|
port: 2001,
|
|
});
|
|
});
|
|
|
|
test("parseTelnetDeepLink accepts credentials and IPv6 hosts", () => {
|
|
assert.deepEqual(parseTelnetDeepLink("telnet://admin:p%40ss@[2001:db8::10]:2323"), {
|
|
rawUrl: "telnet://admin:p%40ss@[2001:db8::10]:2323",
|
|
username: "admin",
|
|
password: "p@ss",
|
|
hostname: "2001:db8::10",
|
|
port: 2323,
|
|
});
|
|
});
|
|
|
|
test("parseTelnetDeepLink rejects unsupported or incomplete links", () => {
|
|
assert.equal(parseTelnetDeepLink("ssh://example.com"), null);
|
|
assert.equal(parseTelnetDeepLink("telnet://"), null);
|
|
assert.equal(parseTelnetDeepLink("telnet://example.com:99999"), null);
|
|
});
|
|
|
|
test("shouldHandleTelnetDeepLink respects the shared deep link setting", () => {
|
|
assert.equal(shouldHandleTelnetDeepLink("telnet://example.com:23", true), true);
|
|
assert.equal(shouldHandleTelnetDeepLink("telnet://example.com:23", false), false);
|
|
assert.equal(shouldHandleTelnetDeepLink("https://example.com", true), false);
|
|
});
|
|
|
|
test("findTelnetDeepLinkHost matches saved telnet hosts by hostname and port", () => {
|
|
const hosts = [
|
|
host({ id: "wrong-port", hostname: "example.com", port: 23 }),
|
|
host({ id: "match", hostname: "example.com", port: 2001 }),
|
|
host({ id: "ssh", hostname: "example.com", port: 2001, protocol: "ssh" }),
|
|
];
|
|
|
|
const match = findTelnetDeepLinkHost(hosts, parseTelnetDeepLink("telnet://example.com:2001")!);
|
|
|
|
assert.equal(match?.id, "match");
|
|
});
|
|
|
|
test("findTelnetDeepLinkHost treats omitted ports as the telnet default", () => {
|
|
const hosts = [
|
|
host({ id: "custom-port", hostname: "example.com", port: 2323 }),
|
|
host({ id: "default-port", hostname: "example.com" }),
|
|
];
|
|
|
|
const match = findTelnetDeepLinkHost(hosts, parseTelnetDeepLink("telnet://example.com")!);
|
|
|
|
assert.equal(match?.id, "default-port");
|
|
});
|
|
|
|
test("findTelnetDeepLinkHost avoids ambiguous saved hosts", () => {
|
|
const hosts = [
|
|
host({ id: "one", hostname: "example.com", port: 23 }),
|
|
host({ id: "two", hostname: "example.com", telnetEnabled: true, protocol: "ssh", telnetPort: 23 }),
|
|
];
|
|
|
|
const match = findTelnetDeepLinkHost(hosts, parseTelnetDeepLink("telnet://example.com")!);
|
|
|
|
assert.equal(match, null);
|
|
});
|
|
|
|
test("findTelnetDeepLinkHost can ignore URL username for one-time credential links", () => {
|
|
const hosts = [
|
|
host({
|
|
id: "saved",
|
|
hostname: "example.com",
|
|
port: 23,
|
|
telnetUsername: "vault-user",
|
|
}),
|
|
];
|
|
|
|
const strictMatch = findTelnetDeepLinkHost(
|
|
hosts,
|
|
parseTelnetDeepLink("telnet://link-user:link-password@example.com")!,
|
|
);
|
|
const credentialMatch = findTelnetDeepLinkHost(
|
|
hosts,
|
|
parseTelnetDeepLink("telnet://link-user:link-password@example.com")!,
|
|
{ ignoreTargetUsername: true },
|
|
);
|
|
|
|
assert.equal(strictMatch, null);
|
|
assert.equal(credentialMatch?.id, "saved");
|
|
});
|
|
|
|
test("buildTelnetDeepLinkConnectionHost forces a saved host to open with telnet", () => {
|
|
const savedHost = host({
|
|
id: "saved",
|
|
hostname: "example.com",
|
|
protocol: "ssh",
|
|
telnetEnabled: true,
|
|
telnetPort: 2323,
|
|
moshEnabled: true,
|
|
etEnabled: true,
|
|
});
|
|
|
|
const connectionHost = buildTelnetDeepLinkConnectionHost(savedHost);
|
|
|
|
assert.equal(connectionHost.protocol, "telnet");
|
|
assert.equal(connectionHost.telnetEnabled, true);
|
|
assert.equal(connectionHost.telnetPort, 2323);
|
|
assert.equal(connectionHost.moshEnabled, false);
|
|
assert.equal(connectionHost.etEnabled, false);
|
|
});
|
|
|
|
test("buildTelnetDeepLinkConnectionHost uses the telnet default for ssh hosts with telnet enabled", () => {
|
|
const savedHost = host({
|
|
id: "saved",
|
|
hostname: "example.com",
|
|
protocol: "ssh",
|
|
port: 22,
|
|
telnetEnabled: true,
|
|
});
|
|
|
|
const connectionHost = buildTelnetDeepLinkConnectionHost(savedHost);
|
|
|
|
assert.equal(connectionHost.protocol, "telnet");
|
|
assert.equal(connectionHost.port, 23);
|
|
assert.equal(connectionHost.telnetPort, 23);
|
|
});
|
|
|
|
test("buildTelnetDeepLinkEphemeralHostFromSaved keeps saved settings but uses URL credentials", () => {
|
|
const savedHost = host({
|
|
id: "saved",
|
|
label: "Saved",
|
|
hostname: "example.com",
|
|
username: "vault-user",
|
|
port: 22,
|
|
protocol: "ssh",
|
|
telnetEnabled: true,
|
|
telnetUsername: "vault-telnet-user",
|
|
telnetPassword: "vault-password",
|
|
telnetIdentityId: "identity-1",
|
|
proxyProfileId: "proxy-1",
|
|
charset: "gb18030",
|
|
group: "network",
|
|
});
|
|
|
|
const ephemeral = buildTelnetDeepLinkEphemeralHostFromSaved(
|
|
savedHost,
|
|
parseTelnetDeepLink("telnet://link-user:link-password@example.com")!,
|
|
{ id: "ephemeral-id", now: 789 },
|
|
);
|
|
|
|
assert.equal(ephemeral.id, "ephemeral-id");
|
|
assert.equal(ephemeral.ephemeral, true);
|
|
assert.equal(ephemeral.protocol, "telnet");
|
|
assert.equal(ephemeral.telnetEnabled, true);
|
|
assert.equal(ephemeral.telnetPort, 23);
|
|
assert.equal(ephemeral.username, "link-user");
|
|
assert.equal(ephemeral.telnetUsername, "link-user");
|
|
assert.equal(ephemeral.telnetPassword, "link-password");
|
|
assert.equal(ephemeral.telnetIdentityId, undefined);
|
|
assert.equal(ephemeral.savePassword, false);
|
|
assert.equal(ephemeral.group, "");
|
|
assert.equal(ephemeral.proxyProfileId, "proxy-1");
|
|
assert.equal(ephemeral.charset, "gb18030");
|
|
});
|
|
|
|
test("buildTelnetDeepLinkHostDraft prepares an ephemeral telnet host", () => {
|
|
const draft = buildTelnetDeepLinkHostDraft(
|
|
parseTelnetDeepLink("telnet://admin:secret@example.com:2323")!,
|
|
{ id: "new-id", now: 123 },
|
|
);
|
|
|
|
assert.deepEqual(draft, {
|
|
id: "new-id",
|
|
label: "admin@example.com",
|
|
hostname: "example.com",
|
|
username: "admin",
|
|
port: 2323,
|
|
group: "",
|
|
tags: [],
|
|
os: "linux",
|
|
protocol: "telnet",
|
|
telnetEnabled: true,
|
|
telnetPort: 2323,
|
|
telnetUsername: "admin",
|
|
telnetPassword: "secret",
|
|
savePassword: false,
|
|
ephemeral: true,
|
|
moshEnabled: false,
|
|
etEnabled: false,
|
|
createdAt: 123,
|
|
});
|
|
});
|
|
|
|
test("materializeTelnetDeepLinkMatchHost makes telnet identity usernames matchable", () => {
|
|
const savedHost = host({
|
|
id: "saved",
|
|
hostname: "example.com",
|
|
username: "",
|
|
protocol: "ssh",
|
|
telnetEnabled: true,
|
|
telnetIdentityId: "identity-1",
|
|
});
|
|
|
|
const materialized = materializeTelnetDeepLinkMatchHost(savedHost, [
|
|
{ id: "identity-1", username: "identity-user" },
|
|
]);
|
|
const match = findTelnetDeepLinkHost(
|
|
[materialized],
|
|
parseTelnetDeepLink("telnet://identity-user@example.com")!,
|
|
);
|
|
|
|
assert.equal(match?.id, "saved");
|
|
});
|
|
|
|
test("buildTelnetDeepLinkOpenHost falls back to a draft host when no saved host matches", () => {
|
|
const openHost = buildTelnetDeepLinkOpenHost(
|
|
[],
|
|
parseTelnetDeepLink("telnet://missing.example.com:2001")!,
|
|
{ id: "draft-id", now: 456 },
|
|
);
|
|
|
|
assert.equal(openHost.id, "draft-id");
|
|
assert.equal(openHost.hostname, "missing.example.com");
|
|
assert.equal(openHost.port, 2001);
|
|
assert.equal(openHost.protocol, "telnet");
|
|
assert.equal(openHost.telnetEnabled, true);
|
|
assert.equal(openHost.ephemeral, true);
|
|
});
|
|
|
|
test("buildTelnetDeepLinkOpenHost keeps URL password instead of strict matching a saved host", () => {
|
|
const openHost = buildTelnetDeepLinkOpenHost(
|
|
[
|
|
host({
|
|
id: "saved-alice",
|
|
hostname: "example.com",
|
|
port: 23,
|
|
telnetUsername: "alice",
|
|
telnetPassword: "saved-password",
|
|
}),
|
|
],
|
|
parseTelnetDeepLink("telnet://alice:new-password@example.com")!,
|
|
{ id: "draft-id", now: 456 },
|
|
);
|
|
|
|
assert.equal(openHost.id, "draft-id");
|
|
assert.equal(openHost.username, "alice");
|
|
assert.equal(openHost.telnetUsername, "alice");
|
|
assert.equal(openHost.telnetPassword, "new-password");
|
|
assert.equal(openHost.ephemeral, true);
|
|
});
|