const request = require("supertest"); const { app, server } = require("../../server"); const User = require("../../models/User"); const sseService = require("../../services/sseService"); const jwt = require("jsonwebtoken"); const couchdbService = require("../../services/couchdbService"); describe("SSE Routes", () => { let token; let userId; beforeAll(async () => { await couchdbService.initialize(); }); beforeEach(async () => { // Create a test user with unique email to avoid conflicts const timestamp = Date.now(); const user = await User.create({ name: "SSE Test User", username: `sseuser${timestamp}`, email: `sse${timestamp}@test.com`, password: "Password123!", }); userId = user._id; // Generate token const payload = { user: { id: user._id } }; token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h" }); // Clear SSE service state sseService.clients.clear(); sseService.topics.clear(); }); afterAll(async () => { await couchdbService.shutdown(); server.close(); }); describe("POST /api/sse/subscribe", () => { test("should require authentication", async () => { const res = await request(app) .post("/api/sse/subscribe") .send({ topics: ["test"] }); expect(res.statusCode).toBe(401); expect(res.body.success).toBe(false); }); test("should subscribe to topics with valid token", async () => { // First, add client to SSE service const mockRes = { write: jest.fn() }; sseService.addClient(userId, mockRes); const res = await request(app) .post("/api/sse/subscribe") .set("x-auth-token", token) .send({ topics: ["events", "posts"] }); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); expect(res.body.topics).toEqual(["events", "posts"]); // Verify subscription in service const stats = sseService.getStats(); expect(stats.topics).toHaveProperty("events"); expect(stats.topics).toHaveProperty("posts"); }); test("should fail if user not connected to SSE stream", async () => { const res = await request(app) .post("/api/sse/subscribe") .set("x-auth-token", token) .send({ topics: ["events"] }); expect(res.statusCode).toBe(400); expect(res.body.success).toBe(false); }); test("should validate topics array", async () => { const res = await request(app) .post("/api/sse/subscribe") .set("x-auth-token", token) .send({ topics: "not-an-array" }); expect(res.statusCode).toBe(400); expect(res.body.success).toBe(false); }); }); describe("POST /api/sse/unsubscribe", () => { test("should require authentication", async () => { const res = await request(app) .post("/api/sse/unsubscribe") .send({ topics: ["test"] }); expect(res.statusCode).toBe(401); expect(res.body.success).toBe(false); }); test("should unsubscribe from topics", async () => { // Setup: Add client and subscribe const mockRes = { write: jest.fn() }; sseService.addClient(userId, mockRes); sseService.subscribe(userId, ["events", "posts"]); // Unsubscribe from one topic const res = await request(app) .post("/api/sse/unsubscribe") .set("x-auth-token", token) .send({ topics: ["events"] }); expect(res.statusCode).toBe(200); expect(res.body.success).toBe(true); // Verify unsubscription const stats = sseService.getStats(); expect(stats.topics).not.toHaveProperty("events"); expect(stats.topics).toHaveProperty("posts"); }); test("should validate topics array", async () => { const res = await request(app) .post("/api/sse/unsubscribe") .set("x-auth-token", token) .send({ topics: null }); expect(res.statusCode).toBe(400); expect(res.body.success).toBe(false); }); }); describe("SSE Service", () => { test("should add and remove clients", () => { const mockRes = { write: jest.fn() }; sseService.addClient(userId, mockRes); let stats = sseService.getStats(); expect(stats.totalClients).toBe(1); sseService.removeClient(userId); stats = sseService.getStats(); expect(stats.totalClients).toBe(0); }); test("should broadcast to all clients", () => { const mockRes1 = { write: jest.fn() }; const mockRes2 = { write: jest.fn() }; sseService.addClient("user1", mockRes1); sseService.addClient("user2", mockRes2); sseService.broadcast("testEvent", { message: "Hello" }); expect(mockRes1.write).toHaveBeenCalledWith( 'event: testEvent\ndata: {"message":"Hello"}\n\n' ); expect(mockRes2.write).toHaveBeenCalledWith( 'event: testEvent\ndata: {"message":"Hello"}\n\n' ); }); test("should broadcast to topic subscribers only", () => { const mockRes1 = { write: jest.fn() }; const mockRes2 = { write: jest.fn() }; sseService.addClient("user1", mockRes1); sseService.addClient("user2", mockRes2); sseService.subscribe("user1", ["events"]); sseService.broadcastToTopic("events", "eventUpdate", { id: 1 }); expect(mockRes1.write).toHaveBeenCalled(); expect(mockRes2.write).not.toHaveBeenCalled(); }); test("should send to specific user", () => { const mockRes = { write: jest.fn() }; sseService.addClient(userId, mockRes); const success = sseService.sendToUser(userId, "notification", { text: "Test" }); expect(success).toBe(true); expect(mockRes.write).toHaveBeenCalledWith( 'event: notification\ndata: {"text":"Test"}\n\n' ); }); test("should return false when sending to non-existent user", () => { const success = sseService.sendToUser("nonexistent", "test", {}); expect(success).toBe(false); }); test("should get accurate stats", () => { const mockRes1 = { write: jest.fn() }; const mockRes2 = { write: jest.fn() }; sseService.addClient("user1", mockRes1); sseService.addClient("user2", mockRes2); sseService.subscribe("user1", ["events", "posts"]); sseService.subscribe("user2", ["events"]); const stats = sseService.getStats(); expect(stats.totalClients).toBe(2); expect(stats.totalTopics).toBe(2); expect(stats.topics.events).toBe(2); expect(stats.topics.posts).toBe(1); }); test("should clean up topics when last subscriber leaves", () => { const mockRes = { write: jest.fn() }; sseService.addClient(userId, mockRes); sseService.subscribe(userId, ["events"]); let stats = sseService.getStats(); expect(stats.totalTopics).toBe(1); sseService.removeClient(userId); stats = sseService.getStats(); expect(stats.totalTopics).toBe(0); }); }); });