import { describe, expect, it } from "vitest"; import { isRejected, knownHostToken, parseKnownHosts, resolveComputer, resolveComputers, type SshConfigResolveEnv, } from "./config.js"; const env = (overrides: Partial = {}): SshConfigResolveEnv => ({ configText: "", knownHostsText: "", defaultUser: "fallback-user", homeDir: "/home/test", ...overrides, }); const FIXTURE = ` # top-level comment Host * ServerAliveInterval 60 Host myserver HostName 10.0.0.5 Port 2222 User deploy IdentityFile ~/.ssh/deploy_key Host web *.example.com HostName web.internal User webuser Host barehost # no HostName → falls back to alias Host github.com HostName github.com User git IdentityFile ~/.ssh/github_key `; describe("resolveComputers", () => { it("returns one Computer per named (non-wildcard) Host alias, sorted", () => { const computers = resolveComputers(env({ configText: FIXTURE })); expect(computers.map((c) => c.alias)).toEqual(["barehost", "github.com", "myserver", "web"]); }); it("skips wildcard-only Host patterns (* and ?)", () => { const computers = resolveComputers(env({ configText: FIXTURE })); // The bare `*` host is a pattern, not a computer — excluded. expect(computers.find((c) => c.alias === "*")).toBeUndefined(); }); it("skips wildcard aliases within a multi-alias Host line (*.example.com)", () => { const computers = resolveComputers(env({ configText: FIXTURE })); expect(computers.find((c) => c.alias === "*.example.com")).toBeUndefined(); // but the named alias on the SAME line (web) is included. expect(computers.find((c) => c.alias === "web")).toBeDefined(); }); it("resolves HostName/Port/User/IdentityFile from the config (first-match-wins)", () => { const computers = resolveComputers(env({ configText: FIXTURE })); const my = computers.find((c) => c.alias === "myserver"); expect(my).toEqual({ alias: "myserver", hostName: "10.0.0.5", port: 2222, user: "deploy", identityFile: "/home/test/.ssh/deploy_key", knownHost: false, }); }); it("falls back HostName → alias when no HostName is set", () => { const computers = resolveComputers(env({ configText: FIXTURE })); const bare = computers.find((c) => c.alias === "barehost"); expect(bare?.hostName).toBe("barehost"); expect(bare?.port).toBe(22); expect(bare?.user).toBe("fallback-user"); expect(bare?.identityFile).toBeNull(); }); it("expands ~ in IdentityFile to homeDir", () => { const computers = resolveComputers(env({ configText: FIXTURE })); const gh = computers.find((c) => c.alias === "github.com"); expect(gh?.identityFile).toBe("/home/test/.ssh/github_key"); }); it("resolves a Host block whose first alias is a wildcard but later alias is named", () => { const computers = resolveComputers(env({ configText: FIXTURE })); const web = computers.find((c) => c.alias === "web"); expect(web?.hostName).toBe("web.internal"); expect(web?.user).toBe("webuser"); }); it("de-dups aliases listed in multiple Host lines (first wins)", () => { const dup = ` Host dup HostName first.example Host dup HostName second.example `; const computers = resolveComputers(env({ configText: dup })); expect(computers).toHaveLength(1); expect(computers[0]?.hostName).toBe("first.example"); }); it("knownHost=true when the resolved HostName:port token is in known_hosts", () => { // myserver is port 2222 → token is [10.0.0.5]:2222; web is port 22 → bare host. const known = "[10.0.0.5]:2222 ssh-ed25519 AAA\nweb.internal ssh-ed25519 BBB\n"; const computers = resolveComputers(env({ configText: FIXTURE, knownHostsText: known })); expect(computers.find((c) => c.alias === "myserver")?.knownHost).toBe(true); // default port 22 → token is just the hostName (no bracket). expect(computers.find((c) => c.alias === "web")?.knownHost).toBe(true); expect(computers.find((c) => c.alias === "barehost")?.knownHost).toBe(false); }); it("knownHost keys a non-default port as [host]:port", () => { const known = "[10.0.0.5]:2222 ssh-ed25519 AAA\n"; const computers = resolveComputers(env({ configText: FIXTURE, knownHostsText: known })); expect(computers.find((c) => c.alias === "myserver")?.knownHost).toBe(true); }); }); describe("resolveComputer (single alias)", () => { it("resolves a named alias", () => { const c = resolveComputer("myserver", env({ configText: FIXTURE })); expect(c?.hostName).toBe("10.0.0.5"); expect(c?.port).toBe(2222); }); it("returns null for an unknown alias", () => { expect(resolveComputer("nope", env({ configText: FIXTURE }))).toBeNull(); }); it("returns null for a wildcard alias (not a selectable computer)", () => { expect(resolveComputer("*.example.com", env({ configText: FIXTURE }))).toBeNull(); }); it("applies top-level wildcard defaults to a named host (first-match-wins)", () => { const cfg = ` Host * ServerAliveInterval 60 User stardefault Host named HostName named.example `; const c = resolveComputer("named", env({ configText: cfg })); // User inherited from the `Host *` block via first-match-wins. expect(c?.user).toBe("stardefault"); expect(c?.hostName).toBe("named.example"); }); }); describe("knownHostToken", () => { it("returns the bare host for the default port (22)", () => { expect(knownHostToken("host.example", 22)).toBe("host.example"); }); it("returns [host]:port for a non-default port", () => { expect(knownHostToken("host.example", 2222)).toBe("[host.example]:2222"); }); }); // ─── known_hosts discovery ───────────────────────────────────────────────── const KNOWN_HOSTS_FIXTURE = [ "# comment line", "arch-razer ssh-ed25519 AAAA1", "xenifse1 ssh-ed25519 AAAA2", "xenifse1 ssh-rsa AAAA3", "xenifse1 ecdsa-sha2-nistp256 AAAA4", "[xenifse1]:2222 ssh-ed25519 AAAA5", "github.com ssh-ed25519 AAAA6", "|1|+W4/jC7U8rJwiEC2m0KvG2A7uAA=|rWwV8p5J1FfR3k= ssh-ed25519 AAAA7", "localhost ssh-ed25519 AAAA8", "[localhost]:2222 ssh-ed25519 AAAA9", ].join("\n"); describe("parseKnownHosts", () => { it("extracts hostnames with default port 22 from bare entries", () => { const entries = parseKnownHosts(KNOWN_HOSTS_FIXTURE); const arch = entries.find((e) => e.hostname === "arch-razer"); expect(arch).toEqual({ hostname: "arch-razer", port: 22 }); }); it("extracts hostname + port from [host]:port notation", () => { const entries = parseKnownHosts(KNOWN_HOSTS_FIXTURE); // xenifse1 has entries on port 22 (first) and port 2222 — first wins. const xen = entries.find((e) => e.hostname === "xenifse1"); expect(xen?.port).toBe(22); // localhost appears as port 22 (first) and [localhost]:2222 — first wins. const local = entries.find((e) => e.hostname === "localhost"); expect(local?.port).toBe(22); }); it("deduplicates by hostname (first port wins)", () => { const entries = parseKnownHosts(KNOWN_HOSTS_FIXTURE); const xenCount = entries.filter((e) => e.hostname === "xenifse1").length; expect(xenCount).toBe(1); // Multiple key types (ed25519, rsa, ecdsa) for the same host:port = 1 entry. expect(entries).toHaveLength(4); // arch-razer, xenifse1, github.com, localhost }); it("skips hashed entries (|1|...)", () => { const entries = parseKnownHosts(KNOWN_HOSTS_FIXTURE); expect(entries.find((e) => e.hostname.startsWith("|"))).toBeUndefined(); }); it("skips comment lines and empty lines", () => { const entries = parseKnownHosts("\n# comment\n\n \nhost1 ssh-ed25519 AAA\n"); expect(entries).toHaveLength(1); expect(entries[0]?.hostname).toBe("host1"); }); it("returns empty for empty text", () => { expect(parseKnownHosts("")).toEqual([]); expect(parseKnownHosts(" \n\n ")).toEqual([]); }); it("handles comma-separated host markers", () => { const entries = parseKnownHosts("hostA,hostB ssh-ed25519 AAA\n"); expect(entries.map((e) => e.hostname)).toEqual(["hostA", "hostB"]); }); it("preserves non-default port when the host only has [host]:port entries", () => { const entries = parseKnownHosts("[specialhost]:9999 ssh-ed25519 AAA\n"); expect(entries[0]).toEqual({ hostname: "specialhost", port: 9999 }); }); }); // ─── known_hosts discovery merged into resolveComputers ─────────────────── describe("resolveComputers with known_hosts discovery", () => { it("discovers computers from known_hosts when no ~/.ssh/config exists", () => { const computers = resolveComputers(env({ knownHostsText: KNOWN_HOSTS_FIXTURE })); const aliases = computers.map((c) => c.alias); expect(aliases).toContain("arch-razer"); expect(aliases).toContain("xenifse1"); expect(aliases).toContain("github.com"); expect(aliases).toContain("localhost"); }); it("defaulted params for known_hosts-discovered computers", () => { const computers = resolveComputers(env({ knownHostsText: KNOWN_HOSTS_FIXTURE })); const arch = computers.find((c) => c.alias === "arch-razer"); expect(arch).toEqual({ alias: "arch-razer", hostName: "arch-razer", port: 22, user: "fallback-user", identityFile: null, knownHost: true, }); }); it("config entries take precedence over known_hosts (no duplication)", () => { const cfg = ` Host arch-razer HostName 192.168.1.100 User root Port 2222 `; const computers = resolveComputers( env({ configText: cfg, knownHostsText: KNOWN_HOSTS_FIXTURE }), ); const arch = computers.filter((c) => c.alias === "arch-razer"); expect(arch).toHaveLength(1); // Config params win, not the known_hosts defaults. expect(arch[0]?.hostName).toBe("192.168.1.100"); expect(arch[0]?.user).toBe("root"); expect(arch[0]?.port).toBe(2222); }); it("merges config + known_hosts entries (union, sorted)", () => { const cfg = ` Host server-a HostName 10.0.0.1 `; const computers = resolveComputers( env({ configText: cfg, knownHostsText: KNOWN_HOSTS_FIXTURE }), ); const aliases = computers.map((c) => c.alias); // config entry + known_hosts entries, all unique, sorted. expect(aliases).toEqual(["arch-razer", "github.com", "localhost", "server-a", "xenifse1"]); }); }); // ─── reject-list filtering ──────────────────────────────────────────────── describe("resolveComputers with rejectPatterns", () => { it("filters out exact-match rejected hostnames", () => { const computers = resolveComputers( env({ knownHostsText: KNOWN_HOSTS_FIXTURE, rejectPatterns: ["github.com"], }), ); expect(computers.find((c) => c.alias === "github.com")).toBeUndefined(); expect(computers.find((c) => c.alias === "arch-razer")).toBeDefined(); }); it("filters out glob patterns (* matches any sequence)", () => { const computers = resolveComputers( env({ knownHostsText: KNOWN_HOSTS_FIXTURE, rejectPatterns: ["*.com"], }), ); expect(computers.find((c) => c.alias === "github.com")).toBeUndefined(); expect(computers.find((c) => c.alias === "arch-razer")).toBeDefined(); }); it("filters multiple patterns", () => { const computers = resolveComputers( env({ knownHostsText: KNOWN_HOSTS_FIXTURE, rejectPatterns: ["github.com", "localhost"], }), ); const aliases = computers.map((c) => c.alias); expect(aliases).toEqual(["arch-razer", "xenifse1"]); }); it("does NOT filter resolveComputer (single alias lookup ignores reject list)", () => { // resolveComputer is for specific lookups (status, test, connect) — // the reject list is a discovery/catalog filter, not access control. const c = resolveComputer( "github.com", env({ knownHostsText: KNOWN_HOSTS_FIXTURE, rejectPatterns: ["github.com"] }), ); expect(c).not.toBeNull(); expect(c?.alias).toBe("github.com"); }); it("empty rejectPatterns does not filter anything", () => { const computers = resolveComputers( env({ knownHostsText: KNOWN_HOSTS_FIXTURE, rejectPatterns: [] }), ); expect(computers).toHaveLength(4); }); it("absent rejectPatterns does not filter anything", () => { const computers = resolveComputers(env({ knownHostsText: KNOWN_HOSTS_FIXTURE })); expect(computers).toHaveLength(4); }); }); // ─── resolveComputer known_hosts fallback ───────────────────────────────── describe("resolveComputer with known_hosts fallback", () => { it("resolves a host from known_hosts when not in config", () => { const c = resolveComputer("arch-razer", env({ knownHostsText: KNOWN_HOSTS_FIXTURE })); expect(c).toEqual({ alias: "arch-razer", hostName: "arch-razer", port: 22, user: "fallback-user", identityFile: null, knownHost: true, }); }); it("returns null for a host in neither config nor known_hosts", () => { const c = resolveComputer("nonexistent", env({ knownHostsText: KNOWN_HOSTS_FIXTURE })); expect(c).toBeNull(); }); it("config takes precedence over known_hosts for resolveComputer", () => { const cfg = ` Host arch-razer HostName 192.168.1.100 User root `; const c = resolveComputer( "arch-razer", env({ configText: cfg, knownHostsText: KNOWN_HOSTS_FIXTURE }), ); expect(c?.hostName).toBe("192.168.1.100"); expect(c?.user).toBe("root"); }); }); // ─── isRejected glob matching ────────────────────────────────────────────── describe("isRejected", () => { it("matches exact hostname", () => { expect(isRejected("github.com", ["github.com"])).toBe(true); expect(isRejected("arch-razer", ["github.com"])).toBe(false); }); it("matches * glob (any sequence)", () => { expect(isRejected("foo.ts.net", ["*.ts.net"])).toBe(true); // * matches zero chars, but the remaining ".ts.net" (with literal dot) // still doesn't match "ts.net" — the dot is required. expect(isRejected("ts.net", ["*.ts.net"])).toBe(false); expect(isRejected("foo.other.net", ["*.ts.net"])).toBe(false); }); it("matches ? glob (single char)", () => { expect(isRejected("host1", ["host?"])).toBe(true); expect(isRejected("host12", ["host?"])).toBe(false); // ? = exactly one char }); it("is case-insensitive", () => { expect(isRejected("GitHub.Com", ["github.com"])).toBe(true); expect(isRejected("FOO.TS.NET", ["*.ts.net"])).toBe(true); }); it("returns false for absent or empty patterns", () => { expect(isRejected("anything")).toBe(false); expect(isRejected("anything", [])).toBe(false); expect(isRejected("anything", undefined)).toBe(false); }); });