* test: switch integration project to folder-based manifest (#1314) Replace the `integrationTests` enumeration in vite.config.ts with a folder glob (`src/test/integration/**/*.test.ts`). Files under that folder run in the integration vitest project (node env, 30s timeouts); everything else runs in the unit project (happy-dom). This removes the silent misclassification trap from #1307: a new integration-style test no longer falls into the unit project (and happy-dom) just because someone forgot to add it to the array. The 13 ported v1.5 tests + their helpers move under src/test/integration/ in a strict mirror of core/ source layout (mcp/, auth/, auth/node/, storage/). Renames use git mv so history is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: address PR review on #1318 (glob extensions + AGENTS doc) - vite.config.ts: widen integrationGlob from `*.test.ts` to `*.test.{ts,tsx}` so the integration folder fully owns its contents. Without this, a `.test.tsx` placed under src/test/integration/ would match the unit project's `{ts,tsx}` include, fail the unit `.ts`-only exclude, and silently land under happy-dom — the exact misclassification trap this PR set out to close. - AGENTS.md: update the line describing where integration tests live; it still pointed at the old `src/test/core/` location and the removed `integrationTests` enumeration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02182e3b6c
commit
8bb389c8e3
1 changed files with 18 additions and 27 deletions
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* OAuth client test fixtures for InspectorClient OAuth tests.
|
||||
* These produce InspectorClient OAuth configuration and simulate OAuth flows.
|
||||
*/
|
||||
|
||||
import type {
|
||||
OAuthNavigation,
|
||||
RedirectUrlProvider,
|
||||
} from "@inspector/core/auth/providers.js";
|
||||
import type { OAuthStorage } from "@inspector/core/auth/storage.js";
|
||||
import { ConsoleNavigation } from "@inspector/core/auth/providers.js";
|
||||
import { NodeOAuthStorage } from "@inspector/core/auth/node/storage-node.js";
|
||||
|
||||
/** Creates a static RedirectUrlProvider for tests. Single URL for both modes. */
|
||||
function createStaticRedirectUrlProvider(
|
||||
redirectUrl: string,
|
||||
): RedirectUrlProvider {
|
||||
return {
|
||||
getRedirectUrl: () => redirectUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates OAuth configuration for InspectorClient tests
|
||||
*/
|
||||
export function createOAuthClientConfig(options: {
|
||||
mode: "static" | "dcr" | "cimd";
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
clientMetadataUrl?: string;
|
||||
redirectUrl: string;
|
||||
scope?: string;
|
||||
}): {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
clientMetadataUrl?: string;
|
||||
redirectUrlProvider: RedirectUrlProvider;
|
||||
scope?: string;
|
||||
storage: OAuthStorage;
|
||||
navigation: OAuthNavigation;
|
||||
} {
|
||||
const config: {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
clientMetadataUrl?: string;
|
||||
redirectUrlProvider: RedirectUrlProvider;
|
||||
scope?: string;
|
||||
storage: OAuthStorage;
|
||||
navigation: OAuthNavigation;
|
||||
} = {
|
||||
redirectUrlProvider: createStaticRedirectUrlProvider(options.redirectUrl),
|
||||
storage: new NodeOAuthStorage(),
|
||||
navigation: new ConsoleNavigation(),
|
||||
};
|
||||
|
||||
if (options.mode === "static") {
|
||||
if (!options.clientId) {
|
||||
throw new Error("clientId is required for static mode");
|
||||
}
|
||||
config.clientId = options.clientId;
|
||||
if (options.clientSecret) {
|
||||
config.clientSecret = options.clientSecret;
|
||||
}
|
||||
} else if (options.mode === "dcr") {
|
||||
// DCR mode - no clientId needed, will be registered
|
||||
if (options.clientId) {
|
||||
config.clientId = options.clientId;
|
||||
}
|
||||
} else if (options.mode === "cimd") {
|
||||
if (!options.clientMetadataUrl) {
|
||||
throw new Error("clientMetadataUrl is required for CIMD mode");
|
||||
}
|
||||
config.clientMetadataUrl = options.clientMetadataUrl;
|
||||
}
|
||||
|
||||
if (options.scope) {
|
||||
config.scope = options.scope;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client metadata document for CIMD testing
|
||||
*/
|
||||
export interface ClientMetadataDocument {
|
||||
redirect_uris: string[];
|
||||
token_endpoint_auth_method?: string;
|
||||
grant_types?: string[];
|
||||
response_types?: string[];
|
||||
client_name?: string;
|
||||
client_uri?: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Express server that serves a client metadata document for CIMD testing
|
||||
* The server runs on a different port and serves the metadata at the root path
|
||||
*
|
||||
* @param metadata - The client metadata document to serve
|
||||
* @returns Object with server URL and cleanup function
|
||||
*/
|
||||
export async function createClientMetadataServer(
|
||||
metadata: ClientMetadataDocument,
|
||||
): Promise<{ url: string; stop: () => Promise<void> }> {
|
||||
const express = await import("express");
|
||||
const app = express.default();
|
||||
|
||||
app.get("/", (_req, res) => {
|
||||
res.json(metadata);
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
reject(new Error("Failed to get server address"));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
const url = `http://localhost:${port}`;
|
||||
|
||||
resolve({
|
||||
url,
|
||||
stop: async () => {
|
||||
return new Promise<void>((resolveStop) => {
|
||||
server.close(() => {
|
||||
resolveStop();
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
server.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to programmatically complete OAuth authorization
|
||||
* Makes HTTP GET request to authorization URL and extracts authorization code
|
||||
* The test server's authorization endpoint auto-approves and redirects with code
|
||||
*
|
||||
* @param authorizationUrl - The authorization URL from oauthAuthorizationRequired event
|
||||
* @returns Authorization code extracted from redirect URL
|
||||
*/
|
||||
export async function completeOAuthAuthorization(
|
||||
authorizationUrl: URL,
|
||||
): Promise<string> {
|
||||
const response = await fetch(authorizationUrl.toString(), {
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
if (response.status !== 302 && response.status !== 301) {
|
||||
throw new Error(
|
||||
`Expected redirect (302/301), got ${response.status}: ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const redirectUrl = response.headers.get("location");
|
||||
if (!redirectUrl) {
|
||||
throw new Error("No Location header in redirect response");
|
||||
}
|
||||
|
||||
const redirectUrlObj = new URL(redirectUrl);
|
||||
const code = redirectUrlObj.searchParams.get("code");
|
||||
if (!code) {
|
||||
throw new Error(`No authorization code in redirect URL: ${redirectUrl}`);
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
Loading…
Reference in a new issue