Guided-auth removal, OAuth smoke testing, and TUI/CLI support (#1531)

* refactor(web): update OAuth E2E tests to use new authentication method

- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.

Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.

* Guided-auth removal, OAuth smoke testing, and TUI/CLI support

* Added CLI OAuth runner flags tests

* Added clarifying comment on port 0 binding

* README notes on TUI oauth callback port, prettier fixes for CLI tests

* CIMD validation uses inline like EMA IdP

* Fixed TUI test failures from merge

* test: bring 5 OAuth files over the 90 per-file coverage gate

The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:

- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
  throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
  provably-dead guards (empty host, out-of-range port, String(err)
  fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
  with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
  the stale-onChange-after-close prev===null guard, and the
  timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
  kinds, the expired IdP session, and the configured-scope/empty-scope
  branches of formatScopes.

npm run coverage now passes (exit 0) across web, cli, tui, launcher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bob Dickinson 2026-06-29 12:35:51 -07:00 • committed by GitHub
commit 13dafe457b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 5702 additions and 4444 deletions

View file

@ -13,6 +13,8 @@ const configuredSettings: ClientSettingsFormValues = {
issuer: "https://idp.example.com",
clientId: "inspector-idp-client",
clientSecret: "super-secret-idp-value",
cimdEnabled: true,
clientMetadataUrl: "https://example.com/oauth/client.json",
};
function InteractiveRender(args: ClientSettingsModalProps) {
@ -25,7 +27,11 @@ function InteractiveRender(args: ClientSettingsModalProps) {
{...args}
onSettingsChange={(settings) => {
args.onSettingsChange(settings);
updateArgs({ settings });
const next =
typeof settings === "function"
? settings(args.settings)
: settings;
updateArgs({ settings: next });
}}
/>
</AppShell.Main>
@ -61,6 +67,8 @@ export const Empty: Story = {
issuer: "",
clientId: "",
clientSecret: "",
cimdEnabled: false,
clientMetadataUrl: "",
},
},
};
@ -72,6 +80,8 @@ export const EnabledEmptyFields: Story = {
issuer: "",
clientId: "",
clientSecret: "",
cimdEnabled: false,
clientMetadataUrl: "",
},
},
};

View file

@ -6,7 +6,24 @@ import { ClientSettingsModal } from "./ClientSettingsModal";
import {
EMPTY_CLIENT_SETTINGS,
ISSUER_URL_ERROR,
type ClientSettingsFormValues,
} from "../ClientSettingsForm/clientSettingsValues";
import { CIMD_METADATA_URL_INVALID_ERROR } from "@inspector/core/client/config-parse.js";
function resolveSettingsChange(
call: unknown,
prev: ClientSettingsFormValues,
): ClientSettingsFormValues {
return typeof call === "function"
? (call as (p: ClientSettingsFormValues) => ClientSettingsFormValues)(prev)
: (call as ClientSettingsFormValues);
}
async function expandCimdSection(user: ReturnType<typeof userEvent.setup>) {
await user.click(
screen.getByRole("button", { name: /OAuth Client ID Metadata Document/i }),
);
}
describe("ClientSettingsModal", () => {
it("renders the title when opened", () => {
@ -54,7 +71,13 @@ describe("ClientSettingsModal", () => {
name: "Enable enterprise IdP configuration",
}),
);
expect(onSettingsChange).toHaveBeenCalledWith({
expect(onSettingsChange).toHaveBeenCalledWith(expect.any(Function));
expect(
resolveSettingsChange(
onSettingsChange.mock.calls[0]![0],
EMPTY_CLIENT_SETTINGS,
),
).toEqual({
...EMPTY_CLIENT_SETTINGS,
emaEnabled: true,
});
@ -85,11 +108,12 @@ describe("ClientSettingsModal", () => {
onSettingsChange={onSettingsChange}
/>,
);
const initial = { ...EMPTY_CLIENT_SETTINGS, emaEnabled: true };
await user.type(screen.getByLabelText("Issuer"), "h");
expect(onSettingsChange).toHaveBeenCalled();
const call =
onSettingsChange.mock.calls[onSettingsChange.mock.calls.length - 1][0];
expect(call.issuer).toBe("h");
onSettingsChange.mock.calls[onSettingsChange.mock.calls.length - 1]![0];
expect(resolveSettingsChange(call, initial).issuer).toBe("h");
});
it("collapses then re-expands all sections via the list toggle", async () => {
@ -103,21 +127,23 @@ describe("ClientSettingsModal", () => {
/>,
);
// Starts fully expanded (all sections open), so the toggle offers "Collapse all".
const collapse = screen.getByRole("button", { name: "Collapse all" });
expect(collapse).toBeInTheDocument();
// The EMA section panel content is visible while expanded.
// Default: EMA open, CIMD collapsed — toggle offers "Expand all".
const expand = screen.getByRole("button", { name: "Expand all" });
expect(expand).toBeInTheDocument();
expect(
screen.getByText("Enable enterprise IdP configuration"),
).toBeInTheDocument();
// Collapse all -> handleToggleAll sets expandedSections to [].
await user.click(collapse);
await user.click(expand);
expect(
screen.getByRole("button", { name: "Collapse all" }),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Collapse all" }));
expect(
screen.getByRole("button", { name: "Expand all" }),
).toBeInTheDocument();
// Expand all -> handleToggleAll restores ALL_SECTIONS.
await user.click(screen.getByRole("button", { name: "Expand all" }));
expect(
screen.getByRole("button", { name: "Collapse all" }),
@ -212,6 +238,100 @@ describe("ClientSettingsModal", () => {
expect(onClose).toHaveBeenCalledTimes(1);
});
it("blocks close and reveals the CIMD URL error when the URL is invalid", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
renderWithMantine(
<ClientSettingsModal
opened
settings={{
...EMPTY_CLIENT_SETTINGS,
cimdEnabled: true,
clientMetadataUrl: "not-a-url",
}}
onClose={onClose}
onSettingsChange={vi.fn()}
/>,
);
expect(
screen.queryByText(CIMD_METADATA_URL_INVALID_ERROR),
).not.toBeInTheDocument();
await user.click(
document.querySelector("button.mantine-CloseButton-root")!,
);
expect(onClose).not.toHaveBeenCalled();
expect(
screen.getByText(CIMD_METADATA_URL_INVALID_ERROR),
).toBeInTheDocument();
});
it("allows close once an invalid CIMD URL is corrected", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
function Host() {
const [settings, setSettings] = useState({
...EMPTY_CLIENT_SETTINGS,
cimdEnabled: true,
clientMetadataUrl: "not-a-url",
});
return (
<ClientSettingsModal
opened
settings={settings}
onClose={onClose}
onSettingsChange={setSettings}
/>
);
}
renderWithMantine(<Host />);
const close = () =>
user.click(document.querySelector("button.mantine-CloseButton-root")!);
await close();
expect(onClose).not.toHaveBeenCalled();
await expandCimdSection(user);
const url = screen.getByLabelText("Client ID metadata document URL");
await user.clear(url);
await user.type(url, "https://example.com/cimd.json");
expect(
screen.queryByText(CIMD_METADATA_URL_INVALID_ERROR),
).not.toBeInTheDocument();
await close();
expect(onClose).toHaveBeenCalledTimes(1);
});
it("allows close when an invalid CIMD URL is cleared (empty is valid to leave)", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
function Host() {
const [settings, setSettings] = useState({
...EMPTY_CLIENT_SETTINGS,
cimdEnabled: true,
clientMetadataUrl: "not-a-url",
});
return (
<ClientSettingsModal
opened
settings={settings}
onClose={onClose}
onSettingsChange={setSettings}
/>
);
}
renderWithMantine(<Host />);
await expandCimdSection(user);
await user.clear(screen.getByLabelText("Client ID metadata document URL"));
await user.click(
document.querySelector("button.mantine-CloseButton-root")!,
);
expect(onClose).toHaveBeenCalledTimes(1);
});
it("does not render the modal body when closed", () => {
renderWithMantine(
<ClientSettingsModal

View file

@ -11,13 +11,17 @@ import {
type ClientSettingsFormValues,
} from "../ClientSettingsForm/clientSettingsValues.js";
const ALL_SECTIONS: ClientSettingsSection[] = ["ema"];
const ALL_SECTIONS: ClientSettingsSection[] = ["ema", "cimd"];
export interface ClientSettingsModalProps {
opened: boolean;
settings: ClientSettingsFormValues;
onClose: () => void;
onSettingsChange: (settings: ClientSettingsFormValues) => void;
onSettingsChange: (
settings:
| ClientSettingsFormValues
| ((prev: ClientSettingsFormValues) => ClientSettingsFormValues),
) => void;
emaIdpLoginState?: EmaIdpLoginState;
onEmaIdpLogout?: () => void;
}
@ -34,6 +38,8 @@ export function ClientSettingsModal({
ClientSettingsSection[]
>(["ema"]);
const [revealIssuerError, setRevealIssuerError] = useState(false);
const [revealClientMetadataUrlError, setRevealClientMetadataUrlError] =
useState(false);
const allExpanded = expandedSections.length === ALL_SECTIONS.length;
@ -42,14 +48,20 @@ export function ClientSettingsModal({
}
// Closing (X / Esc / overlay) is the implicit "save" for this auto-saving
// modal — the parent flushes the debounced persist on close. If the issuer is
// invalid the persist gate would silently drop it, so instead of closing we
// reveal the issuer error (overriding the form's on-blur gating). The user
// can fix the URL or clear the field — an empty issuer is valid to leave —
// and then close. Resets via the parent's open/close remount key.
// modal — the parent flushes the debounced persist on close. If a validated
// URL field is invalid the persist gate would silently drop it, so instead of
// closing we reveal the field error (overriding the form's on-blur gating).
// The user can fix the URL or clear the field — an empty value is valid to
// leave — and then close. Resets via the parent's open/close remount key.
function handleClose() {
if (validateClientSettings(settings).issuer) {
const errors = validateClientSettings(settings);
if (errors.issuer) {
setRevealIssuerError(true);
}
if (errors.clientMetadataUrl) {
setRevealClientMetadataUrlError(true);
}
if (errors.issuer || errors.clientMetadataUrl) {
return;
}
onClose();
@ -83,6 +95,7 @@ export function ClientSettingsModal({
emaIdpLoginState={emaIdpLoginState}
onEmaIdpLogout={onEmaIdpLogout}
revealIssuerError={revealIssuerError}
revealClientMetadataUrlError={revealClientMetadataUrlError}
/>
</Stack>
</Modal>