[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
333
components/HostDetailsConnectionSections.test.tsx
Normal file
333
components/HostDetailsConnectionSections.test.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
import test from "node:test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import assert from "node:assert/strict";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import type { Host, SSHKey } from "../types.ts";
|
||||
import {
|
||||
applyEffectiveHostAuthMethodSelection,
|
||||
detachEffectiveHostIdentity,
|
||||
HOST_AUTH_METHOD_CHOICES,
|
||||
HostDetailsConnectionSections,
|
||||
removeSelectedHostCredential,
|
||||
shouldForceAuthMethodReselect,
|
||||
} from "./HostDetailsConnectionSections.tsx";
|
||||
import { TooltipProvider } from "./ui/tooltip.tsx";
|
||||
|
||||
const longCredentialLabel =
|
||||
"D:\\download\\acdrOgxses_wuzh02.hpccube.com_really_really_long_private_key_name.pem";
|
||||
|
||||
const availableKey: SSHKey = {
|
||||
id: "key-1",
|
||||
label: longCredentialLabel,
|
||||
type: "ED25519",
|
||||
privateKey: "",
|
||||
publicKey: "",
|
||||
source: "imported",
|
||||
category: "key",
|
||||
created: 1,
|
||||
};
|
||||
|
||||
const renderConnectionSections = (
|
||||
formOverrides: Record<string, unknown> = {},
|
||||
groupDefaults?: Record<string, unknown>,
|
||||
) =>
|
||||
renderToStaticMarkup(
|
||||
React.createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
React.createElement(HostDetailsConnectionSections, {
|
||||
t: (key: string) => key,
|
||||
form: {
|
||||
id: "host-1",
|
||||
label: "Host",
|
||||
hostname: "example.com",
|
||||
username: "root",
|
||||
port: 22,
|
||||
protocol: "ssh",
|
||||
os: "linux",
|
||||
authMethod: "key",
|
||||
identityFileId: availableKey.id,
|
||||
...formOverrides,
|
||||
},
|
||||
setForm: () => {},
|
||||
update: () => {},
|
||||
groupDefaults,
|
||||
effectiveAuthMethod: formOverrides.effectiveAuthMethod || formOverrides.authMethod || "key",
|
||||
effectiveIdentityId: formOverrides.effectiveIdentityId || formOverrides.identityId,
|
||||
selectedIdentity: undefined,
|
||||
clearIdentity: () => {},
|
||||
identities: [],
|
||||
identitySuggestionsOpen: false,
|
||||
filteredIdentitySuggestions: [],
|
||||
setIdentitySuggestionsOpen: () => {},
|
||||
availableKeys: [availableKey],
|
||||
applyIdentity: () => {},
|
||||
showPassword: false,
|
||||
setShowPassword: () => {},
|
||||
pendingReferenceKeyPath: null,
|
||||
setPendingReferenceKeyPath: () => {},
|
||||
selectedCredentialType: null,
|
||||
setSelectedCredentialType: () => {},
|
||||
credentialPopoverOpen: false,
|
||||
setCredentialPopoverOpen: () => {},
|
||||
keysByCategory: { key: [availableKey], certificate: [] },
|
||||
newKeyFilePath: "",
|
||||
setNewKeyFilePath: () => {},
|
||||
addLocalKeyFilePath: () => {},
|
||||
handleDistroModeChange: () => {},
|
||||
distroOptions: [],
|
||||
effectiveFormDistro: undefined,
|
||||
getDistroOptionLabel: () => "",
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
test("selected host credential keeps the remove button visible with a long name", () => {
|
||||
const markup = renderConnectionSections();
|
||||
|
||||
assert.match(markup, new RegExp(longCredentialLabel.replaceAll("\\", "\\\\")));
|
||||
assert.match(
|
||||
markup,
|
||||
/class="flex items-center gap-2 min-w-0 overflow-hidden p-2 rounded-md bg-secondary\/50 border border-border\/60"/,
|
||||
);
|
||||
assert.match(markup, /class="text-sm min-w-0 flex-1 truncate"/);
|
||||
assert.match(markup, /class="[^"]*h-6 w-6 shrink-0[^"]*"/);
|
||||
});
|
||||
|
||||
test("color and icon settings render for non-Linux hosts", () => {
|
||||
const markup = renderConnectionSections({
|
||||
os: "macos",
|
||||
distro: "macos",
|
||||
iconMode: "custom",
|
||||
iconId: "terminal",
|
||||
});
|
||||
|
||||
assert.match(markup, /hostDetails\.icon\.sectionTitle/);
|
||||
assert.match(markup, /hostDetails\.icon\.colorLabel/);
|
||||
assert.match(markup, /hostDetails\.icon\.manualLabel/);
|
||||
});
|
||||
|
||||
test("basic connection settings do not require operating system selection", () => {
|
||||
const markup = renderConnectionSections({ os: "windows" });
|
||||
assert.doesNotMatch(markup, /hostDetails\.os\./);
|
||||
});
|
||||
|
||||
test("host credentials expose automatic and password-only choices", () => {
|
||||
const markup = renderConnectionSections({
|
||||
authMethod: "auto",
|
||||
identityFileId: undefined,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
HOST_AUTH_METHOD_CHOICES.map(([value, labelKey]) => [value, labelKey]),
|
||||
[
|
||||
["auto", "hostDetails.auth.auto"],
|
||||
["password", "hostDetails.auth.passwordOnly"],
|
||||
["key", "hostDetails.auth.key"],
|
||||
["certificate", "hostDetails.auth.certificate"],
|
||||
],
|
||||
);
|
||||
assert.match(markup, /hostDetails\.auth\.method/);
|
||||
assert.match(markup, /role="combobox"[^>]*aria-label="hostDetails\.auth\.method"/);
|
||||
assert.match(markup, /hostDetails\.auth\.auto/);
|
||||
assert.match(markup, /hostDetails\.auth\.mfaFirst/);
|
||||
// Login method uses the same option-style setting row as other host form controls.
|
||||
assert.match(
|
||||
markup,
|
||||
/hostDetails\.auth\.method[\s\S]*?role="combobox"[\s\S]*?hostDetails\.auth\.auto[\s\S]*?<\/div>\s*<div class="flex h-12 items-center justify-between gap-3 rounded-lg border border-border\/60 bg-secondary\/40 px-3">[\s\S]*hostDetails\.auth\.mfaFirst/,
|
||||
);
|
||||
assert.match(markup, /class="[^"]*h-8 w-32[^"]*"/);
|
||||
assert.match(
|
||||
markup,
|
||||
/role="combobox"[\s\S]*?<span class="min-w-0 flex-1 truncate text-left">hostDetails\.auth\.auto<\/span>/,
|
||||
);
|
||||
});
|
||||
|
||||
test("wires same-value key/certificate reselect through pointer and keyboard handlers", () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(path.dirname(fileURLToPath(import.meta.url)), "HostDetailsConnectionSections.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(source, /onPointerUp=\{\(\) => \{[\s\S]*shouldForceAuthMethodReselect/);
|
||||
assert.match(source, /onKeyDown=\{\(event\) => \{[\s\S]*shouldForceAuthMethodReselect/);
|
||||
assert.doesNotMatch(source, /onSelect=\{\(\) => \{[\s\S]*shouldForceAuthMethodReselect/);
|
||||
});
|
||||
|
||||
test("username identity suggestions use PopoverAnchor so focus click does not flash-close", () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(path.dirname(fileURLToPath(import.meta.url)), "HostDetailsConnectionSections.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
// Identity suggestions must anchor to the username field without making the
|
||||
// Input a PopoverTrigger (focus open + trigger toggle close = flash).
|
||||
assert.match(
|
||||
source,
|
||||
/identitySuggestionsOpen[\s\S]*?<PopoverAnchor asChild>[\s\S]*?hostDetails\.username\.placeholder[\s\S]*?<\/PopoverAnchor>/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/identitySuggestionsOpen[\s\S]*?<PopoverTrigger asChild>[\s\S]*?hostDetails\.username\.placeholder/,
|
||||
);
|
||||
// Anchor clicks are outside interactions in Radix 1.1.x; ignore them so
|
||||
// caret reposition / chevron toggle do not dismiss-then-reopen.
|
||||
assert.match(
|
||||
source,
|
||||
/data-identity-suggestions-anchor[\s\S]*?onInteractOutside=\{\(e\) => \{[\s\S]*?closest\("\[data-identity-suggestions-anchor\]"\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("reselecting the current key or certificate method still forces the chooser path", () => {
|
||||
assert.equal(shouldForceAuthMethodReselect("key", "key"), true);
|
||||
assert.equal(shouldForceAuthMethodReselect("certificate", "certificate"), true);
|
||||
assert.equal(shouldForceAuthMethodReselect("password", "password"), false);
|
||||
assert.equal(shouldForceAuthMethodReselect("auto", "auto"), false);
|
||||
assert.equal(shouldForceAuthMethodReselect("key", "password"), false);
|
||||
});
|
||||
|
||||
test("ET hosts do not offer the unsupported interactive-auth preference", () => {
|
||||
const legacyEtMarkup = renderConnectionSections({ etEnabled: true });
|
||||
const explicitEtMarkup = renderConnectionSections({ protocol: "et" });
|
||||
const inheritedEtMarkup = renderConnectionSections({}, { etEnabled: true });
|
||||
const hostOverrideMarkup = renderConnectionSections(
|
||||
{ etEnabled: false },
|
||||
{ etEnabled: true },
|
||||
);
|
||||
|
||||
assert.doesNotMatch(legacyEtMarkup, /hostDetails\.auth\.mfaFirst/);
|
||||
assert.doesNotMatch(explicitEtMarkup, /hostDetails\.auth\.mfaFirst/);
|
||||
assert.doesNotMatch(inheritedEtMarkup, /hostDetails\.auth\.mfaFirst/);
|
||||
assert.match(hostOverrideMarkup, /hostDetails\.auth\.mfaFirst/);
|
||||
});
|
||||
|
||||
test("host authentication choices remain visible for a selected identity", () => {
|
||||
const markup = renderConnectionSections({
|
||||
identityId: "identity-1",
|
||||
authMethod: "password",
|
||||
});
|
||||
|
||||
assert.match(markup, /hostDetails\.auth\.method/);
|
||||
assert.match(markup, /role="combobox"[^>]*aria-label="hostDetails\.auth\.method"/);
|
||||
assert.match(
|
||||
markup,
|
||||
/role="combobox"[\s\S]*?<span class="min-w-0 flex-1 truncate text-left">hostDetails\.auth\.passwordOnly<\/span>/,
|
||||
);
|
||||
});
|
||||
|
||||
test("host authentication choices show the inherited effective method", () => {
|
||||
const markup = renderConnectionSections({
|
||||
authMethod: undefined,
|
||||
identityFileId: undefined,
|
||||
effectiveAuthMethod: "password",
|
||||
});
|
||||
|
||||
assert.match(markup, /hostDetails\.auth\.method/);
|
||||
assert.match(markup, /role="combobox"[^>]*aria-label="hostDetails\.auth\.method"/);
|
||||
assert.match(
|
||||
markup,
|
||||
/role="combobox"[\s\S]*?<span class="min-w-0 flex-1 truncate text-left">hostDetails\.auth\.passwordOnly<\/span>/,
|
||||
);
|
||||
});
|
||||
|
||||
test("reselecting an inherited authentication method preserves group credentials", () => {
|
||||
const host = {
|
||||
id: "host-1",
|
||||
label: "Host",
|
||||
hostname: "example.com",
|
||||
username: "root",
|
||||
} as Host;
|
||||
|
||||
assert.equal(applyEffectiveHostAuthMethodSelection(host, "key", "key"), host);
|
||||
});
|
||||
|
||||
test("selecting automatic opts out of an inherited identity", () => {
|
||||
const host = {
|
||||
id: "host-1",
|
||||
label: "Host",
|
||||
hostname: "example.com",
|
||||
username: "root",
|
||||
authMethod: "auto",
|
||||
} as Host;
|
||||
|
||||
assert.deepEqual(
|
||||
applyEffectiveHostAuthMethodSelection(host, "auto", "password"),
|
||||
{
|
||||
...host,
|
||||
authPolicyVersion: 1,
|
||||
identityId: "",
|
||||
identityFileId: undefined,
|
||||
identityFilePaths: undefined,
|
||||
identityAgent: undefined,
|
||||
identitiesOnly: undefined,
|
||||
useSshAgent: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("overriding inherited authentication preserves the effective username", () => {
|
||||
const host = {
|
||||
id: "host-1",
|
||||
label: "Host",
|
||||
hostname: "example.com",
|
||||
username: "",
|
||||
} as Host;
|
||||
|
||||
assert.equal(
|
||||
applyEffectiveHostAuthMethodSelection(host, "password", "key", "deploy").username,
|
||||
"deploy",
|
||||
);
|
||||
});
|
||||
|
||||
test("detaching an inherited identity preserves the effective username", () => {
|
||||
const host = {
|
||||
id: "host-1",
|
||||
label: "Host",
|
||||
hostname: "example.com",
|
||||
username: "",
|
||||
} as Host;
|
||||
|
||||
assert.deepEqual(detachEffectiveHostIdentity(host, "deploy"), {
|
||||
...host,
|
||||
identityId: "",
|
||||
username: "deploy",
|
||||
});
|
||||
});
|
||||
|
||||
test("removing a selected key resets stale agent settings for automatic auth", () => {
|
||||
const host = {
|
||||
id: "host-1",
|
||||
label: "Host",
|
||||
hostname: "example.com",
|
||||
username: "root",
|
||||
authMethod: "key",
|
||||
authPolicyVersion: 1,
|
||||
identityFileId: "key-1",
|
||||
useSshAgent: false,
|
||||
} as Host;
|
||||
|
||||
assert.deepEqual(removeSelectedHostCredential(host, "key"), {
|
||||
...host,
|
||||
authMethod: "auto",
|
||||
identityId: "",
|
||||
identityFileId: undefined,
|
||||
identityFilePaths: undefined,
|
||||
identityAgent: undefined,
|
||||
identitiesOnly: undefined,
|
||||
useSshAgent: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("an inherited deleted identity remains visible and clearable", () => {
|
||||
const markup = renderConnectionSections({
|
||||
authMethod: undefined,
|
||||
identityFileId: undefined,
|
||||
effectiveAuthMethod: "auto",
|
||||
effectiveIdentityId: "deleted-group-identity",
|
||||
});
|
||||
|
||||
assert.match(markup, /hostDetails\.identity\.missing/);
|
||||
assert.doesNotMatch(markup, /placeholder="hostDetails\.username\.placeholder"/);
|
||||
});
|
||||
Reference in New Issue
Block a user