/**
* postgres-mcp - OAuth Errors Tests
*
* Tests for OAuth error classes, inheritance, error codes, and HTTP status codes.
*/
import { describe, it, expect } from "vitest";
import {
OAuthError,
TokenMissingError,
InvalidTokenError,
TokenExpiredError,
InvalidSignatureError,
InsufficientScopeError,
AuthServerDiscoveryError,
JwksFetchError,
ClientRegistrationError,
} from "../errors.js";
describe("OAuth Errors", () => {
describe("OAuthError (base class)", () => {
it("should create an error with message, code, and default status", () => {
const error = new OAuthError("Test error", "TEST_CODE");
expect(error.message).toBe("Test error");
expect(error.code).toBe("TEST_CODE");
expect(error.httpStatus).toBe(401);
expect(error.name).toBe("OAuthError");
});
it("should accept custom HTTP status", () => {
const error = new OAuthError("Server error", "SERVER_ERROR", 500);
expect(error.httpStatus).toBe(500);
});
it("should be an instance of Error", () => {
const error = new OAuthError("Test", "TEST");
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(OAuthError);
});
});
describe("TokenMissingError", () => {
it("should use default message", () => {
const error = new TokenMissingError();
expect(error.message).toBe("No bearer token provided");
expect(error.code).toBe("TOKEN_MISSING");
expect(error.httpStatus).toBe(401);
expect(error.name).toBe("TokenMissingError");
});
it("should accept custom message", () => {
const error = new TokenMissingError("Authorization header required");
expect(error.message).toBe("Authorization header required");
});
it("should inherit from OAuthError", () => {
const error = new TokenMissingError();
expect(error).toBeInstanceOf(OAuthError);
expect(error).toBeInstanceOf(TokenMissingError);
});
});
describe("InvalidTokenError", () => {
it("should use default message", () => {
const error = new InvalidTokenError();
expect(error.message).toBe("Invalid access token");
expect(error.code).toBe("INVALID_TOKEN");
expect(error.httpStatus).toBe(401);
expect(error.name).toBe("InvalidTokenError");
});
it("should accept custom message", () => {
const error = new InvalidTokenError(
"Token signature verification failed",
);
expect(error.message).toBe("Token signature verification failed");
});
});
describe("TokenExpiredError", () => {
it("should use default message", () => {
const error = new TokenExpiredError();
expect(error.message).toBe("Access token has expired");
expect(error.code).toBe("TOKEN_EXPIRED");
expect(error.httpStatus).toBe(401);
expect(error.name).toBe("TokenExpiredError");
});
});
describe("InvalidSignatureError", () => {
it("should use default message", () => {
const error = new InvalidSignatureError();
expect(error.message).toBe("Invalid token signature");
expect(error.code).toBe("INVALID_SIGNATURE");
expect(error.httpStatus).toBe(401);
expect(error.name).toBe("InvalidSignatureError");
});
});
describe("InsufficientScopeError", () => {
it("should include required scopes in message", () => {
const error = new InsufficientScopeError(["read", "write"]);
expect(error.message).toBe("Insufficient scope. Required: read, write");
expect(error.code).toBe("INSUFFICIENT_SCOPE");
expect(error.httpStatus).toBe(403); // 403 Forbidden, not 401
expect(error.requiredScopes).toEqual(["read", "write"]);
expect(error.name).toBe("InsufficientScopeError");
});
it("should accept custom message", () => {
const error = new InsufficientScopeError(
["admin"],
"Admin access required",
);
expect(error.message).toBe("Admin access required");
expect(error.requiredScopes).toEqual(["admin"]);
});
it("should handle single scope", () => {
const error = new InsufficientScopeError(["read"]);
expect(error.message).toBe("Insufficient scope. Required: read");
expect(error.requiredScopes).toEqual(["read"]);
});
it("should handle empty scopes array", () => {
const error = new InsufficientScopeError([]);
expect(error.message).toBe("Insufficient scope. Required: ");
expect(error.requiredScopes).toEqual([]);
});
});
describe("AuthServerDiscoveryError", () => {
it("should use default message", () => {
const error = new AuthServerDiscoveryError();
expect(error.message).toBe(
"Failed to discover authorization server metadata",
);
expect(error.code).toBe("DISCOVERY_FAILED");
expect(error.httpStatus).toBe(500); // Server error
expect(error.name).toBe("AuthServerDiscoveryError");
});
it("should accept custom message with server details", () => {
const error = new AuthServerDiscoveryError(
"Connection refused to http://localhost:8080",
);
expect(error.message).toBe("Connection refused to http://localhost:8080");
});
});
describe("JwksFetchError", () => {
it("should use default message", () => {
const error = new JwksFetchError();
expect(error.message).toBe("Failed to fetch JWKS");
expect(error.code).toBe("JWKS_FETCH_FAILED");
expect(error.httpStatus).toBe(500);
expect(error.name).toBe("JwksFetchError");
});
it("should accept custom message with URI", () => {
const error = new JwksFetchError(
"Failed to fetch JWKS from https://auth.example.com/jwks",
);
expect(error.message).toBe(
"Failed to fetch JWKS from https://auth.example.com/jwks",
);
});
});
describe("ClientRegistrationError", () => {
it("should use default message", () => {
const error = new ClientRegistrationError();
expect(error.message).toBe("Client registration failed");
expect(error.code).toBe("REGISTRATION_FAILED");
expect(error.httpStatus).toBe(400); // Bad Request
expect(error.name).toBe("ClientRegistrationError");
});
});
describe("Error inheritance chain", () => {
it("all errors should be catchable as OAuthError", () => {
const errors = [
new TokenMissingError(),
new InvalidTokenError(),
new TokenExpiredError(),
new InvalidSignatureError(),
new InsufficientScopeError(["test"]),
new AuthServerDiscoveryError(),
new JwksFetchError(),
new ClientRegistrationError(),
];
for (const error of errors) {
expect(error).toBeInstanceOf(OAuthError);
expect(error).toBeInstanceOf(Error);
}
});
it("all errors should have stack trace", () => {
const error = new TokenMissingError();
expect(error.stack).toBeDefined();
expect(error.stack).toContain("TokenMissingError");
});
});
});