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:
parent
4a927d3978
commit
13dafe457b
3 changed files with 5702 additions and 4444 deletions
|
|
@ -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: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue