import { FlinkApp, FlinkContext, FlinkAuthPlugin, FlinkRequest } from "@flink-app/flink"; import { EventEmitter } from "events"; import { streamingPlugin, StreamHandler, StreamingRouteProps } from "../src"; interface TestContext extends FlinkContext { repos: {}; plugins: {}; } // Mock auth plugin class MockAuthPlugin implements FlinkAuthPlugin { public shouldAuthenticate: boolean = true; async authenticateRequest(req: FlinkRequest, permissions: string | string[]): Promise { // Simulate authentication check const token = req.headers?.authorization; if (!token) { return false; } if (token === "Bearer valid-token") { // Set user info on request (req as any).user = { userId: "test-user", name: "Test User" }; return this.shouldAuthenticate; } return false; } createToken(userId: string): Promise { // Mock token creation return Promise.resolve(`mock-token-${userId}`); } } // Mock response object class MockResponse extends EventEmitter { public headers: { [key: string]: string } = {}; public statusCode?: number; public data: string[] = []; public jsonData: any[] = []; public ended = false; setHeader(key: string, value: string) { this.headers[key] = value; } status(code: number) { this.statusCode = code; return this; } json(data: any) { this.jsonData.push(data); return this; } flushHeaders() { // No-op for mock } write(chunk: string) { this.data.push(chunk); } end() { this.ended = true; this.emit("close"); } } // Mock request object class MockRequest { public query: { [key: string]: any } = {}; public params: { [key: string]: any } = {}; public body: any = {}; public headers: { [key: string]: any } = {}; on(event: string, callback: () => void) { // No-op for basic tests } } describe("StreamingPlugin", () => { let app: FlinkApp; let plugin: ReturnType; beforeEach(async () => { plugin = streamingPlugin({ debug: false }); app = new FlinkApp({ name: "Test App", port: 3999, plugins: [plugin], disableHttpServer: false, }); await app.start(); }); afterEach(async () => { await app.stop(); }); describe("SSE Format", () => { it("should stream data in SSE format", (done) => { const route: StreamingRouteProps = { path: "/test-sse", format: "sse", skipAutoRegister: true, }; const handler: StreamHandler = async ({ stream }) => { stream.write({ message: "Hello" }); stream.write({ message: "World" }); stream.end(); }; plugin.registerStreamHandler(handler, route); // Simulate request const req = new MockRequest(); const res = new MockResponse(); // Find the registered route and call it const expressApp = app.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } // Make request to the endpoint expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-sse") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.headers["Content-Type"]).toBe("text/event-stream"); expect(res.headers["Cache-Control"]).toBe("no-cache"); expect(res.headers["Connection"]).toBe("keep-alive"); expect(res.data.length).toBe(2); expect(res.data[0]).toContain('data: {"message":"Hello"}'); expect(res.data[1]).toContain('data: {"message":"World"}'); expect(res.ended).toBe(true); done(); }, 100); }); it("should send error events in SSE format", (done) => { const route: StreamingRouteProps = { path: "/test-sse-error", format: "sse", skipAutoRegister: true, }; const handler: StreamHandler = async ({ stream }) => { stream.error(new Error("Test error")); stream.end(); }; plugin.registerStreamHandler(handler, route); const req = new MockRequest(); const res = new MockResponse(); const expressApp = app.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-sse-error") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.data[0]).toContain("event: error"); expect(res.data[0]).toContain("Test error"); done(); }, 100); }); }); describe("NDJSON Format", () => { it("should stream data in NDJSON format", (done) => { const route: StreamingRouteProps = { path: "/test-ndjson", format: "ndjson", skipAutoRegister: true, }; const handler: StreamHandler = async ({ stream }) => { stream.write({ delta: "Hello" }); stream.write({ delta: "World", done: true }); stream.end(); }; plugin.registerStreamHandler(handler, route); const req = new MockRequest(); const res = new MockResponse(); const expressApp = app.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-ndjson") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.headers["Content-Type"]).toBe("application/x-ndjson"); expect(res.data.length).toBe(2); expect(res.data[0]).toBe('{"delta":"Hello"}\n'); expect(res.data[1]).toBe('{"delta":"World","done":true}\n'); expect(res.ended).toBe(true); done(); }, 100); }); }); describe("Stream Writer", () => { it("should detect closed connections", (done) => { const route: StreamingRouteProps = { path: "/test-closed", format: "sse", skipAutoRegister: true, }; const handler: StreamHandler = async ({ stream }) => { expect(stream.isOpen()).toBe(true); stream.end(); // After end, isOpen should be false (handled by plugin) }; plugin.registerStreamHandler(handler, route); const req = new MockRequest(); const res = new MockResponse(); const expressApp = app.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-closed") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.ended).toBe(true); done(); }, 100); }); }); describe("Authentication", () => { let appWithAuth: FlinkApp; let pluginWithAuth: ReturnType; let mockAuth: MockAuthPlugin; beforeEach(async () => { mockAuth = new MockAuthPlugin(); pluginWithAuth = streamingPlugin({ debug: false }); appWithAuth = new FlinkApp({ name: "Test App With Auth", port: 4000, plugins: [pluginWithAuth], auth: mockAuth, disableHttpServer: false, }); await appWithAuth.start(); }); afterEach(async () => { await appWithAuth.stop(); }); it("should allow authenticated requests with valid token", (done) => { const route: StreamingRouteProps = { path: "/test-auth-success", format: "sse", skipAutoRegister: true, permissions: ["admin"], }; const handler: StreamHandler = async ({ stream, req }) => { const user = (req as any).user; stream.write({ message: `Hello ${user?.name}` }); stream.end(); }; pluginWithAuth.registerStreamHandler(handler, route); const req = new MockRequest(); req.headers.authorization = "Bearer valid-token"; const res = new MockResponse(); const expressApp = appWithAuth.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-auth-success") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.statusCode).not.toBe(401); expect(res.data.length).toBe(1); expect(res.data[0]).toContain("Hello Test User"); expect(res.ended).toBe(true); done(); }, 100); }); it("should reject unauthenticated requests without token", (done) => { const route: StreamingRouteProps = { path: "/test-auth-fail-no-token", format: "sse", skipAutoRegister: true, permissions: ["admin"], }; const handler: StreamHandler = async ({ stream }) => { stream.write({ message: "Should not see this" }); stream.end(); }; pluginWithAuth.registerStreamHandler(handler, route); const req = new MockRequest(); // No authorization header const res = new MockResponse(); const expressApp = appWithAuth.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-auth-fail-no-token") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.statusCode).toBe(401); expect(res.jsonData.length).toBe(1); expect(res.jsonData[0].error.title).toBe("Unauthorized"); expect(res.data.length).toBe(0); // Should not stream any data done(); }, 100); }); it("should reject requests with invalid token", (done) => { const route: StreamingRouteProps = { path: "/test-auth-fail-invalid-token", format: "sse", skipAutoRegister: true, permissions: ["admin"], }; const handler: StreamHandler = async ({ stream }) => { stream.write({ message: "Should not see this" }); stream.end(); }; pluginWithAuth.registerStreamHandler(handler, route); const req = new MockRequest(); req.headers.authorization = "Bearer invalid-token"; const res = new MockResponse(); const expressApp = appWithAuth.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-auth-fail-invalid-token") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.statusCode).toBe(401); expect(res.jsonData.length).toBe(1); expect(res.jsonData[0].error.title).toBe("Unauthorized"); expect(res.data.length).toBe(0); done(); }, 100); }); it("should allow streaming without permissions when no auth is required", (done) => { const route: StreamingRouteProps = { path: "/test-no-auth-required", format: "sse", skipAutoRegister: true, // No permissions specified }; const handler: StreamHandler = async ({ stream }) => { stream.write({ message: "Public endpoint" }); stream.end(); }; pluginWithAuth.registerStreamHandler(handler, route); const req = new MockRequest(); // No authorization header, but endpoint doesn't require it const res = new MockResponse(); const expressApp = appWithAuth.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-no-auth-required") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.statusCode).not.toBe(401); expect(res.data.length).toBe(1); expect(res.data[0]).toContain("Public endpoint"); expect(res.ended).toBe(true); done(); }, 100); }); it("should provide access to req.user populated by auth plugin", (done) => { const route: StreamingRouteProps = { path: "/test-req-user-access", format: "ndjson", skipAutoRegister: true, permissions: ["admin"], }; const handler: StreamHandler = async ({ stream, req }) => { // Access req.user that was set by auth plugin const user = (req as any).user; // Verify user object exists and has expected properties expect(user).toBeDefined(); expect(user.userId).toBe("test-user"); expect(user.name).toBe("Test User"); // Stream user info stream.write({ userId: user.userId, userName: user.name, message: "User info from req.user" }); stream.end(); }; pluginWithAuth.registerStreamHandler(handler, route); const req = new MockRequest(); req.headers.authorization = "Bearer valid-token"; const res = new MockResponse(); const expressApp = appWithAuth.expressApp; if (!expressApp) { fail("Express app not initialized"); return; } expressApp._router.stack.forEach((layer: any) => { if (layer.route?.path === "/test-req-user-access") { layer.route.stack[0].handle(req, res, () => {}); } }); setTimeout(() => { expect(res.statusCode).not.toBe(401); expect(res.data.length).toBe(1); // Parse the NDJSON line const data = JSON.parse(res.data[0].trim()); expect(data.userId).toBe("test-user"); expect(data.userName).toBe("Test User"); expect(data.message).toBe("User info from req.user"); expect(res.ended).toBe(true); done(); }, 100); }); }); });