const request = require("supertest"); const { app } = require("../../server"); const couchdbService = require("../../services/couchdbService"); const jwt = require("jsonwebtoken"); // Mock couchdbService jest.mock("../../services/couchdbService"); describe("Analytics API", () => { let authToken; let mockUser; beforeAll(() => { // Create a mock user and token mockUser = { _id: "user_123", type: "user", name: "Test User", email: "test@test.com", points: 100, isPremium: false, adoptedStreets: ["street_1"], completedTasks: ["task_1", "task_2"], posts: ["post_1"], events: ["event_1"], earnedBadges: [{ badgeId: "badge_1" }], stats: { streetsAdopted: 1, tasksCompleted: 2, postsCreated: 1, eventsParticipated: 1, badgesEarned: 1, }, }; authToken = jwt.sign({ id: mockUser._id }, process.env.JWT_SECRET || "test_secret"); // Mock couchdbService methods couchdbService.find = jest.fn(); couchdbService.findUserById = jest.fn(); couchdbService.getDocument = jest.fn(); }); afterEach(() => { jest.clearAllMocks(); }); describe("GET /api/analytics/overview", () => { it("should return overview statistics without auth token", async () => { const mockUsers = [ { type: "user", points: 100 }, { type: "user", points: 150 }, ]; const mockStreets = [ { type: "street", status: "adopted" }, { type: "street", status: "available" }, { type: "street", status: "adopted" }, ]; const mockTasks = [ { type: "task", status: "completed" }, { type: "task", status: "pending" }, ]; const mockEvents = [ { type: "event", status: "upcoming" }, { type: "event", status: "completed" }, ]; const mockPosts = [{ type: "post" }, { type: "post" }]; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "user") return mockUsers; if (query.selector.type === "street") return mockStreets; if (query.selector.type === "task") return mockTasks; if (query.selector.type === "event") return mockEvents; if (query.selector.type === "post") return mockPosts; return []; }); const res = await request(app) .get("/api/analytics/overview") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty("overview"); expect(res.body.overview).toHaveProperty("totalUsers", 2); expect(res.body.overview).toHaveProperty("totalStreets", 3); expect(res.body.overview).toHaveProperty("adoptedStreets", 2); expect(res.body.overview).toHaveProperty("totalTasks", 2); expect(res.body.overview).toHaveProperty("completedTasks", 1); expect(res.body.overview).toHaveProperty("totalEvents", 2); expect(res.body.overview).toHaveProperty("totalPosts", 2); expect(res.body.overview).toHaveProperty("totalPoints", 250); expect(res.body.overview).toHaveProperty("averagePointsPerUser", 125); }); it("should filter by timeframe", async () => { const now = new Date(); const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const mockUsers = [{ type: "user", points: 100 }]; const mockStreets = [{ type: "street", status: "adopted" }]; const mockTasks = [ { type: "task", status: "completed", createdAt: now.toISOString() }, ]; const mockEvents = [ { type: "event", status: "upcoming", createdAt: now.toISOString() }, ]; const mockPosts = [{ type: "post", createdAt: now.toISOString() }]; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "user") return mockUsers; if (query.selector.type === "street") return mockStreets; if (query.selector.type === "task") return mockTasks; if (query.selector.type === "event") return mockEvents; if (query.selector.type === "post") return mockPosts; return []; }); const res = await request(app) .get("/api/analytics/overview?timeframe=7d") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.timeframe).toBe("7d"); }); it("should require authentication", async () => { const res = await request(app).get("/api/analytics/overview"); expect(res.statusCode).toBe(401); }); }); describe("GET /api/analytics/user/:userId", () => { it("should return user-specific analytics", async () => { couchdbService.findUserById.mockResolvedValue(mockUser); couchdbService.getDocument.mockResolvedValue({ _id: "street_1", name: "Main Street", }); const mockTasks = [ { type: "task", completedBy: { userId: mockUser._id }, createdAt: new Date().toISOString() }, ]; const mockPosts = [ { type: "post", user: { userId: mockUser._id }, likesCount: 5, commentsCount: 3, createdAt: new Date().toISOString() }, ]; const mockEvents = [ { type: "event", participants: [{ userId: mockUser._id }], createdAt: new Date().toISOString() }, ]; const mockTransactions = [ { type: "point_transaction", user: { userId: mockUser._id }, amount: 50, createdAt: new Date().toISOString() }, { type: "point_transaction", user: { userId: mockUser._id }, amount: -20, createdAt: new Date().toISOString() }, ]; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "task") return mockTasks; if (query.selector.type === "post") return mockPosts; if (query.selector.type === "event") return mockEvents; if (query.selector.type === "point_transaction") return mockTransactions; return []; }); const res = await request(app) .get(`/api/analytics/user/${mockUser._id}`) .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty("user"); expect(res.body.user.name).toBe("Test User"); expect(res.body).toHaveProperty("stats"); expect(res.body.stats).toHaveProperty("streetsAdopted"); expect(res.body.stats).toHaveProperty("tasksCompleted"); expect(res.body.stats).toHaveProperty("pointsEarned", 50); expect(res.body.stats).toHaveProperty("pointsSpent", 20); expect(res.body.stats).toHaveProperty("totalLikesReceived", 5); expect(res.body.stats).toHaveProperty("totalCommentsReceived", 3); expect(res.body).toHaveProperty("recentActivity"); }); it("should return 404 for non-existent user", async () => { couchdbService.findUserById.mockResolvedValue(null); const res = await request(app) .get("/api/analytics/user/invalid_user_id") .set("x-auth-token", authToken); expect(res.statusCode).toBe(404); expect(res.body.msg).toBe("User not found"); }); it("should require authentication", async () => { const res = await request(app).get(`/api/analytics/user/${mockUser._id}`); expect(res.statusCode).toBe(401); }); }); describe("GET /api/analytics/activity", () => { it("should return activity data grouped by day", async () => { const today = new Date().toISOString(); const mockTasks = [{ type: "task", createdAt: today }]; const mockPosts = [{ type: "post", createdAt: today }]; const mockEvents = [{ type: "event", createdAt: today }]; const mockStreets = [{ type: "street", status: "adopted", createdAt: today }]; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "task") return mockTasks; if (query.selector.type === "post") return mockPosts; if (query.selector.type === "event") return mockEvents; if (query.selector.type === "street") return mockStreets; return []; }); const res = await request(app) .get("/api/analytics/activity?groupBy=day") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty("activity"); expect(res.body).toHaveProperty("groupBy", "day"); expect(res.body).toHaveProperty("summary"); expect(Array.isArray(res.body.activity)).toBe(true); }); it("should group activity by week", async () => { const today = new Date().toISOString(); const mockTasks = [{ type: "task", createdAt: today }]; const mockPosts = []; const mockEvents = []; const mockStreets = []; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "task") return mockTasks; if (query.selector.type === "post") return mockPosts; if (query.selector.type === "event") return mockEvents; if (query.selector.type === "street") return mockStreets; return []; }); const res = await request(app) .get("/api/analytics/activity?groupBy=week") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.groupBy).toBe("week"); }); it("should group activity by month", async () => { const today = new Date().toISOString(); const mockTasks = [{ type: "task", createdAt: today }]; const mockPosts = []; const mockEvents = []; const mockStreets = []; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "task") return mockTasks; if (query.selector.type === "post") return mockPosts; if (query.selector.type === "event") return mockEvents; if (query.selector.type === "street") return mockStreets; return []; }); const res = await request(app) .get("/api/analytics/activity?groupBy=month") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.groupBy).toBe("month"); }); it("should require authentication", async () => { const res = await request(app).get("/api/analytics/activity"); expect(res.statusCode).toBe(401); }); }); describe("GET /api/analytics/top-contributors", () => { it("should return top contributors by points", async () => { const mockUsers = [ { _id: "user_1", name: "User 1", email: "user1@test.com", points: 500, stats: { tasksCompleted: 10, postsCreated: 5, streetsAdopted: 2 }, earnedBadges: [], }, { _id: "user_2", name: "User 2", email: "user2@test.com", points: 300, stats: { tasksCompleted: 7, postsCreated: 3, streetsAdopted: 1 }, earnedBadges: [], }, ]; couchdbService.find.mockResolvedValue(mockUsers); const res = await request(app) .get("/api/analytics/top-contributors?limit=5") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty("contributors"); expect(res.body.contributors.length).toBeLessThanOrEqual(5); expect(res.body.metric).toBe("points"); // Verify sorted by score descending if (res.body.contributors.length > 1) { expect(res.body.contributors[0].score).toBeGreaterThanOrEqual( res.body.contributors[1].score ); } }); it("should return top contributors by tasks", async () => { const mockUsers = [ { _id: "user_1", name: "User 1", email: "user1@test.com", points: 500, stats: { tasksCompleted: 10, postsCreated: 5, streetsAdopted: 2 }, earnedBadges: [], }, ]; couchdbService.find.mockResolvedValue(mockUsers); const res = await request(app) .get("/api/analytics/top-contributors?metric=tasks") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.metric).toBe("tasks"); }); it("should return top contributors by posts", async () => { const mockUsers = [ { _id: "user_1", name: "User 1", email: "user1@test.com", points: 500, stats: { tasksCompleted: 10, postsCreated: 5, streetsAdopted: 2 }, earnedBadges: [], }, ]; couchdbService.find.mockResolvedValue(mockUsers); const res = await request(app) .get("/api/analytics/top-contributors?metric=posts") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.metric).toBe("posts"); }); it("should return top contributors by streets", async () => { const mockUsers = [ { _id: "user_1", name: "User 1", email: "user1@test.com", points: 500, stats: { tasksCompleted: 10, postsCreated: 5, streetsAdopted: 2 }, earnedBadges: [], }, ]; couchdbService.find.mockResolvedValue(mockUsers); const res = await request(app) .get("/api/analytics/top-contributors?metric=streets") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.metric).toBe("streets"); }); it("should require authentication", async () => { const res = await request(app).get("/api/analytics/top-contributors"); expect(res.statusCode).toBe(401); }); }); describe("GET /api/analytics/street-stats", () => { it("should return street adoption and task statistics", async () => { const mockStreets = [ { type: "street", status: "adopted" }, { type: "street", status: "available" }, { type: "street", status: "adopted" }, ]; const mockTasks = [ { type: "task", status: "completed", street: { streetId: "street_1", name: "Main St" }, }, { type: "task", status: "completed", street: { streetId: "street_1", name: "Main St" }, }, { type: "task", status: "pending", street: { streetId: "street_2", name: "Oak St" } }, ]; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "street") return mockStreets; if (query.selector.type === "task") return mockTasks; return []; }); const res = await request(app) .get("/api/analytics/street-stats") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty("adoption"); expect(res.body.adoption).toHaveProperty("totalStreets", 3); expect(res.body.adoption).toHaveProperty("adoptedStreets", 2); expect(res.body.adoption).toHaveProperty("availableStreets", 1); expect(res.body.adoption).toHaveProperty("adoptionRate"); expect(res.body).toHaveProperty("tasks"); expect(res.body.tasks).toHaveProperty("totalTasks", 3); expect(res.body.tasks).toHaveProperty("completedTasks", 2); expect(res.body.tasks).toHaveProperty("pendingTasks", 1); expect(res.body.tasks).toHaveProperty("completionRate"); expect(res.body).toHaveProperty("topStreets"); expect(Array.isArray(res.body.topStreets)).toBe(true); }); it("should calculate correct adoption rate", async () => { const mockStreets = [ { type: "street", status: "adopted" }, { type: "street", status: "adopted" }, { type: "street", status: "available" }, { type: "street", status: "available" }, ]; const mockTasks = []; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "street") return mockStreets; if (query.selector.type === "task") return mockTasks; return []; }); const res = await request(app) .get("/api/analytics/street-stats") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.adoption.adoptionRate).toBe(50.0); }); it("should calculate correct task completion rate", async () => { const mockStreets = []; const mockTasks = [ { type: "task", status: "completed" }, { type: "task", status: "completed" }, { type: "task", status: "completed" }, { type: "task", status: "pending" }, ]; couchdbService.find.mockImplementation(async (query) => { if (query.selector.type === "street") return mockStreets; if (query.selector.type === "task") return mockTasks; return []; }); const res = await request(app) .get("/api/analytics/street-stats") .set("x-auth-token", authToken); expect(res.statusCode).toBe(200); expect(res.body.tasks.completionRate).toBe(75.0); }); it("should require authentication", async () => { const res = await request(app).get("/api/analytics/street-stats"); expect(res.statusCode).toBe(401); }); }); });