test: switch integration project to folder-based manifest (#1314) (#1318)

* 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:
Cliff Hall 2026-05-18 11:54:49 -04:00 • committed by GitHub
commit 8bb389c8e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,317 @@
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),
);
});
});