Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Tests

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read

steps:
- uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"

- name: Install Dependencies
run: pnpm install --frozen-lockfile
working-directory: ./claim-db-worker

- name: Run claim-db-worker tests
run: pnpm test
working-directory: ./claim-db-worker
env:
NODE_ENV: test

- name: Install create-db dependencies
run: pnpm install --frozen-lockfile
working-directory: ./create-db

- name: Run create-db tests
run: pnpm test
working-directory: ./create-db
env:
NODE_ENV: test
196 changes: 196 additions & 0 deletions claim-db-worker/__tests__/callback-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// __tests__/callback-api.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { GET } from "../app/api/auth/callback/route";
import { NextRequest } from "next/server";

vi.mock("@/lib/env", () => ({
getEnv: vi.fn(() => ({
CLAIM_DB_RATE_LIMITER: {
limit: vi.fn(() => Promise.resolve({ success: true })),
},
POSTHOG_API_KEY: "test-key",
POSTHOG_API_HOST: "https://app.posthog.com",
})),
}));

vi.mock("@/lib/auth-utils", () => ({
exchangeCodeForToken: vi.fn(),
validateProject: vi.fn(),
}));

vi.mock("@/lib/response-utils", () => ({
redirectToError: vi.fn(),
redirectToSuccess: vi.fn(),
getBaseUrl: vi.fn(() => "http://localhost:3000"),
}));

vi.mock("@/lib/project-transfer", () => ({
transferProject: vi.fn(),
}));

const mockFetch = vi.fn();
global.fetch = mockFetch;

describe("auth callback API", () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe("successful claim flow", () => {
it("completes full OAuth callback and project transfer", async () => {
const { exchangeCodeForToken, validateProject } = await import(
"@/lib/auth-utils"
);
const { redirectToError, redirectToSuccess } = await import(
"@/lib/response-utils"
);
const { transferProject } = await import("@/lib/project-transfer");

vi.mocked(exchangeCodeForToken).mockResolvedValue({
access_token: "test-token",
});

vi.mocked(validateProject).mockResolvedValue(undefined);

vi.mocked(transferProject).mockResolvedValue({
success: true,
status: 200,
});

vi.mocked(redirectToSuccess).mockReturnValue(
new Response(null, {
status: 302,
headers: { Location: "/success?projectID=test-project-123" },
})
);

mockFetch.mockResolvedValue({
ok: true,
json: async () => ({}),
});

const request = new NextRequest(
"http://localhost:3000/api/auth/callback?code=test-code&state=test-state&projectID=test-project-123"
);

const response = await GET(request);

expect(exchangeCodeForToken).toHaveBeenCalledWith(
"test-code",
expect.stringContaining("test-project-123")
);
expect(validateProject).toHaveBeenCalledWith("test-project-123");
expect(transferProject).toHaveBeenCalledWith(
"test-project-123",
"test-token"
);
expect(redirectToSuccess).toHaveBeenCalledWith(
request,
"test-project-123"
);

expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("posthog.com"),
expect.objectContaining({
method: "POST",
body: expect.stringContaining("create_db:claim_successful"),
})
);
});
});

describe("error handling", () => {
it("handles missing parameters", async () => {
const { redirectToError } = await import("@/lib/response-utils");

vi.mocked(redirectToError).mockReturnValue(
new Response(null, {
status: 302,
headers: { Location: "/error" },
})
);

const request = new NextRequest(
"http://localhost:3000/api/auth/callback?code=test-code"
);

await GET(request);

expect(redirectToError).toHaveBeenCalledWith(
request,
"Missing State Parameter",
"Please try again.",
"The state parameter is required for security purposes."
);
});

it("handles auth token exchange failure", async () => {
const { exchangeCodeForToken } = await import("@/lib/auth-utils");
const { redirectToError } = await import("@/lib/response-utils");

vi.mocked(exchangeCodeForToken).mockRejectedValue(
new Error("Invalid authorization code")
);

vi.mocked(redirectToError).mockReturnValue(
new Response(null, {
status: 302,
headers: { Location: "/error" },
})
);

const request = new NextRequest(
"http://localhost:3000/api/auth/callback?code=invalid-code&state=test-state&projectID=test-project-123"
);

await GET(request);

expect(redirectToError).toHaveBeenCalledWith(
request,
"Authentication Failed",
"Failed to authenticate with Prisma. Please try again.",
"Invalid authorization code"
);
});

it("handles project transfer failure", async () => {
const { exchangeCodeForToken, validateProject } = await import(
"@/lib/auth-utils"
);
const { redirectToError } = await import("@/lib/response-utils");
const { transferProject } = await import("@/lib/project-transfer");

vi.mocked(exchangeCodeForToken).mockResolvedValue({
access_token: "test-token",
});

vi.mocked(validateProject).mockResolvedValue(undefined);

vi.mocked(transferProject).mockResolvedValue({
success: false,
status: 403,
error: "Insufficient permissions",
});

vi.mocked(redirectToError).mockReturnValue(
new Response(null, {
status: 302,
headers: { Location: "/error" },
})
);

const request = new NextRequest(
"http://localhost:3000/api/auth/callback?code=test-code&state=test-state&projectID=test-project-123"
);

await GET(request);

expect(redirectToError).toHaveBeenCalledWith(
request,
"Transfer Failed",
"Failed to transfer the project. Please try again.",
expect.stringContaining("Insufficient permissions")
);
});
});
});
Loading