import { FlinkApp, FlinkAuthPlugin, FlinkRequest } from "@flink-app/flink"; import { genericRequestPlugin, HttpMethod } from "../src/index"; describe("GenericRequestPlugin", () => { let mockApp: any; let mockExpressApp: any; let mockAuth: FlinkAuthPlugin; let registeredRoutes: Map; beforeEach(() => { registeredRoutes = new Map(); // Mock Express app mockExpressApp = { get: jasmine.createSpy("get").and.callFake((path: string, handler: Function) => { registeredRoutes.set(`GET:${path}`, handler); }), post: jasmine.createSpy("post").and.callFake((path: string, handler: Function) => { registeredRoutes.set(`POST:${path}`, handler); }), put: jasmine.createSpy("put").and.callFake((path: string, handler: Function) => { registeredRoutes.set(`PUT:${path}`, handler); }), delete: jasmine.createSpy("delete").and.callFake((path: string, handler: Function) => { registeredRoutes.set(`DELETE:${path}`, handler); }), }; // Mock auth plugin mockAuth = { authenticateRequest: jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(true)), createToken: jasmine.createSpy("createToken").and.returnValue(Promise.resolve("token")), }; // Mock FlinkApp mockApp = { expressApp: mockExpressApp, auth: mockAuth, } as unknown as FlinkApp; }); describe("Plugin Registration", () => { it("should create a valid plugin", () => { const plugin = genericRequestPlugin({ path: "/test", method: HttpMethod.get, handler: () => {}, }); expect(plugin).toBeDefined(); expect(plugin.id).toBe("genericRequestPlugin"); expect(plugin.init).toBeDefined(); }); it("should throw error if Express app is not initialized", () => { const appWithoutExpress = { expressApp: null } as unknown as FlinkApp; const plugin = genericRequestPlugin({ path: "/test", method: HttpMethod.get, handler: () => {}, }); expect(() => plugin.init(appWithoutExpress)).toThrowError("Express app not initialized"); }); it("should register GET route", () => { const plugin = genericRequestPlugin({ path: "/test", method: HttpMethod.get, handler: () => {}, }); plugin.init(mockApp); expect(mockExpressApp.get).toHaveBeenCalledWith("/test", jasmine.any(Function)); }); it("should register POST route", () => { const plugin = genericRequestPlugin({ path: "/webhook", method: HttpMethod.post, handler: () => {}, }); plugin.init(mockApp); expect(mockExpressApp.post).toHaveBeenCalledWith("/webhook", jasmine.any(Function)); }); it("should register PUT route", () => { const plugin = genericRequestPlugin({ path: "/update", method: HttpMethod.put, handler: () => {}, }); plugin.init(mockApp); expect(mockExpressApp.put).toHaveBeenCalledWith("/update", jasmine.any(Function)); }); it("should register DELETE route", () => { const plugin = genericRequestPlugin({ path: "/remove", method: HttpMethod.delete, handler: () => {}, }); plugin.init(mockApp); expect(mockExpressApp.delete).toHaveBeenCalledWith("/remove", jasmine.any(Function)); }); }); describe("Handler Execution - No Permissions", () => { it("should call handler directly when no permissions are set", async () => { const handlerSpy = jasmine.createSpy("handler"); const plugin = genericRequestPlugin({ path: "/public", method: HttpMethod.get, handler: handlerSpy, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("GET:/public"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as any; const mockRes = {} as any; await routeHandler(mockReq, mockRes); expect(handlerSpy).toHaveBeenCalledWith(mockReq, mockRes, mockApp); expect(mockAuth.authenticateRequest).not.toHaveBeenCalled(); }); it("should pass req, res, and app to handler", async () => { let capturedReq: any; let capturedRes: any; let capturedApp: any; const handler = (req: any, res: any, app: FlinkApp) => { capturedReq = req; capturedRes = res; capturedApp = app; }; const plugin = genericRequestPlugin({ path: "/test", method: HttpMethod.get, handler, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("GET:/test"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = { path: "/test" } as any; const mockRes = { status: () => ({ json: () => {} }) } as any; await routeHandler(mockReq, mockRes); expect(capturedReq).toBe(mockReq); expect(capturedRes).toBe(mockRes); expect(capturedApp).toBe(mockApp); }); }); describe("Permission Validation", () => { it("should validate permissions when set", async () => { const handlerSpy = jasmine.createSpy("handler"); const plugin = genericRequestPlugin({ path: "/protected", method: HttpMethod.get, permissions: "read", handler: handlerSpy, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("GET:/protected"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as FlinkRequest; const mockRes = {} as any; await routeHandler(mockReq, mockRes); expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read"); expect(handlerSpy).toHaveBeenCalled(); }); it("should validate multiple permissions", async () => { const handlerSpy = jasmine.createSpy("handler"); const plugin = genericRequestPlugin({ path: "/admin", method: HttpMethod.post, permissions: ["read", "write", "admin"], handler: handlerSpy, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("POST:/admin"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as FlinkRequest; const mockRes = {} as any; await routeHandler(mockReq, mockRes); expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, ["read", "write", "admin"]); expect(handlerSpy).toHaveBeenCalled(); }); it("should return 401 when authentication fails", async () => { mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(false)); const handlerSpy = jasmine.createSpy("handler"); const plugin = genericRequestPlugin({ path: "/protected", method: HttpMethod.get, permissions: "read", handler: handlerSpy, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("GET:/protected"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as FlinkRequest; const mockJsonSpy = jasmine.createSpy("json"); const mockRes = { status: jasmine.createSpy("status").and.returnValue({ json: mockJsonSpy }), } as any; await routeHandler(mockReq, mockRes); expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read"); expect(mockRes.status).toHaveBeenCalledWith(401); expect(mockJsonSpy).toHaveBeenCalledWith({ status: 401, error: { title: "Unauthorized", detail: "Authentication required or insufficient permissions", }, }); expect(handlerSpy).not.toHaveBeenCalled(); }); it("should not call handler when authentication fails", async () => { mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(false)); const handlerSpy = jasmine.createSpy("handler"); const plugin = genericRequestPlugin({ path: "/protected", method: HttpMethod.post, permissions: "write", handler: handlerSpy, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("POST:/protected"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as FlinkRequest; const mockRes = { status: () => ({ json: () => {} }), } as any; await routeHandler(mockReq, mockRes); expect(handlerSpy).not.toHaveBeenCalled(); }); it("should throw error if permissions are set but no auth plugin is configured", async () => { const appWithoutAuth = { expressApp: mockExpressApp, auth: null, } as unknown as FlinkApp; const plugin = genericRequestPlugin({ path: "/protected", method: HttpMethod.get, permissions: "read", handler: () => {}, }); plugin.init(appWithoutAuth); const routeHandler = registeredRoutes.get("GET:/protected"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as FlinkRequest; const mockRes = {} as any; await expectAsync(routeHandler(mockReq, mockRes)).toBeRejectedWithError( "Route GET /protected requires permissions but no auth plugin is configured" ); }); it("should call handler after successful authentication", async () => { mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.callFake((req: FlinkRequest) => { req.user = { id: "123", username: "testuser" }; return Promise.resolve(true); }); const handlerSpy = jasmine.createSpy("handler"); const plugin = genericRequestPlugin({ path: "/protected", method: HttpMethod.get, permissions: "read", handler: handlerSpy, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("GET:/protected"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as FlinkRequest; const mockRes = {} as any; await routeHandler(mockReq, mockRes); expect(mockAuth.authenticateRequest).toHaveBeenCalled(); expect(mockReq.user).toEqual({ id: "123", username: "testuser" }); expect(handlerSpy).toHaveBeenCalledWith(mockReq, mockRes, mockApp); }); }); describe("Wildcard Permission", () => { it("should validate wildcard permission for any authenticated user", async () => { const handlerSpy = jasmine.createSpy("handler"); const plugin = genericRequestPlugin({ path: "/authenticated", method: HttpMethod.get, permissions: "*", handler: handlerSpy, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("GET:/authenticated"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = {} as FlinkRequest; const mockRes = {} as any; await routeHandler(mockReq, mockRes); expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "*"); expect(handlerSpy).toHaveBeenCalled(); }); }); describe("Real-world Scenarios", () => { it("should handle webhook with permissions", async () => { let webhookData: any; const webhookHandler = (req: any, res: any) => { webhookData = req.body; res.json({ received: true }); }; const plugin = genericRequestPlugin({ path: "/webhook/stripe", method: HttpMethod.post, permissions: "webhook:stripe", handler: webhookHandler, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("POST:/webhook/stripe"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = { body: { event: "payment.success" } } as any; const mockJsonSpy = jasmine.createSpy("json"); const mockRes = { json: mockJsonSpy } as any; await routeHandler(mockReq, mockRes); expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "webhook:stripe"); expect(webhookData).toEqual({ event: "payment.success" }); expect(mockJsonSpy).toHaveBeenCalledWith({ received: true }); }); it("should handle file download with permissions", async () => { const fileHandler = (req: any, res: any) => { res.setHeader("Content-Type", "application/pdf"); res.end("file-data"); }; const plugin = genericRequestPlugin({ path: "/download/:fileId", method: HttpMethod.get, permissions: "file:download", handler: fileHandler, }); plugin.init(mockApp); const routeHandler = registeredRoutes.get("GET:/download/:fileId"); if (!routeHandler) { fail("Route handler not registered"); return; } const mockReq = { params: { fileId: "123" } } as any; const mockSetHeaderSpy = jasmine.createSpy("setHeader"); const mockEndSpy = jasmine.createSpy("end"); const mockRes = { setHeader: mockSetHeaderSpy, end: mockEndSpy, } as any; await routeHandler(mockReq, mockRes); expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "file:download"); expect(mockSetHeaderSpy).toHaveBeenCalledWith("Content-Type", "application/pdf"); expect(mockEndSpy).toHaveBeenCalledWith("file-data"); }); }); });