/** * Critical Integration Tests for OAuth Plugin * * These tests cover the 10 most critical end-to-end OAuth workflows * focusing on JWT integration, error handling, and core functionality. * * Tests invoke handlers directly with mocked context to validate * OAuth flow logic independently of plugin registration. */ import { Db, MongoClient } from "mongodb"; import CallbackOAuth from "../src/handlers/CallbackOAuth"; import InitiateOAuth from "../src/handlers/InitiateOAuth"; import { GitHubProvider } from "../src/providers/GitHubProvider"; import { GoogleProvider } from "../src/providers/GoogleProvider"; import OAuthConnectionRepo from "../src/repos/OAuthConnectionRepo"; import OAuthSessionRepo from "../src/repos/OAuthSessionRepo"; import { createFailingMockJwtAuthPlugin, createMockJwtAuthPlugin, decodeTestToken } from "./helpers/mockJwtAuthPlugin"; import { createMockGitHubProfile, createMockGoogleProfile } from "./helpers/mockOAuthProviders"; import { extractTokenFromUrl } from "./helpers/testHelpers"; describe("OAuth Plugin - Critical Integration Tests", () => { let client: MongoClient; let db: Db; let sessionRepo: OAuthSessionRepo; let connectionRepo: OAuthConnectionRepo; let mockJwtAuth: any; const TEST_SECRET = "test-secret-that-is-at-least-32-characters-long-for-testing"; const GITHUB_CONFIG = { clientId: "test-github-client", clientSecret: TEST_SECRET, callbackUrl: "https://app.test/oauth/github/callback", }; const GOOGLE_CONFIG = { clientId: "test-google-client", clientSecret: TEST_SECRET, callbackUrl: "https://app.test/oauth/google/callback", }; beforeAll(async () => { const mongoUrl = process.env.TEST_MONGO_URL || "mongodb://localhost:27017"; client = new MongoClient(mongoUrl); await client.connect(); db = client.db("oauth_integration_test"); sessionRepo = new OAuthSessionRepo("oauth_sessions", db, client); connectionRepo = new OAuthConnectionRepo("oauth_connections", db, client); }); beforeEach(async () => { await db.collection("oauth_sessions").deleteMany({}); await db.collection("oauth_connections").deleteMany({}); mockJwtAuth = createMockJwtAuthPlugin(TEST_SECRET); }); afterAll(async () => { await db.dropDatabase(); await client.close(); }); /** * Test 1: Complete GitHub OAuth flow with JWT token generation * Critical: Validates end-to-end flow from initiate to JWT return */ it("should complete full GitHub OAuth flow with JWT token generation", async () => { // Setup mocked context const mockUser = { _id: "user123", email: "test@example.com", name: "Test User" }; let onAuthSuccessCalledWithContext = false; const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, storeTokens: false, onAuthSuccess: async ({ profile, provider }: any, ctx: any) => { onAuthSuccessCalledWithContext = !!ctx; const token = await mockJwtAuth.createToken({ userId: mockUser._id, email: profile.email }, ["user"]); return { user: mockUser, token, redirectUrl: "https://app.test/dashboard", }; }, }, }, }, }; // Step 1: Initiate OAuth flow const initiateReq: any = { params: { provider: "github" }, query: {}, }; const initiateResponse = await InitiateOAuth({ ctx: mockCtx, req: initiateReq } as any); expect(initiateResponse.status).toBe(302); expect(initiateResponse.headers?.Location).toContain("github.com/login/oauth/authorize"); expect(initiateResponse.headers?.Location).toContain("client_id=test-github-client"); expect(initiateResponse.headers?.Location).toContain("state="); // Extract state from redirect URL const stateMatch = initiateResponse.headers?.Location.match(/state=([^&]+)/); const state = stateMatch ? stateMatch[1] : ""; expect(state).toBeTruthy(); expect(state.length).toBe(64); // 32 bytes hex encoded // Verify session created const sessions = await db.collection("oauth_sessions").find({}).toArray(); expect(sessions.length).toBe(1); expect(sessions[0].state).toBe(state); expect(sessions[0].provider).toBe("github"); // Step 2: Mock provider responses const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "gho_test_token", token_type: "bearer", scope: "user:email", }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile().raw); } return Promise.reject(new Error("Unknown URL")); }); // Spy on getProvider to return our mocked provider const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); // Step 3: Handle callback const callbackReq: any = { params: { provider: "github" }, query: { code: "test_auth_code", state: state, }, }; const callbackResponse = await CallbackOAuth({ ctx: mockCtx, req: callbackReq } as any); // Step 4: Verify JWT token is returned expect(callbackResponse.status).toBe(302); expect(callbackResponse.headers?.Location).toContain("app.test/dashboard"); expect(callbackResponse.headers?.Location).toContain("token="); const token = extractTokenFromUrl(callbackResponse.headers?.Location || ""); expect(token).toBeTruthy(); // Verify JWT token contents const decoded = decodeTestToken(token!, TEST_SECRET); expect(decoded.userId).toBe("user123"); expect(decoded.email).toBe("testuser@example.com"); expect(decoded.roles).toContain("user"); // Verify context was passed to callback expect(onAuthSuccessCalledWithContext).toBe(true); // Verify session was deleted const sessionsAfter = await db.collection("oauth_sessions").find({}).toArray(); expect(sessionsAfter.length).toBe(0); }); /** * Test 2: Complete Google OAuth flow with JWT token generation * Critical: Validates Google provider integration */ it("should complete full Google OAuth flow with JWT token generation", async () => { const mockUser = { _id: "user456", email: "test@gmail.com", name: "Google User" }; const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { google: GOOGLE_CONFIG }, storeTokens: false, onAuthSuccess: async ({ profile }: any) => { const token = await mockJwtAuth.createToken({ userId: mockUser._id, email: profile.email }, ["user"]); return { user: mockUser, token, redirectUrl: "https://app.test/dashboard", }; }, }, }, }, }; // Initiate OAuth const initiateResponse = await InitiateOAuth({ ctx: mockCtx, req: { params: { provider: "google" }, query: {} } as any, } as any); const stateMatch = initiateResponse.headers?.Location.match(/state=([^&]+)/); const state = stateMatch ? stateMatch[1] : ""; // Mock Google provider const googleProvider = new GoogleProvider(GOOGLE_CONFIG); spyOn(googleProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("oauth2.googleapis.com/token")) { return Promise.resolve({ access_token: "ya29_test_token", expires_in: 3600, token_type: "Bearer", scope: "openid email profile", }); } if (url.includes("oauth2/v2/userinfo")) { return Promise.resolve(createMockGoogleProfile().raw); } return Promise.reject(new Error("Unknown URL")); }); const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(googleProvider); // Handle callback const callbackResponse = await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "google" }, query: { code: "google_auth_code", state }, } as any, } as any); // Verify response expect(callbackResponse.status).toBe(302); const token = extractTokenFromUrl(callbackResponse.headers?.Location || ""); expect(token).toBeTruthy(); const decoded = decodeTestToken(token!, TEST_SECRET); expect(decoded.userId).toBe("user456"); expect(decoded.roles).toContain("user"); }); /** * Test 3: Response type JSON handling * Critical: Validates API client integration */ it("should return JSON response when response_type=json", async () => { const mockUser = { _id: "user789", email: "api@example.com" }; const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, onAuthSuccess: async ({ profile }: any) => { const token = await mockJwtAuth.createToken({ userId: mockUser._id }, ["user"]); return { user: mockUser, token }; }, }, }, }, }; // Create session const session = await sessionRepo.create({ sessionId: "session-json-test", state: "test-state-json", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); // Mock provider const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "token", scope: "user:email" }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile({ email: "api@example.com" }).raw); } }); const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); // Callback with response_type=json const response = await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "github" }, query: { code: "auth_code", state: "test-state-json", response_type: "json", }, } as any, } as any); // Verify JSON response expect(response.status).toBe(200); expect(response.data).toBeDefined(); expect(response.data.user).toBeDefined(); expect(response.data.token).toBeDefined(); // Verify JWT token const decoded = decodeTestToken(response.data.token, TEST_SECRET); expect(decoded.userId).toBe("user789"); }); /** * Test 4: JWT token validation in response * Critical: Ensures JWT contains correct claims */ it("should generate valid JWT token with user data and roles", async () => { const mockUser = { _id: "user-jwt-test", email: "jwt@example.com", name: "JWT Test" }; const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, onAuthSuccess: async ({ profile }: any) => { const token = await mockJwtAuth.createToken( { userId: mockUser._id, email: profile.email, name: profile.name, }, ["user", "verified"] ); return { user: mockUser, token }; }, }, }, }, }; // Create session await sessionRepo.create({ sessionId: "jwt-validation-test", state: "jwt-test-state", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); // Mock provider const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "token", scope: "user:email" }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile({ email: "jwt@example.com" }).raw); } }); const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); // Handle callback const response = await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "github" }, query: { code: "code", state: "jwt-test-state", response_type: "json", }, } as any, } as any); // Decode JWT and validate claims const token = response.data.token; const decoded = decodeTestToken(token, TEST_SECRET); expect(decoded.userId).toBe("user-jwt-test"); expect(decoded.email).toBe("testuser@example.com"); expect(decoded.roles).toEqual(["user", "verified"]); expect(decoded.iat).toBeDefined(); expect(decoded.exp).toBeDefined(); expect(decoded.exp).toBeGreaterThan(decoded.iat); }); /** * Test 5: JWT generation failure handling * Critical: Validates error handling when JWT fails */ it("should handle JWT generation failure gracefully", async () => { const failingJwtAuth = createFailingMockJwtAuthPlugin(); let onAuthErrorCalled = false; let errorCode: string | undefined; const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, onAuthSuccess: async () => { // This will throw const token = await failingJwtAuth.createToken({}, []); return { user: {}, token }; }, onAuthError: async ({ error }: any) => { onAuthErrorCalled = true; errorCode = error.code; return { redirectUrl: "https://app.test/error" }; }, }, }, }, }; // Create session await sessionRepo.create({ sessionId: "jwt-fail-test", state: "jwt-fail-state", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); // Mock provider const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "token", scope: "user:email" }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile().raw); } }); const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); // Handle callback const response = await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "github" }, query: { code: "code", state: "jwt-fail-state" }, } as any, } as any); // Verify error handling expect(onAuthErrorCalled).toBe(true); expect(errorCode).toBe("jwt_generation_failed"); expect(response.status).toBe(302); expect(response.headers?.Location).toBe("https://app.test/error"); }); /** * Test 6: Invalid state parameter handling * Critical: Validates CSRF protection */ it("should reject callback with invalid state parameter", async () => { const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, onAuthSuccess: async () => ({ user: {}, token: "should-not-reach", }), }, }, }, }; // Create session with one state await sessionRepo.create({ sessionId: "csrf-test", state: "valid-state-123", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); // Attempt callback with different state (CSRF attack) const response = await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "github" }, query: { code: "code", state: "invalid-state-456", // Different state }, } as any, } as any); // Verify request is rejected with 400 status expect(response.status).toBe(400); // The error message should be in the response // Note: The actual error structure may vary based on Flink's badRequest implementation // For now, just verify we got a 400 status which indicates proper CSRF protection // Verify original session still exists (not deleted) const sessions = await db.collection("oauth_sessions").find({}).toArray(); expect(sessions.length).toBe(1); }); /** * Test 7: Session cleanup after successful callback * Critical: Ensures sessions are deleted after use */ it("should delete session after successful callback", async () => { const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, onAuthSuccess: async () => ({ user: { _id: "user" }, token: await mockJwtAuth.createToken({ userId: "user" }, ["user"]), }), }, }, }, }; // Create session await sessionRepo.create({ sessionId: "cleanup-test", state: "cleanup-state", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); // Verify session exists let sessions = await db.collection("oauth_sessions").find({}).toArray(); expect(sessions.length).toBe(1); // Mock provider const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "token", scope: "user:email" }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile().raw); } }); const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); // Handle callback await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "github" }, query: { code: "code", state: "cleanup-state" }, } as any, } as any); // Verify session is deleted sessions = await db.collection("oauth_sessions").find({}).toArray(); expect(sessions.length).toBe(0); }); /** * Test 8: Multiple provider linking * Critical: Validates linking GitHub and Google to same user */ it("should link multiple providers to same user", async () => { const mockUser = { _id: "multi-provider-user", email: "multi@example.com" }; const createMockCtx = (provider: "github" | "google") => ({ repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { [provider]: provider === "github" ? GITHUB_CONFIG : GOOGLE_CONFIG, }, storeTokens: true, onAuthSuccess: async ({ profile }: any) => ({ user: mockUser, token: await mockJwtAuth.createToken({ userId: mockUser._id }, ["user"]), }), }, }, }, }); // Link GitHub await sessionRepo.create({ sessionId: "github-session", state: "github-state", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "gh_token", scope: "user:email" }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile({ email: "multi@example.com" }).raw); } }); let ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); await CallbackOAuth({ ctx: createMockCtx("github"), req: { params: { provider: "github" }, query: { code: "gh_code", state: "github-state" }, } as any, } as any); // Verify GitHub connection stored let connections = await db.collection("oauth_connections").find({}).toArray(); expect(connections.length).toBe(1); expect(connections[0].provider).toBe("github"); expect(connections[0].userId).toBe("multi-provider-user"); // Link Google await sessionRepo.create({ sessionId: "google-session", state: "google-state", provider: "google", redirectUri: "https://app.test/callback", createdAt: new Date(), }); const googleProvider = new GoogleProvider(GOOGLE_CONFIG); spyOn(googleProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("oauth2.googleapis.com/token")) { return Promise.resolve({ access_token: "g_token", scope: "openid email", expires_in: 3600 }); } if (url.includes("oauth2/v2/userinfo")) { return Promise.resolve(createMockGoogleProfile({ email: "multi@example.com" }).raw); } }); // Note: Cannot reset spy on already-spied method in same test // Instead, update the existing spy's return value (ProviderRegistry.getProvider as jasmine.Spy).and.returnValue(googleProvider); await CallbackOAuth({ ctx: createMockCtx("google"), req: { params: { provider: "google" }, query: { code: "g_code", state: "google-state" }, } as any, } as any); // Verify both connections exist connections = await db.collection("oauth_connections").find({}).toArray(); expect(connections.length).toBe(2); expect(connections.map((c) => c.provider).sort()).toEqual(["github", "google"]); expect(connections.every((c) => c.userId === "multi-provider-user")).toBe(true); }); /** * Test 9: Token storage enabled scenario * Critical: Validates OAuth tokens are encrypted and stored */ it("should store and encrypt OAuth tokens when storeTokens=true", async () => { const mockUser = { _id: "token-storage-user", email: "storage@example.com" }; const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, storeTokens: true, onAuthSuccess: async () => ({ user: mockUser, token: await mockJwtAuth.createToken({ userId: mockUser._id }, ["user"]), }), }, }, }, }; await sessionRepo.create({ sessionId: "storage-session", state: "storage-state", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "gho_real_access_token", refresh_token: "gho_refresh_token", scope: "user:email", expires_in: 3600, }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile({ email: "storage@example.com" }).raw); } }); const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "github" }, query: { code: "code", state: "storage-state" }, } as any, } as any); // Verify connection stored const connections = await db.collection("oauth_connections").find({}).toArray(); expect(connections.length).toBe(1); const connection = connections[0]; expect(connection.userId).toBe("token-storage-user"); expect(connection.provider).toBe("github"); expect(connection.providerId).toBe("12345678"); expect(connection.accessToken).toBeDefined(); expect(connection.refreshToken).toBeDefined(); // Verify tokens are encrypted (should not be plain text) expect(connection.accessToken).not.toBe("gho_real_access_token"); expect(connection.refreshToken).not.toBe("gho_refresh_token"); expect(connection.accessToken).toContain(":"); // Encryption format uses colons }); /** * Test 10: Context passing to onAuthSuccess callback * Critical: Validates context is properly passed for repository access */ it("should pass Flink context to onAuthSuccess callback", async () => { let contextReceived: any = null; let reposAvailable = false; let pluginsAvailable = false; const mockCtx: any = { repos: { oauthSessionRepo: sessionRepo, oauthConnectionRepo: connectionRepo, userRepo: { create: jasmine.createSpy("create") }, }, plugins: { oauth: { options: { providers: { github: GITHUB_CONFIG }, onAuthSuccess: async ({ profile }: any, ctx: any) => { contextReceived = ctx; reposAvailable = !!ctx.repos; pluginsAvailable = !!ctx.plugins; // Test that we can access repos if (ctx.repos.userRepo) { await ctx.repos.userRepo.create({ email: profile.email, name: profile.name, }); } const token = await mockJwtAuth.createToken({ userId: "test" }, ["user"]); return { user: { _id: "test", email: profile.email }, token, }; }, }, }, }, }; await sessionRepo.create({ sessionId: "context-session", state: "context-state", provider: "github", redirectUri: "https://app.test/callback", createdAt: new Date(), }); const githubProvider = new GitHubProvider(GITHUB_CONFIG); spyOn(githubProvider, "makeHttpRequest").and.callFake((url: string) => { if (url.includes("access_token")) { return Promise.resolve({ access_token: "token", scope: "user:email" }); } if (url.includes("/user")) { return Promise.resolve(createMockGitHubProfile().raw); } }); const ProviderRegistry = require("../src/providers/ProviderRegistry"); spyOn(ProviderRegistry, "getProvider").and.returnValue(githubProvider); await CallbackOAuth({ ctx: mockCtx, req: { params: { provider: "github" }, query: { code: "code", state: "context-state" }, } as any, } as any); // Verify context was passed expect(contextReceived).toBeTruthy(); expect(reposAvailable).toBe(true); expect(pluginsAvailable).toBe(true); // Verify repository was called expect(mockCtx.repos.userRepo.create).toHaveBeenCalled(); }); });