* 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
|
|
@ -1,317 +0,0 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
discoverScopes,
|
||||
getAuthorizationServerUrl,
|
||||
} from "@inspector/core/auth/discovery.js";
|
||||
import type { OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
|
||||
// Mock SDK functions
|
||||
vi.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||
discoverAuthorizationServerMetadata: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("OAuth Scope Discovery", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return scopes from resource metadata when available", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "http://localhost:3000",
|
||||
authorization_endpoint: "http://localhost:3000/authorize",
|
||||
token_endpoint: "http://localhost:3000/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: "http://localhost:3000",
|
||||
authorization_servers: ["http://localhost:3000"],
|
||||
scopes_supported: ["read", "write", "admin"],
|
||||
};
|
||||
|
||||
const scopes = await discoverScopes(
|
||||
"http://localhost:3000",
|
||||
resourceMetadata,
|
||||
);
|
||||
|
||||
expect(scopes).toBe("read write admin");
|
||||
});
|
||||
|
||||
it("should fall back to OAuth metadata scopes when resource metadata has no scopes", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "http://localhost:3000",
|
||||
authorization_endpoint: "http://localhost:3000/authorize",
|
||||
token_endpoint: "http://localhost:3000/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: "http://localhost:3000",
|
||||
authorization_servers: ["http://localhost:3000"],
|
||||
scopes_supported: [],
|
||||
};
|
||||
|
||||
const scopes = await discoverScopes(
|
||||
"http://localhost:3000",
|
||||
resourceMetadata,
|
||||
);
|
||||
|
||||
expect(scopes).toBe("read write");
|
||||
});
|
||||
|
||||
it("should fall back to OAuth metadata scopes when resource metadata is not provided", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "http://localhost:3000",
|
||||
authorization_endpoint: "http://localhost:3000/authorize",
|
||||
token_endpoint: "http://localhost:3000/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
const scopes = await discoverScopes("http://localhost:3000");
|
||||
|
||||
expect(scopes).toBe("read write");
|
||||
});
|
||||
|
||||
it("should return undefined when no scopes are available", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "http://localhost:3000",
|
||||
authorization_endpoint: "http://localhost:3000/authorize",
|
||||
token_endpoint: "http://localhost:3000/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: [],
|
||||
});
|
||||
|
||||
const scopes = await discoverScopes("http://localhost:3000");
|
||||
|
||||
expect(scopes).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined when discovery fails", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockRejectedValue(
|
||||
new Error("Discovery failed"),
|
||||
);
|
||||
|
||||
const scopes = await discoverScopes("http://localhost:3000");
|
||||
|
||||
expect(scopes).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined when metadata is undefined", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue(undefined);
|
||||
|
||||
const scopes = await discoverScopes("http://localhost:3000");
|
||||
|
||||
expect(scopes).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should use OAuth metadata scopes when resource has scopes_supported undefined", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "http://localhost:3000",
|
||||
authorization_endpoint: "http://localhost:3000/authorize",
|
||||
token_endpoint: "http://localhost:3000/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: "http://localhost:3000",
|
||||
authorization_servers: ["http://localhost:3000"],
|
||||
scopes_supported: undefined as unknown as string[],
|
||||
};
|
||||
|
||||
const scopes = await discoverScopes(
|
||||
"http://localhost:3000",
|
||||
resourceMetadata,
|
||||
);
|
||||
|
||||
expect(scopes).toBe("read write");
|
||||
});
|
||||
|
||||
it("should return single scope when only one scope is supported", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "http://localhost:3000",
|
||||
authorization_endpoint: "http://localhost:3000/authorize",
|
||||
token_endpoint: "http://localhost:3000/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["openid"],
|
||||
});
|
||||
|
||||
const scopes = await discoverScopes("http://localhost:3000");
|
||||
|
||||
expect(scopes).toBe("openid");
|
||||
});
|
||||
|
||||
it("should pass fetchFn to discoverAuthorizationServerMetadata when provided", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
const mockFetchFn = vi.fn();
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "http://localhost:3000",
|
||||
authorization_endpoint: "http://localhost:3000/authorize",
|
||||
token_endpoint: "http://localhost:3000/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
await discoverScopes("http://localhost:3000", undefined, mockFetchFn);
|
||||
|
||||
expect(discoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||
new URL("/", "http://localhost:3000"),
|
||||
{ fetchFn: mockFetchFn },
|
||||
);
|
||||
});
|
||||
|
||||
it("should use authorization_servers URL from resource metadata for discovery (different domain)", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "https://auth-server.com",
|
||||
authorization_endpoint: "https://auth-server.com/authorize",
|
||||
token_endpoint: "https://auth-server.com/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: "https://mcp-server.com",
|
||||
authorization_servers: ["https://auth-server.com/"],
|
||||
scopes_supported: ["read", "write"],
|
||||
};
|
||||
|
||||
const scopes = await discoverScopes(
|
||||
"https://mcp-server.com",
|
||||
resourceMetadata,
|
||||
);
|
||||
|
||||
expect(scopes).toBe("read write");
|
||||
expect(discoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||
new URL("https://auth-server.com/"),
|
||||
{ fetchFn: undefined },
|
||||
);
|
||||
});
|
||||
|
||||
it("should preserve full path in authorization_servers URL", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "https://auth-server.com/realms/my-realm",
|
||||
authorization_endpoint:
|
||||
"https://auth-server.com/realms/my-realm/authorize",
|
||||
token_endpoint: "https://auth-server.com/realms/my-realm/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: "https://mcp-server.com",
|
||||
authorization_servers: ["https://auth-server.com/realms/my-realm/"],
|
||||
scopes_supported: ["read", "write"],
|
||||
};
|
||||
|
||||
const scopes = await discoverScopes(
|
||||
"https://mcp-server.com",
|
||||
resourceMetadata,
|
||||
);
|
||||
|
||||
expect(scopes).toBe("read write");
|
||||
expect(discoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||
new URL("https://auth-server.com/realms/my-realm/"),
|
||||
{ fetchFn: undefined },
|
||||
);
|
||||
});
|
||||
|
||||
it("should fall back to serverUrl when authorization_servers is empty", async () => {
|
||||
const { discoverAuthorizationServerMetadata } =
|
||||
await import("@modelcontextprotocol/sdk/client/auth.js");
|
||||
vi.mocked(discoverAuthorizationServerMetadata).mockResolvedValue({
|
||||
issuer: "https://mcp-server.com",
|
||||
authorization_endpoint: "https://mcp-server.com/authorize",
|
||||
token_endpoint: "https://mcp-server.com/token",
|
||||
response_types_supported: ["code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: "https://mcp-server.com",
|
||||
authorization_servers: [],
|
||||
scopes_supported: ["read", "write"],
|
||||
};
|
||||
|
||||
const scopes = await discoverScopes(
|
||||
"https://mcp-server.com",
|
||||
resourceMetadata,
|
||||
);
|
||||
|
||||
expect(scopes).toBe("read write");
|
||||
expect(discoverAuthorizationServerMetadata).toHaveBeenCalledWith(
|
||||
new URL("/", "https://mcp-server.com"),
|
||||
{ fetchFn: undefined },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuthorizationServerUrl", () => {
|
||||
const serverUrl = "https://mcp.example.com";
|
||||
|
||||
it("returns server URL when resourceMetadata is null", () => {
|
||||
expect(getAuthorizationServerUrl(serverUrl, null)).toEqual(
|
||||
new URL("/", serverUrl),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns server URL when resourceMetadata is undefined", () => {
|
||||
expect(getAuthorizationServerUrl(serverUrl)).toEqual(
|
||||
new URL("/", serverUrl),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns server URL when authorization_servers is empty array", () => {
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: serverUrl,
|
||||
authorization_servers: [],
|
||||
};
|
||||
expect(getAuthorizationServerUrl(serverUrl, resourceMetadata)).toEqual(
|
||||
new URL("/", serverUrl),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to server URL when authorization_servers[0] is empty string", () => {
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: serverUrl,
|
||||
authorization_servers: [""],
|
||||
};
|
||||
expect(getAuthorizationServerUrl(serverUrl, resourceMetadata)).toEqual(
|
||||
new URL("/", serverUrl),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns authorization_servers[0] when present and truthy", () => {
|
||||
const authUrl = "https://auth.example.com/";
|
||||
const resourceMetadata: OAuthProtectedResourceMetadata = {
|
||||
resource: serverUrl,
|
||||
authorization_servers: [authUrl],
|
||||
};
|
||||
expect(getAuthorizationServerUrl(serverUrl, resourceMetadata)).toEqual(
|
||||
new URL(authUrl),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue