Skip to content
Open
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
122 changes: 122 additions & 0 deletions src/__tests__/unit/utils/mockSetup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { Prisma } from "@prisma/client";
import { describe, expect, it, vi } from "vitest";

vi.mock("@/lib/db", () => ({ db: {} }));

vi.mock("@/lib/encryption", () => ({
EncryptionService: {
getInstance: vi.fn(() => ({
encryptField: vi.fn((field: string, value: string) => ({
data: `encrypted_${value}`,
iv: "mock_iv",
tag: "mock_tag",
version: "v1",
encryptedAt: new Date().toISOString(),
})),
})),
},
}));

vi.mock("@/utils/mockSeedData", () => ({
seedMockData: vi.fn(),
seedPublicMockWorkspace: vi.fn(),
}));

import { deriveMockGitHubUserId, deriveMockInstallationId, resolveMockInstallationId } from "@/utils/mockSetup";

const INT4_MAX = 2_147_483_647;
const MOCK_ORG_INSTALLATION_ID = 999001;

const sampleUsernames = [
"alice",
"bob",
"mock-user",
"tom-smith",
"alice-stakwork",
"bob-stakwork",
"tom-smith-stakwork",
...Array.from({ length: 500 }, (_, i) => `user-${i}`),
];

function fakeTx(orgsByInstallationId: Record<number, { githubLogin: string }>): Prisma.TransactionClient {
return {
sourceControlOrg: {
findUnique: vi.fn(
async ({ where }: { where: { githubInstallationId: number } }) =>
orgsByInstallationId[where.githubInstallationId] ?? null,
),
},
} as unknown as Prisma.TransactionClient;
}

describe("deriveMockInstallationId", () => {
it("returns the same id for the same username on every call", () => {
// Regression guard for the original bug: the id used to come from a
// module-level counter that reset on server restart, so a persisted
// SourceControlOrg from a previous process collided (P2002) with the
// create for a new mock username.
const first = deriveMockInstallationId("alice");
expect(deriveMockInstallationId("alice")).toBe(first);
expect(deriveMockInstallationId("alice")).toBe(first);
});

it("returns distinct ids for distinct usernames", () => {
const ids = sampleUsernames.map(deriveMockInstallationId);
expect(new Set(ids).size).toBe(sampleUsernames.length);
});

it("stays within Postgres INT4 range and clear of the fixed mock-org id", () => {
for (const id of sampleUsernames.map(deriveMockInstallationId)) {
expect(id).toBeGreaterThanOrEqual(1_000_000_000);
expect(id).toBeLessThanOrEqual(INT4_MAX);
expect(id).not.toBe(MOCK_ORG_INSTALLATION_ID);
}
});
});

describe("deriveMockGitHubUserId", () => {
it("returns a stable numeric string per username", () => {
const first = deriveMockGitHubUserId("alice");
expect(deriveMockGitHubUserId("alice")).toBe(first);
expect(first).toMatch(/^\d+$/);
});

it("returns distinct ids for distinct usernames", () => {
const ids = sampleUsernames.map(deriveMockGitHubUserId);
expect(new Set(ids).size).toBe(sampleUsernames.length);
});

it("does not collide with the installation id for the same username", () => {
for (const name of sampleUsernames) {
expect(deriveMockGitHubUserId(name)).not.toBe(String(deriveMockInstallationId(name)));
}
});
});

describe("resolveMockInstallationId", () => {
it("returns the derived id when no org holds it", async () => {
const tx = fakeTx({});
await expect(resolveMockInstallationId(tx, "alice")).resolves.toBe(deriveMockInstallationId("alice"));
});

it("returns the derived id when the same login already holds it (re-seed)", async () => {
const derived = deriveMockInstallationId("alice");
const tx = fakeTx({ [derived]: { githubLogin: "alice" } });
await expect(resolveMockInstallationId(tx, "alice")).resolves.toBe(derived);
});

it("probes past an id held by a different login", async () => {
const derived = deriveMockInstallationId("alice");
const tx = fakeTx({ [derived]: { githubLogin: "someone-else" } });
await expect(resolveMockInstallationId(tx, "alice")).resolves.toBe(derived + 1);
});

it("keeps probing until it finds a free id", async () => {
const derived = deriveMockInstallationId("alice");
const tx = fakeTx({
[derived]: { githubLogin: "someone-else" },
[derived + 1]: { githubLogin: "another-login" },
});
await expect(resolveMockInstallationId(tx, "alice")).resolves.toBe(derived + 2);
});
});
58 changes: 52 additions & 6 deletions src/utils/mockSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,61 @@ import {
MilestoneStatus,
PodState,
PoolState,
Prisma,
RepositoryStatus,
SourceControlOrgType,
SwarmStatus,
} from "@prisma/client";
import { seedMockData, seedPublicMockWorkspace } from "./mockSeedData";
import { slugify } from "./slugify";

// Mock GitHub user ID counter (starts high to avoid conflicts)
let mockGitHubIdCounter = 100000;
// Mock GitHub ids must be stable across server restarts: SourceControlOrg is
// upserted by githubLogin but created with githubInstallationId, which is
// unique in the DB. An id from an in-memory counter changes between runs and
// can collide with a row persisted by a previous process, so sign-in for a
// new mock username fails with P2002 (surfaced as NextAuth AccessDenied).
// Ids are therefore derived from the mock username, mapped into [1e9, 2e9):
// inside Postgres INT4 range and clear of real installation ids and the
// fixed MOCK_ORG_INSTALLATION_ID.
const MOCK_ID_RANGE_START = 1_000_000_000;
const MOCK_ID_RANGE_SIZE = 1_000_000_000;

function fnv1aHash(seed: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < seed.length; i++) {
hash ^= seed.charCodeAt(i);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
return hash;
}

export function deriveMockInstallationId(githubLogin: string): number {
return MOCK_ID_RANGE_START + (fnv1aHash(`installation:${githubLogin}`) % MOCK_ID_RANGE_SIZE);
}

export function deriveMockGitHubUserId(githubLogin: string): string {
return String(MOCK_ID_RANGE_START + (fnv1aHash(`user:${githubLogin}`) % MOCK_ID_RANGE_SIZE));
}

/**
* Returns an installation id that is safe to create for `githubLogin`: the
* derived id, or the next free one when a different login already holds it
* (a hash collision between two mock usernames — rare, but it would
* reproduce the exact P2002 sign-in failure the derived ids exist to
* prevent). Idempotent for a login whose org row already exists.
*/
export async function resolveMockInstallationId(tx: Prisma.TransactionClient, githubLogin: string): Promise<number> {
let candidate = deriveMockInstallationId(githubLogin);
for (let attempt = 0; attempt < 1000; attempt++) {
const holder = await tx.sourceControlOrg.findUnique({
where: { githubInstallationId: candidate },
select: { githubLogin: true },
});
if (!holder || holder.githubLogin === githubLogin) return candidate;
candidate = candidate + 1 < MOCK_ID_RANGE_START + MOCK_ID_RANGE_SIZE ? candidate + 1 : MOCK_ID_RANGE_START;
}
throw new Error(`Unable to allocate a mock installation id for ${githubLogin}`);
}

/**
* Ensures a mock workspace and a completed swarm exist for a given user.
Expand Down Expand Up @@ -50,8 +96,7 @@ export async function ensureMockWorkspaceForUser(
const mockGitHubUsername = user?.name?.toLowerCase().replace(/\s+/g, "-")
|| user?.email?.split("@")[0]
|| "mock-user";
const mockGitHubUserId = String(mockGitHubIdCounter++);
const mockInstallationId = mockGitHubIdCounter++;
const mockGitHubUserId = deriveMockGitHubUserId(mockGitHubUsername);

// Create encrypted tokens for mock (optional - gracefully handle if encryption not available)
let encryptedPoolApiKey: string | null = null;
Expand Down Expand Up @@ -102,6 +147,7 @@ export async function ensureMockWorkspaceForUser(
});

// 2. Create SourceControlOrg (represents the GitHub org/user that has the app installed)
const mockInstallationId = await resolveMockInstallationId(tx, mockGitHubUsername);
const sourceControlOrg = await tx.sourceControlOrg.upsert({
where: { githubLogin: mockGitHubUsername },
create: {
Expand Down Expand Up @@ -249,8 +295,7 @@ export async function ensureStakworkMockWorkspace(

// Generate mock GitHub username from user's name or email (stakwork-specific)
const mockGitHubUsername = `${user?.name?.toLowerCase().replace(/\s+/g, "-") || user?.email?.split("@")[0] || "mock-user"}-stakwork`;
const mockGitHubUserId = String(mockGitHubIdCounter++);
const mockInstallationId = mockGitHubIdCounter++;
const mockGitHubUserId = deriveMockGitHubUserId(mockGitHubUsername);

// Create encrypted tokens for mock (optional - gracefully handle if encryption not available)
let encryptedPoolApiKey: string | null = null;
Expand Down Expand Up @@ -297,6 +342,7 @@ export async function ensureStakworkMockWorkspace(
});

// 2. Create SourceControlOrg (represents the GitHub org/user that has the app installed)
const mockInstallationId = await resolveMockInstallationId(tx, mockGitHubUsername);
const sourceControlOrg = await tx.sourceControlOrg.upsert({
where: { githubLogin: mockGitHubUsername },
create: {
Expand Down
Loading