commit
e19d11070a
4 changed files with 211 additions and 4 deletions
|
|
@ -44,6 +44,7 @@ export function downloadJsonFile(filename: string, json: string): void {
|
|||
* nothing usable remains.
|
||||
*/
|
||||
export function fileNameFromUri(uri: string): string {
|
||||
/* v8 ignore next -- String.prototype.split always returns a non-empty array, so .pop() is never undefined; the `?? ""` fallback is unreachable. */
|
||||
const tail = uri.split(/[\\/]/).pop() ?? "";
|
||||
const safe = tail
|
||||
.replace(/[\p{Cc}\p{Cf}]+/gu, "")
|
||||
|
|
|
|||
111
clients/web/src/lib/environmentFactory.test.ts
Normal file
111
clients/web/src/lib/environmentFactory.test.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Tests for the browser `InspectorClientEnvironment` assembly.
|
||||
*
|
||||
* The three remote factories are mocked so we can (a) assert the `baseUrl` /
|
||||
* `authToken` derived from `window.location` are threaded into each, and
|
||||
* (b) capture the internal `fetchFn` wrapper and invoke it to prove it
|
||||
* delegates to `globalThis.fetch` (exercising the arrow body that exists to
|
||||
* preserve the global receiver). `BrowserNavigation` and the OAuth storage
|
||||
* accessor are the real implementations.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { BrowserNavigation } from "@inspector/core/auth/browser/index.js";
|
||||
import type { RedirectUrlProvider } from "@inspector/core/auth/index.js";
|
||||
|
||||
interface CapturedOptions {
|
||||
baseUrl: string;
|
||||
authToken: string | undefined;
|
||||
fetchFn: typeof fetch;
|
||||
}
|
||||
|
||||
const captured: {
|
||||
transport?: CapturedOptions;
|
||||
fetch?: CapturedOptions;
|
||||
logger?: CapturedOptions;
|
||||
} = {};
|
||||
|
||||
vi.mock("@inspector/core/mcp/remote/index.js", () => ({
|
||||
createRemoteTransport: (opts: CapturedOptions) => {
|
||||
captured.transport = opts;
|
||||
return { transport: true };
|
||||
},
|
||||
createRemoteFetch: (opts: CapturedOptions) => {
|
||||
captured.fetch = opts;
|
||||
return (async () => new Response("ok")) as unknown as typeof fetch;
|
||||
},
|
||||
createRemoteLogger: (opts: CapturedOptions) => {
|
||||
captured.logger = opts;
|
||||
return { info: vi.fn() };
|
||||
},
|
||||
}));
|
||||
|
||||
import { createWebEnvironment } from "./environmentFactory";
|
||||
|
||||
const REDIRECT: RedirectUrlProvider = {
|
||||
getRedirectUrl: () => "http://localhost/callback",
|
||||
};
|
||||
|
||||
describe("createWebEnvironment", () => {
|
||||
beforeEach(() => {
|
||||
captured.transport = undefined;
|
||||
captured.fetch = undefined;
|
||||
captured.logger = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("threads the window.location origin and auth token into every remote factory", () => {
|
||||
const expectedBaseUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const { environment, logger } = createWebEnvironment("tok-123", REDIRECT);
|
||||
|
||||
for (const opts of [captured.transport, captured.fetch, captured.logger]) {
|
||||
expect(opts?.baseUrl).toBe(expectedBaseUrl);
|
||||
expect(opts?.authToken).toBe("tok-123");
|
||||
}
|
||||
|
||||
// The returned logger is the same instance the factory produced.
|
||||
expect(logger).toBe(environment.logger);
|
||||
expect(environment.transport).toEqual({ transport: true });
|
||||
expect(typeof environment.fetch).toBe("function");
|
||||
});
|
||||
|
||||
it("passes an undefined auth token straight through", () => {
|
||||
createWebEnvironment(undefined, REDIRECT);
|
||||
expect(captured.logger?.authToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it("wraps fetch so the call preserves the global receiver", async () => {
|
||||
const spy = vi
|
||||
.spyOn(globalThis, "fetch")
|
||||
.mockResolvedValue(new Response("delegated", { status: 200 }));
|
||||
createWebEnvironment("tok", REDIRECT);
|
||||
|
||||
// Every factory receives the identical wrapper; invoking it must delegate.
|
||||
const wrapper = captured.transport!.fetchFn;
|
||||
const res = await wrapper("http://example.test/x");
|
||||
expect(spy).toHaveBeenCalledWith("http://example.test/x");
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it("builds a BrowserNavigation and wires the OAuth storage + redirect provider", () => {
|
||||
const onBeforeRedirect = vi.fn();
|
||||
const { environment } = createWebEnvironment(
|
||||
"tok",
|
||||
REDIRECT,
|
||||
onBeforeRedirect,
|
||||
);
|
||||
|
||||
const oauth = environment.oauth;
|
||||
expect(oauth?.navigation).toBeInstanceOf(BrowserNavigation);
|
||||
expect(oauth?.redirectUrlProvider).toBe(REDIRECT);
|
||||
expect(oauth?.storage).toBeDefined();
|
||||
});
|
||||
|
||||
it("works without an onBeforeOAuthRedirect callback", () => {
|
||||
const { environment } = createWebEnvironment(undefined, REDIRECT);
|
||||
expect(environment.oauth?.navigation).toBeInstanceOf(BrowserNavigation);
|
||||
});
|
||||
});
|
||||
98
clients/web/src/lib/types/customHeaders.test.ts
Normal file
98
clients/web/src/lib/types/customHeaders.test.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Tests for the custom-headers value helpers: constructing headers, filtering
|
||||
* enabled/non-blank ones, converting to/from a plain record, and the legacy
|
||||
* bearer-token migration.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createEmptyHeader,
|
||||
createHeaderFromBearerToken,
|
||||
getEnabledHeaders,
|
||||
headersToRecord,
|
||||
recordToHeaders,
|
||||
migrateFromLegacyAuth,
|
||||
type CustomHeaders,
|
||||
} from "./customHeaders";
|
||||
|
||||
describe("createEmptyHeader", () => {
|
||||
it("returns an enabled header with blank name and value", () => {
|
||||
expect(createEmptyHeader()).toEqual({
|
||||
name: "",
|
||||
value: "",
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createHeaderFromBearerToken", () => {
|
||||
it("defaults to an Authorization: Bearer header when no name is given", () => {
|
||||
expect(createHeaderFromBearerToken("abc")).toEqual({
|
||||
name: "Authorization",
|
||||
value: "Bearer abc",
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("prefixes Bearer when the header name is Authorization (any case)", () => {
|
||||
expect(createHeaderFromBearerToken("abc", "authorization")).toEqual({
|
||||
name: "authorization",
|
||||
value: "Bearer abc",
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the raw token for non-Authorization header names", () => {
|
||||
expect(createHeaderFromBearerToken("abc", "X-Api-Key")).toEqual({
|
||||
name: "X-Api-Key",
|
||||
value: "abc",
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEnabledHeaders", () => {
|
||||
it("keeps only enabled headers with non-blank name and value", () => {
|
||||
const headers: CustomHeaders = [
|
||||
{ name: "A", value: "1", enabled: true },
|
||||
{ name: "B", value: "2", enabled: false },
|
||||
{ name: "", value: "3", enabled: true },
|
||||
{ name: "C", value: " ", enabled: true },
|
||||
];
|
||||
expect(getEnabledHeaders(headers)).toEqual([
|
||||
{ name: "A", value: "1", enabled: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("headersToRecord", () => {
|
||||
it("trims names/values and drops disabled or blank entries", () => {
|
||||
const headers: CustomHeaders = [
|
||||
{ name: " X-One ", value: " a ", enabled: true },
|
||||
{ name: "X-Two", value: "b", enabled: false },
|
||||
];
|
||||
expect(headersToRecord(headers)).toEqual({ "X-One": "a" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("recordToHeaders", () => {
|
||||
it("maps each record entry to an enabled header", () => {
|
||||
expect(recordToHeaders({ "X-A": "1", "X-B": "2" })).toEqual([
|
||||
{ name: "X-A", value: "1", enabled: true },
|
||||
{ name: "X-B", value: "2", enabled: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrateFromLegacyAuth", () => {
|
||||
it("returns a single header for a bearer token", () => {
|
||||
expect(migrateFromLegacyAuth("abc", "X-Api-Key")).toEqual([
|
||||
{ name: "X-Api-Key", value: "abc", enabled: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty list when there is no bearer token", () => {
|
||||
expect(migrateFromLegacyAuth()).toEqual([]);
|
||||
expect(migrateFromLegacyAuth("")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -101,10 +101,7 @@ export default defineConfig(({ command }) => {
|
|||
include: [
|
||||
'src/components/**/*.{ts,tsx}',
|
||||
'src/utils/**/*.{ts,tsx}',
|
||||
// Pure, fully-tested sandbox CSP builder library. Listed by file (not a
|
||||
// `src/lib/**` glob) so the untested legacy siblings in src/lib are not
|
||||
// pulled under the gate before they have tests of their own.
|
||||
'src/lib/sandbox-csp.{ts,tsx}',
|
||||
'src/lib/**/*.{ts,tsx}',
|
||||
'clients/web/server/**/*.{ts,tsx}',
|
||||
path.join(repoRoot, 'core/mcp/**/*.{ts,tsx}'),
|
||||
path.join(repoRoot, 'core/react/**/*.{ts,tsx}'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue