From b258ee0a071ff4d2eece2c4ae991013dd0241cc5 Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Mon, 11 May 2026 17:28:08 +0530 Subject: [PATCH] test: centralize redis mock in test setup --- tests/core/ConfigManager.test.ts | 34 ++++---- tests/core/TokenManager.test.ts | 22 ++--- tests/integration/api.test.ts | 106 +++++++++++++++++++----- tests/integration/auth.test.ts | 71 ++++++++++++++++ tests/integration/config.test.ts | 81 ++++++++++++++++++ tests/integration/dashboard.test.ts | 37 +++++++++ tests/providers/GenericProvider.test.ts | 46 +++++----- tests/setup.ts | 11 +++ 8 files changed, 334 insertions(+), 74 deletions(-) create mode 100644 tests/integration/auth.test.ts create mode 100644 tests/integration/config.test.ts create mode 100644 tests/integration/dashboard.test.ts diff --git a/tests/core/ConfigManager.test.ts b/tests/core/ConfigManager.test.ts index 5cf4718..497370a 100644 --- a/tests/core/ConfigManager.test.ts +++ b/tests/core/ConfigManager.test.ts @@ -13,32 +13,32 @@ describe("ConfigManager", () => { get: mock((key) => Promise.resolve(storage[key] || null)), }; const manager = new ConfigManager(redis); - const config = { - clientId: "id", - clientSecret: "secret", - authUrl: "https://auth.com", - tokenUrl: "https://token.com", - scope: "all", + const traktConfig = { + clientId: "trakt-client-id", + clientSecret: "trakt-client-secret", + authUrl: "https://trakt.tv/oauth/authorize", + tokenUrl: "https://api.trakt.tv/oauth/token", + scope: "public", }; - await manager.setProviderConfig("test", config); - const retrieved = await manager.getProviderConfig("test"); + await manager.setProviderConfig("trakt", traktConfig); + const retrieved = await manager.getProviderConfig("trakt"); - expect(retrieved).toEqual(config); + expect(retrieved).toEqual(traktConfig); expect(redis.set).toHaveBeenCalled(); }); it("should return all providers", async () => { const redis = { - keys: mock(() => Promise.resolve(["config:p1", "config:p2"])), + keys: mock(() => Promise.resolve(["config:trakt", "config:github"])), get: mock((key) => Promise.resolve( JSON.stringify({ - clientId: key, - clientSecret: "s", - authUrl: "https://a.com", - tokenUrl: "https://t.com", - scope: "x", + clientId: `${key}-id`, + clientSecret: "secret", + authUrl: "https://auth.com", + tokenUrl: "https://token.com", + scope: "all", }), ), ), @@ -48,14 +48,14 @@ describe("ConfigManager", () => { const providers = await manager.getAllProviders(); expect(Object.keys(providers)).toHaveLength(2); - expect(providers.p1.clientId).toBe("config:p1"); + expect(providers.trakt.clientId).toBe("config:trakt-id"); }); it("should return null for non-existent provider", async () => { const redis = { get: mock(() => Promise.resolve(null)) }; const manager = new ConfigManager(redis); - const config = await manager.getProviderConfig("missing"); + const config = await manager.getProviderConfig("missing-provider"); expect(config).toBeNull(); }); diff --git a/tests/core/TokenManager.test.ts b/tests/core/TokenManager.test.ts index 1d4c91a..f2456d1 100644 --- a/tests/core/TokenManager.test.ts +++ b/tests/core/TokenManager.test.ts @@ -4,24 +4,24 @@ import { TokenManager } from "../../src/core/TokenManager"; describe("TokenManager", () => { it("should return token from redis if available", async () => { - const redis = { get: mock(() => Promise.resolve("valid_token")) }; + const redis = { get: mock(() => Promise.resolve("active-access-token")) }; const manager = new TokenManager(redis, {}); const token = await manager.getAccessToken("trakt"); - expect(token).toBe("valid_token"); + expect(token).toBe("active-access-token"); }); it("should refresh token if access token is missing but refresh token exists", async () => { const redis = { - get: mock((key) => Promise.resolve(key.includes("refresh") ? "refresh_token" : null)), + get: mock((key) => Promise.resolve(key.includes("refresh") ? "valid-refresh-token" : null)), set: mock(() => Promise.resolve()), }; const provider = { refreshToken: mock(() => Promise.resolve({ - accessToken: "new_token", - refreshToken: "new_refresh", + accessToken: "newly-refreshed-access-token", + refreshToken: "newly-refreshed-refresh-token", expiresIn: 3600, }), ), @@ -30,7 +30,7 @@ describe("TokenManager", () => { const token = await manager.getAccessToken("trakt"); - expect(token).toBe("new_token"); + expect(token).toBe("newly-refreshed-access-token"); expect(redis.set).toHaveBeenCalled(); }); @@ -45,14 +45,14 @@ describe("TokenManager", () => { it("should refresh token via refreshAccessToken", async () => { const redis = { - get: mock(() => Promise.resolve("refresh_token")), + get: mock(() => Promise.resolve("existing-refresh-token")), set: mock(() => Promise.resolve()), }; const provider = { refreshToken: mock(() => Promise.resolve({ - accessToken: "forced_token", - refreshToken: "new_refresh", + accessToken: "manually-refreshed-access-token", + refreshToken: "manually-refreshed-refresh-token", expiresIn: 3600, }), ), @@ -61,8 +61,8 @@ describe("TokenManager", () => { const token = await manager.refreshAccessToken("trakt"); - expect(token).toBe("forced_token"); - expect(provider.refreshToken).toHaveBeenCalledWith("refresh_token"); + expect(token).toBe("manually-refreshed-access-token"); + expect(provider.refreshToken).toHaveBeenCalledWith("existing-refresh-token"); }); it("should return null in refreshAccessToken if no refresh token is found", async () => { diff --git a/tests/integration/api.test.ts b/tests/integration/api.test.ts index 6313d4a..f455100 100644 --- a/tests/integration/api.test.ts +++ b/tests/integration/api.test.ts @@ -1,24 +1,24 @@ // @ts-nocheck -import { afterEach, describe, expect, it, mock } from "bun:test"; - -mock.module("../../src/core/RedisClient", () => ({ - redis: { - get: mock(() => Promise.resolve(null)), - set: mock(() => Promise.resolve()), - keys: mock(() => Promise.resolve([])), - }, -})); - +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test"; import { redis } from "../../src/core/RedisClient"; import { app } from "../../src/index"; describe("API Integration", () => { afterEach(() => { + mock.restore(); redis.get.mockImplementation(() => Promise.resolve(null)); redis.set.mockImplementation(() => Promise.resolve()); redis.keys.mockImplementation(() => Promise.resolve([])); }); + const mockTraktConfig = JSON.stringify({ + clientId: "trakt-client-id", + clientSecret: "trakt-client-secret", + authUrl: "https://trakt.tv/oauth/authorize", + tokenUrl: "https://api.trakt.tv/oauth/token", + scope: "public", + }); + it("should return 401 if API Key is missing", async () => { const res = await app.request("/api/status"); @@ -38,17 +38,10 @@ describe("API Integration", () => { it("should return 200 for status with valid API Key", async () => { redis.keys.mockReturnValue(Promise.resolve(["config:trakt"])); redis.get.mockImplementation((key) => { - if (key.includes("config")) - return Promise.resolve( - JSON.stringify({ - clientId: "id", - clientSecret: "s", - authUrl: "https://a.com", - tokenUrl: "https://t.com", - scope: "x", - }), - ); - return Promise.resolve("mock_value"); + if (key.includes("config")) return Promise.resolve(mockTraktConfig); + if (key.includes("access_token")) return Promise.resolve("current-access-token"); + if (key.includes("refresh_token")) return Promise.resolve("current-refresh-token"); + return Promise.resolve(null); }); const res = await app.request("/api/status", { @@ -60,11 +53,11 @@ describe("API Integration", () => { expect(res.status).toBe(200); const body = await res.json(); expect(body.trakt).toBeDefined(); - expect(body.trakt.accessToken).toBe("mock_value"); + expect(body.trakt.accessToken).toBe("current-access-token"); }); it("should return 404 for unknown provider token", async () => { - const res = await app.request("/api/token/unknown", { + const res = await app.request("/api/token/unconfigured-provider", { headers: { Authorization: "Bearer test-api-key", }, @@ -72,4 +65,71 @@ describe("API Integration", () => { expect(res.status).toBe(404); }); + + it("should return token for a configured provider", async () => { + redis.get.mockImplementation((key) => { + if (key.includes("config:trakt")) return Promise.resolve(mockTraktConfig); + if (key.includes("access_token")) return Promise.resolve("trakt-active-token"); + return Promise.resolve(null); + }); + + const res = await app.request("/api/token/trakt", { + headers: { + Authorization: "Bearer test-api-key", + }, + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.access_token).toBe("trakt-active-token"); + }); + + it("should return 404 if no access token is in redis for a valid provider", async () => { + redis.get.mockImplementation((key) => { + if (key.includes("config:trakt")) return Promise.resolve(mockTraktConfig); + return Promise.resolve(null); + }); + + const res = await app.request("/api/token/trakt", { + headers: { + Authorization: "Bearer test-api-key", + }, + }); + + expect(res.status).toBe(404); + const body = await res.json(); + expect(body.error).toContain("No tokens found"); + }); + + it("should successfully refresh a token", async () => { + redis.get.mockImplementation((key) => { + if (key.includes("config:trakt")) return Promise.resolve(mockTraktConfig); + if (key.includes("refresh_token")) return Promise.resolve("old-refresh-token"); + return Promise.resolve("new-access-token-from-refresh"); + }); + + spyOn(globalThis, "fetch").mockImplementation(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + access_token: "new-access-token-from-fetch", + refresh_token: "new-refresh-token-from-fetch", + expires_in: 3600, + }), + }), + ); + + const res = await app.request("/api/refresh/trakt", { + method: "POST", + headers: { + Authorization: "Bearer test-api-key", + }, + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.success).toBe(true); + expect(body.status.accessToken).toBe("new-access-token-from-refresh"); + }); }); diff --git a/tests/integration/auth.test.ts b/tests/integration/auth.test.ts new file mode 100644 index 0000000..f342b58 --- /dev/null +++ b/tests/integration/auth.test.ts @@ -0,0 +1,71 @@ +// @ts-nocheck +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { redis } from "../../src/core/RedisClient"; +import { app } from "../../src/index"; + +describe("Auth Integration", () => { + afterEach(() => { + mock.restore(); + redis.get.mockImplementation(() => Promise.resolve(null)); + }); + + const mockProviderConfig = JSON.stringify({ + clientId: "trakt-client-id", + clientSecret: "trakt-client-secret", + authUrl: "https://trakt.tv/oauth/authorize", + tokenUrl: "https://api.trakt.tv/oauth/token", + scope: "public", + }); + + it("should redirect to provider login", async () => { + redis.get.mockImplementation((key) => { + if (key.includes("config:trakt")) return Promise.resolve(mockProviderConfig); + return Promise.resolve(null); + }); + + const res = await app.request("/auth/trakt/login"); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toContain("trakt.tv/oauth/authorize"); + expect(res.headers.get("Location")).toContain("client_id=trakt-client-id"); + }); + + it("should handle callback and exchange code", async () => { + redis.get.mockImplementation((key) => { + if (key.includes("config:trakt")) return Promise.resolve(mockProviderConfig); + return Promise.resolve(null); + }); + + const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + access_token: "exchange-access-token", + refresh_token: "exchange-refresh-token", + expires_in: 3600, + }), + }), + ); + + const res = await app.request("/auth/callback?state=trakt&code=temporary-auth-code"); + + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("trakt"); + expect(redis.set).toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalled(); + }); + + it("should return 404 if provider not configured during login", async () => { + const res = await app.request("/auth/unknown-provider/login"); + + expect(res.status).toBe(404); + }); + + it("should return 400 if callback is missing state or code", async () => { + const res = await app.request("/auth/callback?code=some-code"); + + expect(res.status).toBe(400); + }); +}); diff --git a/tests/integration/config.test.ts b/tests/integration/config.test.ts new file mode 100644 index 0000000..68fcbcd --- /dev/null +++ b/tests/integration/config.test.ts @@ -0,0 +1,81 @@ +// @ts-nocheck +import { afterEach, describe, expect, it, mock } from "bun:test"; +import { redis } from "../../src/core/RedisClient"; +import { app } from "../../src/index"; + +describe("Config Integration", () => { + afterEach(() => { + mock.restore(); + redis.get.mockImplementation(() => Promise.resolve(null)); + redis.set.mockImplementation(() => Promise.resolve()); + redis.keys.mockImplementation(() => Promise.resolve([])); + }); + + it("should list all configured providers", async () => { + redis.keys.mockReturnValue(Promise.resolve(["config:trakt"])); + redis.get.mockImplementation(() => + Promise.resolve( + JSON.stringify({ + clientId: "trakt-client-id", + clientSecret: "trakt-client-secret", + authUrl: "https://trakt.tv/oauth/authorize", + tokenUrl: "https://api.trakt.tv/oauth/token", + scope: "public", + }), + ), + ); + + const res = await app.request("/api/config", { + headers: { + Authorization: "Bearer test-api-key", + }, + }); + + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.trakt).toBeDefined(); + expect(body.trakt.clientId).toBe("trakt-client-id"); + }); + + it("should set a new provider config", async () => { + const newProviderConfig = { + clientId: "github-client-id", + clientSecret: "github-client-secret", + authUrl: "https://github.com/login/oauth/authorize", + tokenUrl: "https://github.com/login/oauth/access_token", + scope: "user:email", + }; + + const res = await app.request("/api/config/github", { + method: "POST", + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + }, + body: JSON.stringify(newProviderConfig), + }); + + expect(res.status).toBe(200); + expect(redis.set).toHaveBeenCalledWith( + "config:github", + expect.stringContaining("github-client-id"), + ); + }); + + it("should return 400 for invalid config body", async () => { + const invalidConfig = { + clientId: "missing-other-required-fields", + }; + + const res = await app.request("/api/config/invalid", { + method: "POST", + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + }, + body: JSON.stringify(invalidConfig), + }); + + expect(res.status).toBe(400); + }); +}); diff --git a/tests/integration/dashboard.test.ts b/tests/integration/dashboard.test.ts new file mode 100644 index 0000000..507f952 --- /dev/null +++ b/tests/integration/dashboard.test.ts @@ -0,0 +1,37 @@ +// @ts-nocheck +import { describe, expect, it } from "bun:test"; +import { redis } from "../../src/core/RedisClient"; +import { app } from "../../src/index"; + +describe("Dashboard & Common Integration", () => { + it("should serve the dashboard HTML", async () => { + const res = await app.request("/app"); + + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toContain("text/html"); + const html = await res.text(); + expect(html).toContain(""); + }); + + it("should return 404 with custom handler for unknown routes", async () => { + const res = await app.request("/unknown-route"); + + expect(res.status).toBe(404); + const body = await res.json(); + expect(body.error).toBe("Not Found"); + }); + + it("should return 500 for internal errors", async () => { + redis.keys.mockImplementationOnce(() => { + throw new Error("Redis Crash"); + }); + + const res = await app.request("/api/status", { + headers: { Authorization: "Bearer test-api-key" }, + }); + + expect(res.status).toBe(500); + const body = await res.json(); + expect(body.error).toBe("Internal Server Error"); + }); +}); diff --git a/tests/providers/GenericProvider.test.ts b/tests/providers/GenericProvider.test.ts index 5fca566..e8e86b8 100644 --- a/tests/providers/GenericProvider.test.ts +++ b/tests/providers/GenericProvider.test.ts @@ -3,12 +3,12 @@ import { afterEach, describe, expect, it, mock, spyOn } from "bun:test"; import { GenericProvider } from "../../src/providers/GenericProvider"; describe("GenericProvider", () => { - const config = { - clientId: "id", - clientSecret: "secret", - authUrl: "https://auth.com", - tokenUrl: "https://token.com", - scope: "all", + const traktConfig = { + clientId: "trakt-client-id", + clientSecret: "trakt-client-secret", + authUrl: "https://trakt.tv/oauth/authorize", + tokenUrl: "https://api.trakt.tv/oauth/token", + scope: "public", }; afterEach(() => { @@ -16,54 +16,54 @@ describe("GenericProvider", () => { }); it("should generate correct auth URL", () => { - const provider = new GenericProvider("test", config); + const provider = new GenericProvider("trakt", traktConfig); - const url = provider.getAuthUrl("test", "http://cb.com"); + const url = provider.getAuthUrl("random-state-123", "https://callback.com"); - expect(url).toContain("client_id=id"); - expect(url).toContain("redirect_uri=http%3A%2F%2Fcb.com"); - expect(url).toContain("state=test"); + expect(url).toContain("client_id=trakt-client-id"); + expect(url).toContain("redirect_uri=https%3A%2F%2Fcallback.com"); + expect(url).toContain("state=random-state-123"); }); it("should handle token response with string expiry", async () => { - const provider = new GenericProvider("test", config); + const provider = new GenericProvider("trakt", traktConfig); const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ - access_token: "at", - refresh_token: "rt", - expires_in: "3600", + access_token: "new-access-token", + refresh_token: "new-refresh-token", + expires_in: "7200", }), text: () => Promise.resolve(""), }), ); - const tokens = await provider.refreshToken("old_rt"); + const tokens = await provider.refreshToken("old-refresh-token"); - expect(tokens.accessToken).toBe("at"); - expect(tokens.expiresIn).toBe(3600); + expect(tokens.accessToken).toBe("new-access-token"); + expect(tokens.expiresIn).toBe(7200); expect(fetchSpy).toHaveBeenCalled(); }); it("should handle token response without new refresh token", async () => { - const provider = new GenericProvider("test", config); + const provider = new GenericProvider("trakt", traktConfig); spyOn(globalThis, "fetch").mockImplementation(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ - access_token: "at", + access_token: "new-access-token-only", expires_in: 3600, }), text: () => Promise.resolve(""), }), ); - const tokens = await provider.refreshToken("old_rt"); + const tokens = await provider.refreshToken("existing-refresh-token"); - expect(tokens.accessToken).toBe("at"); - expect(tokens.refreshToken).toBe("old_rt"); + expect(tokens.accessToken).toBe("new-access-token-only"); + expect(tokens.refreshToken).toBe("existing-refresh-token"); }); }); diff --git a/tests/setup.ts b/tests/setup.ts index 1d0a6f2..883bb21 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,4 +1,15 @@ +import { mock } from "bun:test"; + // Global test setup to stub environment variables process.env.API_KEY = "test-api-key"; process.env.REDIS_URL = "redis://localhost:6379"; process.env.PORT = "3000"; + +// Global Redis mock +mock.module("../src/core/RedisClient", () => ({ + redis: { + get: mock(() => Promise.resolve(null)), + set: mock(() => Promise.resolve()), + keys: mock(() => Promise.resolve([])), + }, +}));