/**
* mysql-mcp - Middleware Unit Tests
*
* Tests for OAuth middleware utilities.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
extractBearerToken,
formatOAuthError,
validateAuth,
createAuthContext,
requireScope,
requireAnyScope,
requireToolScope,
type AuthenticatedContext,
} from "../middleware.js";
import {
TokenMissingError,
InvalidTokenError,
InsufficientScopeError,
} from "../errors.js";
import type { TokenValidator } from "../TokenValidator.js";
// Create mock token validator
function createMockTokenValidator(
overrides: Partial<TokenValidator> = {},
): TokenValidator {
return {
validate: vi.fn().mockResolvedValue({
valid: true,
claims: { sub: "user1", scopes: ["read", "write"], exp: 0, iat: 0 },
}),
invalidateCache: vi.fn(),
...overrides,
} as unknown as TokenValidator;
}
describe("extractBearerToken", () => {
it("should extract token from valid Bearer header", () => {
expect(extractBearerToken("Bearer abc123")).toBe("abc123");
});
it("should return null for undefined header", () => {
expect(extractBearerToken(undefined)).toBeNull();
});
it("should return null for empty header", () => {
expect(extractBearerToken("")).toBeNull();
});
it("should return null for non-Bearer auth", () => {
expect(extractBearerToken("Basic abc123")).toBeNull();
});
it("should return null for malformed Bearer header", () => {
expect(extractBearerToken("Bearer")).toBeNull();
});
it("should return empty string for Bearer with trailing space only", () => {
// Note: 'Bearer ' (with trailing space) splits to ['bearer', ''] which returns ''
expect(extractBearerToken("Bearer ")).toBe("");
});
it("should be case-insensitive for Bearer keyword", () => {
expect(extractBearerToken("bearer abc123")).toBe("abc123");
expect(extractBearerToken("BEARER abc123")).toBe("abc123");
});
});
describe("formatOAuthError", () => {
it("should format TokenMissingError as 401", () => {
const result = formatOAuthError(new TokenMissingError());
expect(result.status).toBe(401);
expect(result.body).toHaveProperty("error", "invalid_token");
});
it("should format InvalidTokenError as 401", () => {
const result = formatOAuthError(new InvalidTokenError("Token expired"));
expect(result.status).toBe(401);
expect(result.body).toHaveProperty("error", "invalid_token");
});
it("should format InsufficientScopeError as 403", () => {
const result = formatOAuthError(
new InsufficientScopeError(["admin", "write"]),
);
expect(result.status).toBe(403);
expect(result.body).toHaveProperty("error", "insufficient_scope");
expect(result.body).toHaveProperty("scope", "admin write");
});
it("should format unknown errors as 500", () => {
const result = formatOAuthError(new Error("Something went wrong"));
expect(result.status).toBe(500);
expect(result.body).toHaveProperty("error", "server_error");
});
});
describe("createAuthContext", () => {
let mockValidator: TokenValidator;
beforeEach(() => {
mockValidator = createMockTokenValidator();
});
it("should return unauthenticated context when no token", async () => {
const context = await createAuthContext(undefined, mockValidator);
expect(context.authenticated).toBe(false);
expect(context.scopes).toEqual([]);
});
it("should return authenticated context for valid token", async () => {
const context = await createAuthContext("Bearer validtoken", mockValidator);
expect(context.authenticated).toBe(true);
expect(context.scopes).toEqual(["read", "write"]);
expect(context.claims).toBeDefined();
});
it("should return unauthenticated context for invalid token", async () => {
const invalidValidator = createMockTokenValidator({
validate: vi
.fn()
.mockResolvedValue({ valid: false, error: "Invalid token" }),
});
const context = await createAuthContext(
"Bearer invalidtoken",
invalidValidator,
);
expect(context.authenticated).toBe(false);
expect(context.scopes).toEqual([]);
});
});
describe("validateAuth", () => {
let mockValidator: TokenValidator;
beforeEach(() => {
mockValidator = createMockTokenValidator();
});
it("should throw TokenMissingError when no token and required", async () => {
await expect(
validateAuth(undefined, {
tokenValidator: mockValidator,
required: true,
}),
).rejects.toThrow(TokenMissingError);
});
it("should return unauthenticated context when no token and not required", async () => {
const context = await validateAuth(undefined, {
tokenValidator: mockValidator,
required: false,
});
expect(context.authenticated).toBe(false);
});
it("should throw InvalidTokenError for invalid token", async () => {
const invalidValidator = createMockTokenValidator({
validate: vi
.fn()
.mockResolvedValue({ valid: false, error: "Token expired" }),
});
await expect(
validateAuth("Bearer expired", {
tokenValidator: invalidValidator,
}),
).rejects.toThrow(InvalidTokenError);
});
it("should return authenticated context for valid token", async () => {
const context = await validateAuth("Bearer validtoken", {
tokenValidator: mockValidator,
});
expect(context.authenticated).toBe(true);
expect(context.scopes).toEqual(["read", "write"]);
});
it("should throw InsufficientScopeError when missing required scope", async () => {
await expect(
validateAuth("Bearer validtoken", {
tokenValidator: mockValidator,
requiredScopes: ["admin"], // User has read, write but not admin
}),
).rejects.toThrow(InsufficientScopeError);
});
it("should pass when user has any of required scopes", async () => {
const context = await validateAuth("Bearer validtoken", {
tokenValidator: mockValidator,
requiredScopes: ["read", "admin"], // User has read
});
expect(context.authenticated).toBe(true);
});
});
describe("requireScope", () => {
it("should throw TokenMissingError when not authenticated", () => {
const context: AuthenticatedContext = { authenticated: false, scopes: [] };
expect(() => requireScope(context, "read")).toThrow(TokenMissingError);
});
it("should throw InsufficientScopeError when scope missing", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["read"],
claims: { sub: "user1", scopes: ["read"], exp: 0, iat: 0 },
};
expect(() => requireScope(context, "admin")).toThrow(
InsufficientScopeError,
);
});
it("should pass when scope is present", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["read", "write"],
claims: { sub: "user1", scopes: ["read", "write"], exp: 0, iat: 0 },
};
expect(() => requireScope(context, "read")).not.toThrow();
});
});
describe("requireAnyScope", () => {
it("should throw TokenMissingError when not authenticated", () => {
const context: AuthenticatedContext = { authenticated: false, scopes: [] };
expect(() => requireAnyScope(context, ["read", "write"])).toThrow(
TokenMissingError,
);
});
it("should throw InsufficientScopeError when none of scopes present", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["read"],
claims: { sub: "user1", scopes: ["read"], exp: 0, iat: 0 },
};
expect(() => requireAnyScope(context, ["admin", "superadmin"])).toThrow(
InsufficientScopeError,
);
});
it("should pass when any scope present", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["read", "write"],
claims: { sub: "user1", scopes: ["read", "write"], exp: 0, iat: 0 },
};
expect(() => requireAnyScope(context, ["write", "admin"])).not.toThrow();
});
});
describe("requireToolScope", () => {
it("should throw TokenMissingError when not authenticated", () => {
const context: AuthenticatedContext = { authenticated: false, scopes: [] };
expect(() => requireToolScope(context, ["read"])).toThrow(
TokenMissingError,
);
});
it("should map read scope and pass", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["read"], // hasScope checks for 'read' directly
claims: { sub: "user1", scopes: ["read"], exp: 0, iat: 0 },
};
expect(() => requireToolScope(context, ["read"])).not.toThrow();
});
it("should map write scope and pass", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["write"],
claims: { sub: "user1", scopes: ["write"], exp: 0, iat: 0 },
};
expect(() => requireToolScope(context, ["write"])).not.toThrow();
});
it("should map admin scope and pass", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["admin"],
claims: { sub: "user1", scopes: ["admin"], exp: 0, iat: 0 },
};
expect(() => requireToolScope(context, ["admin"])).not.toThrow();
});
it("should throw InsufficientScopeError when mapped scope missing", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["read"],
claims: { sub: "user1", scopes: ["read"], exp: 0, iat: 0 },
};
expect(() => requireToolScope(context, ["admin"])).toThrow(
InsufficientScopeError,
);
});
it("should pass when user has admin scope and read is required", () => {
const context: AuthenticatedContext = {
authenticated: true,
scopes: ["admin"], // admin includes read and write scopes
claims: { sub: "user1", scopes: ["admin"], exp: 0, iat: 0 },
};
expect(() => requireToolScope(context, ["read"])).not.toThrow();
});
});