This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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("<!DOCTYPE html>");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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([])),
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user