/**
 * Tests for OIDC Single Sign-On
 */
import { describe, it, expect, beforeEach, vi } from "vitest";
import { OIDCClient, SCIMClient, createOIDCClient, createSCIMClient, } from "./oidc.js";
// Mock fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe("OIDCClient", () => {
    let config;
    let client;
    beforeEach(() => {
        mockFetch.mockReset();
        config = {
            provider: "okta",
            clientId: "test-client-id",
            clientSecret: "test-client-secret",
            issuerUrl: "https://company.okta.com",
            redirectUri: "https://app.example.com/callback",
        };
        client = new OIDCClient(config);
    });
    describe("constructor", () => {
        it("creates client with provided config", () => {
            expect(client).toBeInstanceOf(OIDCClient);
        });
        it("merges provider-specific defaults", () => {
            const oktaClient = new OIDCClient({ ...config, provider: "okta" });
            expect(oktaClient).toBeInstanceOf(OIDCClient);
        });
        it("uses default scopes when not provided", () => {
            const clientWithoutScopes = new OIDCClient({
                ...config,
                scopes: undefined,
            });
            expect(clientWithoutScopes).toBeInstanceOf(OIDCClient);
        });
        it("supports custom provider", () => {
            const customClient = new OIDCClient({
                ...config,
                provider: "custom",
                endpoints: {
                    authorization: "https://custom.auth.com/authorize",
                    token: "https://custom.auth.com/token",
                },
            });
            expect(customClient).toBeInstanceOf(OIDCClient);
        });
    });
    describe("discover", () => {
        it("fetches OIDC discovery document", async () => {
            const discoveryDoc = {
                authorization_endpoint: "https://company.okta.com/authorize",
                token_endpoint: "https://company.okta.com/token",
                userinfo_endpoint: "https://company.okta.com/userinfo",
                jwks_uri: "https://company.okta.com/jwks",
            };
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => discoveryDoc,
            });
            const result = await client.discover();
            expect(mockFetch).toHaveBeenCalledWith("https://company.okta.com/.well-known/openid-configuration");
            expect(result).toEqual(discoveryDoc);
        });
        it("caches discovery document", async () => {
            const discoveryDoc = {
                authorization_endpoint: "https://company.okta.com/authorize",
            };
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => discoveryDoc,
            });
            await client.discover();
            await client.discover();
            // Should only fetch once
            expect(mockFetch).toHaveBeenCalledTimes(1);
        });
        it("throws on discovery failure", async () => {
            mockFetch.mockResolvedValueOnce({
                ok: false,
                status: 404,
                statusText: "Not Found",
            });
            await expect(client.discover()).rejects.toThrow("OIDC discovery failed: 404 Not Found");
        });
    });
    describe("getAuthorizationUrl", () => {
        beforeEach(() => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: async () => ({
                    authorization_endpoint: "https://company.okta.com/authorize",
                }),
            });
        });
        it("generates authorization URL with PKCE", async () => {
            const { url, state } = await client.getAuthorizationUrl();
            expect(url).toContain("https://company.okta.com/authorize");
            expect(url).toContain("response_type=code");
            expect(url).toContain(`client_id=${config.clientId}`);
            expect(url).toContain("code_challenge=");
            expect(url).toContain("code_challenge_method=S256");
            expect(state.codeVerifier).toBeDefined();
            expect(state.state).toBeDefined();
            expect(state.nonce).toBeDefined();
        });
        it("includes redirect URI", async () => {
            const { url } = await client.getAuthorizationUrl();
            expect(url).toContain(encodeURIComponent(config.redirectUri));
        });
        it("includes scopes", async () => {
            const { url } = await client.getAuthorizationUrl();
            expect(url).toContain("scope=");
            expect(url).toContain("openid");
        });
    });
    describe("exchangeCode", () => {
        // Helper to create ID token with specific nonce
        const createMockIdToken = (nonce) => {
            const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
            const payload = Buffer.from(JSON.stringify({
                sub: "user123",
                email: "user@example.com",
                name: "Test User",
                nonce,
                iss: "https://company.okta.com",
                aud: "test-client-id",
                exp: 9999999999,
            })).toString("base64url");
            return `${header}.${payload}.SIGNATURE`;
        };
        beforeEach(() => {
            // First call for discovery
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    authorization_endpoint: "https://company.okta.com/authorize",
                    token_endpoint: "https://company.okta.com/token",
                }),
            });
        });
        it("exchanges code for tokens", async () => {
            // Get auth URL first to store state
            const { state } = await client.getAuthorizationUrl();
            // Create ID token with the actual nonce from auth state
            const mockIdToken = createMockIdToken(state.nonce);
            // Mock token exchange
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    access_token: "access-token-123",
                    id_token: mockIdToken,
                    token_type: "Bearer",
                    expires_in: 3600,
                }),
            });
            const result = await client.exchangeCode("auth-code-123", state.state);
            expect(result.tokens.accessToken).toBe("access-token-123");
            expect(result.tokens.tokenType).toBe("Bearer");
            expect(result.user.email).toBe("user@example.com");
        });
        it("throws on invalid state", async () => {
            await expect(client.exchangeCode("code", "invalid-state")).rejects.toThrow("Invalid or expired authorization state");
        });
        it("throws on expired state", async () => {
            const { state } = await client.getAuthorizationUrl();
            // Manually expire the state
            state.expiresAt = Date.now() - 1000;
            // Need to update the stored state - this is a bit of a hack
            // In real tests, we'd mock the time
            await expect(client.exchangeCode("code", state.state)).rejects.toThrow();
        });
    });
    describe("getUserInfo", () => {
        it("parses ID token claims", async () => {
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwibmFtZSI6IlRlc3QgVXNlciIsImdpdmVuX25hbWUiOiJUZXN0IiwiZmFtaWx5X25hbWUiOiJVc2VyIn0.SIGNATURE";
            const tokens = {
                accessToken: "access",
                idToken: mockIdToken,
                tokenType: "Bearer",
                expiresIn: 3600,
            };
            const user = await client.getUserInfo(tokens);
            expect(user.id).toBe("user123");
            expect(user.email).toBe("user@example.com");
            expect(user.name).toBe("Test User");
            expect(user.givenName).toBe("Test");
            expect(user.familyName).toBe("User");
        });
        it("maps roles from claims", async () => {
            const clientWithRoleMapping = new OIDCClient({
                ...config,
                roleMapping: {
                    rolesClaim: "groups",
                    mappings: {
                        "security-admins": "admin",
                        auditors: "auditor",
                    },
                    defaultRole: "viewer",
                },
            });
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZ3JvdXBzIjpbInNlY3VyaXR5LWFkbWlucyJdfQ.SIGNATURE";
            const tokens = {
                accessToken: "access",
                idToken: mockIdToken,
                tokenType: "Bearer",
                expiresIn: 3600,
            };
            const user = await clientWithRoleMapping.getUserInfo(tokens);
            expect(user.role).toBe("admin");
        });
        it("uses default role when no mapping matches", async () => {
            const clientWithRoleMapping = new OIDCClient({
                ...config,
                roleMapping: {
                    rolesClaim: "groups",
                    mappings: {},
                    defaultRole: "viewer",
                },
            });
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZ3JvdXBzIjpbInVua25vd24tZ3JvdXAiXX0.SIGNATURE";
            const tokens = {
                accessToken: "access",
                idToken: mockIdToken,
                tokenType: "Bearer",
                expiresIn: 3600,
            };
            const user = await clientWithRoleMapping.getUserInfo(tokens);
            expect(user.role).toBe("viewer");
        });
        it("validates nonce when provided", async () => {
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwibm9uY2UiOiJ3cm9uZy1ub25jZSJ9.SIGNATURE";
            const tokens = {
                accessToken: "access",
                idToken: mockIdToken,
                tokenType: "Bearer",
                expiresIn: 3600,
            };
            await expect(client.getUserInfo(tokens, "expected-nonce")).rejects.toThrow("ID token nonce mismatch");
        });
    });
    describe("refreshTokens", () => {
        beforeEach(() => {
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    token_endpoint: "https://company.okta.com/token",
                }),
            });
        });
        it("refreshes tokens successfully", async () => {
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => ({
                    access_token: "new-access-token",
                    id_token: "new-id-token",
                    token_type: "Bearer",
                    expires_in: 3600,
                }),
            });
            const tokens = await client.refreshTokens("refresh-token-123");
            expect(tokens.accessToken).toBe("new-access-token");
        });
        it("throws on refresh failure", async () => {
            // The beforeEach already sets up the discovery mock
            // This mock is for the actual token refresh call
            mockFetch.mockResolvedValueOnce({
                ok: false,
            });
            await expect(client.refreshTokens("invalid-refresh")).rejects.toThrow("Token refresh failed");
        });
    });
    describe("validateIdToken", () => {
        it("validates token issuer and audience", async () => {
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiaXNzIjoiaHR0cHM6Ly9jb21wYW55Lm9rdGEuY29tIiwiYXVkIjoidGVzdC1jbGllbnQtaWQiLCJleHAiOjk5OTk5OTk5OTl9.SIGNATURE";
            const valid = await client.validateIdToken(mockIdToken);
            expect(valid).toBe(true);
        });
        it("rejects token with wrong issuer", async () => {
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiaXNzIjoiaHR0cHM6Ly93cm9uZy5jb20iLCJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsImV4cCI6OTk5OTk5OTk5OX0.SIGNATURE";
            const valid = await client.validateIdToken(mockIdToken);
            expect(valid).toBe(false);
        });
        it("rejects token with wrong audience", async () => {
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiaXNzIjoiaHR0cHM6Ly9jb21wYW55Lm9rdGEuY29tIiwiYXVkIjoid3JvbmctY2xpZW50IiwiZXhwIjo5OTk5OTk5OTk5fQ.SIGNATURE";
            const valid = await client.validateIdToken(mockIdToken);
            expect(valid).toBe(false);
        });
        it("rejects expired token", async () => {
            const mockIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiaXNzIjoiaHR0cHM6Ly9jb21wYW55Lm9rdGEuY29tIiwiYXVkIjoidGVzdC1jbGllbnQtaWQiLCJleHAiOjF9.SIGNATURE";
            const valid = await client.validateIdToken(mockIdToken);
            expect(valid).toBe(false);
        });
    });
});
describe("SCIMClient", () => {
    let client;
    beforeEach(() => {
        mockFetch.mockReset();
        client = new SCIMClient("https://scim.example.com", "bearer-token");
    });
    describe("getUsers", () => {
        it("fetches all users", async () => {
            const mockUsers = [
                { id: "1", userName: "user1", active: true, schemas: [] },
                { id: "2", userName: "user2", active: true, schemas: [] },
            ];
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => ({ Resources: mockUsers }),
            });
            const users = await client.getUsers();
            expect(users).toHaveLength(2);
            expect(mockFetch).toHaveBeenCalledWith("https://scim.example.com/Users", expect.objectContaining({
                method: "GET",
                headers: expect.objectContaining({
                    Authorization: "Bearer bearer-token",
                }),
            }));
        });
        it("returns empty array when no resources", async () => {
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => ({}),
            });
            const users = await client.getUsers();
            expect(users).toEqual([]);
        });
    });
    describe("getUser", () => {
        it("fetches user by ID", async () => {
            const mockUser = { id: "123", userName: "test", active: true, schemas: [] };
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => mockUser,
            });
            const user = await client.getUser("123");
            expect(user).toEqual(mockUser);
            expect(mockFetch).toHaveBeenCalledWith("https://scim.example.com/Users/123", expect.any(Object));
        });
        it("returns null for non-existent user", async () => {
            mockFetch.mockRejectedValueOnce(new Error("Not found"));
            const user = await client.getUser("nonexistent");
            expect(user).toBeNull();
        });
    });
    describe("createUser", () => {
        it("creates new user", async () => {
            const newUser = {
                schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
                userName: "newuser",
                active: true,
            };
            const createdUser = { ...newUser, id: "new-id" };
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => createdUser,
            });
            const result = await client.createUser(newUser);
            expect(result.id).toBe("new-id");
            expect(mockFetch).toHaveBeenCalledWith("https://scim.example.com/Users", expect.objectContaining({
                method: "POST",
                body: JSON.stringify(newUser),
            }));
        });
    });
    describe("updateUser", () => {
        it("updates existing user", async () => {
            const updates = { active: false };
            const updatedUser = { id: "123", userName: "test", active: false, schemas: [] };
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => updatedUser,
            });
            const result = await client.updateUser("123", updates);
            expect(result.active).toBe(false);
            expect(mockFetch).toHaveBeenCalledWith("https://scim.example.com/Users/123", expect.objectContaining({
                method: "PATCH",
            }));
        });
    });
    describe("deleteUser", () => {
        it("deletes user", async () => {
            mockFetch.mockResolvedValueOnce({
                ok: true,
            });
            await client.deleteUser("123");
            expect(mockFetch).toHaveBeenCalledWith("https://scim.example.com/Users/123", expect.objectContaining({
                method: "DELETE",
            }));
        });
    });
    describe("getGroups", () => {
        it("fetches all groups", async () => {
            const mockGroups = [
                { id: "g1", displayName: "Admins", schemas: [] },
                { id: "g2", displayName: "Users", schemas: [] },
            ];
            mockFetch.mockResolvedValueOnce({
                ok: true,
                json: async () => ({ Resources: mockGroups }),
            });
            const groups = await client.getGroups();
            expect(groups).toHaveLength(2);
        });
    });
    describe("error handling", () => {
        it("throws on failed request", async () => {
            mockFetch.mockReset();
            mockFetch.mockResolvedValueOnce({
                ok: false,
                status: 401,
            });
            await expect(client.getUsers()).rejects.toThrow("SCIM request failed: 401");
        });
    });
});
describe("Factory Functions", () => {
    it("createOIDCClient creates OIDCClient instance", () => {
        const client = createOIDCClient({
            provider: "okta",
            clientId: "id",
            clientSecret: "secret",
            issuerUrl: "https://example.okta.com",
            redirectUri: "https://app.com/callback",
        });
        expect(client).toBeInstanceOf(OIDCClient);
    });
    it("createSCIMClient creates SCIMClient instance", () => {
        const client = createSCIMClient("https://scim.example.com", "token");
        expect(client).toBeInstanceOf(SCIMClient);
    });
});
//# sourceMappingURL=oidc.test.js.map