From 78a0c7c04ca2981b5602b972348be32be6e5cae8 Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Fri, 28 Aug 2026 11:07:03 +0200 Subject: [PATCH 1/3] fix(github): gate multiple organization installations --- .../github/callback/route.test.ts | 32 ++++++++++ .../api/integrations/github/callback/route.ts | 10 ++-- .../GitHubIntegrationDetails.test.ts | 9 +++ .../integrations/GitHubIntegrationDetails.tsx | 15 +++-- .../db/platform-integrations.test.ts | 19 ++++++ .../integrations/db/platform-integrations.ts | 21 ++++++- .../github/multiple-installations.test.ts | 16 +++++ .../github/multiple-installations.ts | 8 +++ .../src/routers/github-apps-router.test.ts | 60 ++++++++++++++++++- apps/web/src/routers/github-apps-router.ts | 15 ++++- 10 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/lib/integrations/github/multiple-installations.test.ts create mode 100644 apps/web/src/lib/integrations/github/multiple-installations.ts diff --git a/apps/web/src/app/api/integrations/github/callback/route.test.ts b/apps/web/src/app/api/integrations/github/callback/route.test.ts index deae607764..e03f0cb94b 100644 --- a/apps/web/src/app/api/integrations/github/callback/route.test.ts +++ b/apps/web/src/app/api/integrations/github/callback/route.test.ts @@ -1065,6 +1065,38 @@ describe('GET /api/integrations/github/callback admin proof', () => { expectRedirectLocation(response, `/integrations/github?error=installation_already_claimed`); }); + test('redirects when multiple installations are disabled for the organization', async () => { + const organizationId = '00000000-0000-4000-8000-000000000001'; + mockedConsumeInstallState.mockResolvedValue({ + token: INSTALL_STATE_TOKEN, + kilo_user_id: USER_ID, + owner_type: 'org', + owner_id: organizationId, + github_app_type: 'standard', + return_to: null, + expires_at: new Date(Date.now() + 300_000).toISOString(), + consumed_at: null, + created_at: new Date().toISOString(), + }); + mockedUpsertPlatformIntegrationForOwner.mockResolvedValue({ + ok: false, + reason: 'multiple_installations_disabled', + }); + + const { GET } = await import('./route'); + const response = await GET( + makeRequest( + `/api/integrations/github/callback?installation_id=${INSTALLATION_ID}&setup_action=install&state=${INSTALL_STATE_TOKEN}&code=abc` + ) as never + ); + + expect(response.status).toBe(307); + expectRedirectLocation( + response, + `/organizations/${organizationId}/integrations/github?error=multiple_installations_disabled` + ); + }); + test('logs distinct messages for code-absent vs non-admin', async () => { const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); diff --git a/apps/web/src/app/api/integrations/github/callback/route.ts b/apps/web/src/app/api/integrations/github/callback/route.ts index a8efaa489a..250351db00 100644 --- a/apps/web/src/app/api/integrations/github/callback/route.ts +++ b/apps/web/src/app/api/integrations/github/callback/route.ts @@ -636,13 +636,15 @@ async function handleCoreInstallFlow(params: { }); if (!upsertResult.ok) { + const error = + upsertResult.reason === 'multiple_installations_disabled' + ? 'multiple_installations_disabled' + : 'installation_already_claimed'; if (isAppInitiated) { - return NextResponse.redirect( - new URL(appFallbackPath('error=installation_already_claimed'), APP_URL) - ); + return NextResponse.redirect(new URL(appFallbackPath(`error=${error}`), APP_URL)); } return NextResponse.redirect( - new URL(appendQueryParam(redirectPath, 'error=installation_already_claimed'), APP_URL) + new URL(appendQueryParam(redirectPath, `error=${error}`), APP_URL) ); } } diff --git a/apps/web/src/components/integrations/GitHubIntegrationDetails.test.ts b/apps/web/src/components/integrations/GitHubIntegrationDetails.test.ts index 05e67898ad..8d5810f007 100644 --- a/apps/web/src/components/integrations/GitHubIntegrationDetails.test.ts +++ b/apps/web/src/components/integrations/GitHubIntegrationDetails.test.ts @@ -56,6 +56,15 @@ describe('GitHubIntegrationDetails fromApp outcome CTA behavior', () => { expect(view.href).toBe('/cloud/sessions?error=installation_already_claimed'); }); + it('non-retryable multiple-installation error: Back and no retry', () => { + const view = buildAppReturnOutcomeView({ error: 'multiple_installations_disabled' }); + expect(view.kind).toBe('blocked'); + expect(view.cta).toBe('Back'); + expect(view.description).toBe( + 'This Kilo organization can currently connect only one GitHub organization.' + ); + }); + it('non-retryable user mismatch: Back, no retry, mismatch copy preserved', () => { const view = buildAppReturnOutcomeView({ error: 'install_state_user_mismatch' }); expect(view.kind).toBe('blocked'); diff --git a/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx b/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx index 815f1300f7..a199adc2fc 100644 --- a/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx +++ b/apps/web/src/components/integrations/GitHubIntegrationDetails.tsx @@ -76,7 +76,8 @@ export function buildAppReturnOutcomeView(input: { const isNonRetryable = input.error === 'install_state_user_mismatch' || input.error === 'not_installation_admin' || - input.error === 'installation_already_claimed'; + input.error === 'installation_already_claimed' || + input.error === 'multiple_installations_disabled'; const returnQuery = isSuccess ? 'github_install=success' : isPending @@ -97,9 +98,11 @@ export function buildAppReturnOutcomeView(input: { ? 'Only a GitHub admin of that account can connect it. Ask an organization admin to install Kilo.' : input.error === 'installation_already_claimed' ? 'That GitHub installation is already connected to another Kilo account. Disconnect it there first.' - : input.error === 'install_state_user_mismatch' - ? 'This connection was started from the Kilo App signed in as a different account. Sign in to the web with that account, or start again from the app.' - : 'The installation did not complete. Try again or return to the Kilo App.'; + : input.error === 'multiple_installations_disabled' + ? 'This Kilo organization can currently connect only one GitHub organization.' + : input.error === 'install_state_user_mismatch' + ? 'This connection was started from the Kilo App signed in as a different account. Sign in to the web with that account, or start again from the app.' + : 'The installation did not complete. Try again or return to the Kilo App.'; const cta = isSuccess ? 'Continue' : isPending ? 'Done' : isNonRetryable ? 'Back' : 'Try again'; return { @@ -152,6 +155,10 @@ function GitHubIntegrationOutcomeToasts({ 'That GitHub installation is already connected to another Kilo account. Disconnect it there first.', { duration: 8000 } ); + } else if (error === 'multiple_installations_disabled') { + toast.error('This organization can currently connect only one GitHub organization.', { + duration: 8000, + }); } else if (error === 'github_authorization_required') { toast.error('GitHub did not return an authorization. Start the connection again.', { duration: 8000, diff --git a/apps/web/src/lib/integrations/db/platform-integrations.test.ts b/apps/web/src/lib/integrations/db/platform-integrations.test.ts index 41b16a3a0e..13465478ac 100644 --- a/apps/web/src/lib/integrations/db/platform-integrations.test.ts +++ b/apps/web/src/lib/integrations/db/platform-integrations.test.ts @@ -116,6 +116,25 @@ describe('upsertPlatformIntegrationForOwner', () => { expect(row.owned_by_organization_id).toBe(orgId); }); + test('rejects a second GitHub installation for an organization outside the allowlist', async () => { + const owner: Owner = { type: 'org', id: orgId }; + await upsertPlatformIntegrationForOwner(owner, baseInstallData(INSTALLATION_ID)); + + const result = await upsertPlatformIntegrationForOwner( + owner, + baseInstallData(`${INSTALLATION_ID}-second`) + ); + + expect(result).toEqual({ ok: false, reason: 'multiple_installations_disabled' }); + + const rows = await db + .select() + .from(platform_integrations) + .where(eq(platform_integrations.owned_by_organization_id, orgId)); + expect(rows).toHaveLength(1); + expect(rows[0]?.platform_installation_id).toBe(INSTALLATION_ID); + }); + test('same-owner refresh updates the existing row (by primary key)', async () => { const owner: Owner = { type: 'user', id: userId }; diff --git a/apps/web/src/lib/integrations/db/platform-integrations.ts b/apps/web/src/lib/integrations/db/platform-integrations.ts index ef483ca556..77a4cc090c 100644 --- a/apps/web/src/lib/integrations/db/platform-integrations.ts +++ b/apps/web/src/lib/integrations/db/platform-integrations.ts @@ -13,6 +13,7 @@ import type { IntegrationStatus } from '../core/constants'; import { platformIntegrationHealthSql } from '../core/health'; import { PendingInstallationMetadataWrapperSchema } from '../core/schemas'; import type { GitHubAppType } from '../platforms/github/app-selector'; +import { canOrganizationUseMultipleGitHubInstallations } from '../github/multiple-installations'; /** * Finds a platform integration by installation ID. @@ -724,7 +725,10 @@ export async function unsuspendIntegrationForOwner( export type UpsertPlatformIntegrationResult = | { ok: true } - | { ok: false; reason: 'claimed_by_other_owner' }; + | { + ok: false; + reason: 'claimed_by_other_owner' | 'multiple_installations_disabled'; + }; /** * Owner-aware upsert for platform integrations. @@ -776,6 +780,21 @@ export async function upsertPlatformIntegrationForOwner( // Step 2: if the insert was blocked, re-read the row and determine // whether this is a same-owner refresh or a cross-owner claim. if (data.platform === 'github') { + if (owner.type === 'org' && !canOrganizationUseMultipleGitHubInstallations(owner.id)) { + const ownerIntegrations = await getIntegrationsByOrganization(owner.id, PLATFORM.GITHUB); + const hasExistingInstallation = ownerIntegrations.some( + integration => integration.platform_installation_id !== null + ); + const isExistingInstallation = ownerIntegrations.some( + integration => + integration.platform_installation_id === data.platformInstallationId && + integration.github_app_type === appType + ); + if (hasExistingInstallation && !isExistingInstallation) { + return { ok: false, reason: 'multiple_installations_disabled' }; + } + } + const inserted = await db .insert(platform_integrations) .values(values) diff --git a/apps/web/src/lib/integrations/github/multiple-installations.test.ts b/apps/web/src/lib/integrations/github/multiple-installations.test.ts new file mode 100644 index 0000000000..df3961ec50 --- /dev/null +++ b/apps/web/src/lib/integrations/github/multiple-installations.test.ts @@ -0,0 +1,16 @@ +import { canOrganizationUseMultipleGitHubInstallations } from './multiple-installations'; + +describe('canOrganizationUseMultipleGitHubInstallations', () => { + it.each(['9d278969-5453-4ae3-a51f-a8d2274a7b56', '30f1620a-4aad-4456-bf4d-550f335e6f55'])( + 'enables multiple installations for %s', + organizationId => { + expect(canOrganizationUseMultipleGitHubInstallations(organizationId)).toBe(true); + } + ); + + it('keeps multiple installations disabled for other organizations', () => { + expect( + canOrganizationUseMultipleGitHubInstallations('00000000-0000-4000-8000-000000000001') + ).toBe(false); + }); +}); diff --git a/apps/web/src/lib/integrations/github/multiple-installations.ts b/apps/web/src/lib/integrations/github/multiple-installations.ts new file mode 100644 index 0000000000..c3ddbe172c --- /dev/null +++ b/apps/web/src/lib/integrations/github/multiple-installations.ts @@ -0,0 +1,8 @@ +const MULTIPLE_GITHUB_INSTALLATION_ORGANIZATION_IDS = new Set([ + '9d278969-5453-4ae3-a51f-a8d2274a7b56', + '30f1620a-4aad-4456-bf4d-550f335e6f55', +]); + +export function canOrganizationUseMultipleGitHubInstallations(organizationId: string): boolean { + return MULTIPLE_GITHUB_INSTALLATION_ORGANIZATION_IDS.has(organizationId); +} diff --git a/apps/web/src/routers/github-apps-router.test.ts b/apps/web/src/routers/github-apps-router.test.ts index 8284c111c0..f4f7e94faf 100644 --- a/apps/web/src/routers/github-apps-router.test.ts +++ b/apps/web/src/routers/github-apps-router.test.ts @@ -128,6 +128,7 @@ beforeAll(async () => { }); const organizationId = '00000000-0000-4000-8000-000000000001'; +const multiInstallationOrganizationId = '9d278969-5453-4ae3-a51f-a8d2274a7b56'; const integrationId = '00000000-0000-4000-8000-000000000002'; const organizationRoles = [ 'owner', @@ -222,17 +223,70 @@ describe('githubAppsRouter organization install capability', () => { } ); - it.each(organizationRoles)('reports add capability for organization %s roles', async role => { - mockEnsureOrganizationAccess.mockResolvedValue(role); + it.each(organizationRoles)( + 'reports first-install capability for organization %s roles', + async role => { + mockEnsureOrganizationAccess.mockResolvedValue(role); + const caller = createCaller({ user: { id: 'user-1', is_admin: false } as User }); + + const listed = await caller.listOrganizationInstallations({ organizationId }); + + expect(listed.canAdd).toBe(role === 'owner' || role === 'admin'); + expect(listed.installations).toHaveLength(0); + } + ); + + it('hides additional installation capability for organizations outside the allowlist', async () => { + mockEnsureOrganizationAccess.mockResolvedValue('owner'); mockListIntegrations.mockResolvedValue([organizationIntegration()]); const caller = createCaller({ user: { id: 'user-1', is_admin: false } as User }); const listed = await caller.listOrganizationInstallations({ organizationId }); - expect(listed.canAdd).toBe(role === 'owner' || role === 'admin'); + expect(listed.canAdd).toBe(false); expect(listed.installations).toHaveLength(1); }); + it('reports additional installation capability for allowlisted organizations', async () => { + mockEnsureOrganizationAccess.mockResolvedValue('owner'); + mockListIntegrations.mockResolvedValue([ + { ...organizationIntegration(), owned_by_organization_id: multiInstallationOrganizationId }, + ]); + const caller = createCaller({ user: { id: 'user-1', is_admin: false } as User }); + + const listed = await caller.listOrganizationInstallations({ + organizationId: multiInstallationOrganizationId, + }); + + expect(listed.canAdd).toBe(true); + }); + + it('refuses to mint another install state outside the allowlist', async () => { + mockEnsureOrganizationAccess.mockResolvedValue('owner'); + mockListIntegrations.mockResolvedValue([organizationIntegration()]); + const caller = createCaller({ user: { id: 'user-1', is_admin: false } as User }); + + await expect(caller.mintInstallState({ organizationId })).rejects.toMatchObject({ + code: 'FORBIDDEN', + }); + expect(mockCreateInstallState).not.toHaveBeenCalled(); + }); + + it('mints another install state for an allowlisted organization', async () => { + mockEnsureOrganizationAccess.mockResolvedValue('owner'); + mockListIntegrations.mockResolvedValue([ + { ...organizationIntegration(), owned_by_organization_id: multiInstallationOrganizationId }, + ]); + const caller = createCaller({ user: { id: 'user-1', is_admin: false } as User }); + + await expect( + caller.mintInstallState({ organizationId: multiInstallationOrganizationId }) + ).resolves.toEqual({ token: 'install-token' }); + expect(mockCreateInstallState).toHaveBeenCalledWith( + expect.objectContaining({ ownerId: multiInstallationOrganizationId }) + ); + }); + it('still denies callers outside the organization role matrix before minting state', async () => { mockEnsureOrganizationAccess.mockRejectedValue( new TRPCError({ code: 'UNAUTHORIZED', message: 'Organization access required' }) diff --git a/apps/web/src/routers/github-apps-router.ts b/apps/web/src/routers/github-apps-router.ts index 7d67f8b6ef..1ac51d26ef 100644 --- a/apps/web/src/routers/github-apps-router.ts +++ b/apps/web/src/routers/github-apps-router.ts @@ -39,6 +39,7 @@ import { } from '@/lib/integrations/platforms/github/user-authorization'; import { seedUserGithubToken } from '@/lib/github-pr-review/dev-seed'; import { createInstallState } from '@/lib/integrations/github/install-state'; +import { canOrganizationUseMultipleGitHubInstallations } from '@/lib/integrations/github/multiple-installations'; export const githubAppsRouter = createTRPCRouter({ // List all integrations @@ -61,7 +62,10 @@ export const githubAppsRouter = createTRPCRouter({ const primaryId = integrations.find(isPlatformIntegrationHealthy)?.id ?? null; return { - canAdd: canManageOrganization(role), + canAdd: + canManageOrganization(role) && + (integrations.length === 0 || + canOrganizationUseMultipleGitHubInstallations(input.organizationId)), installations: integrations.map(integration => { const repositories = requireNumericPlatformRepositories(integration.repositories) ?? []; const status: 'connected' | 'pending' | 'suspended' | 'needs_attention' = @@ -153,6 +157,15 @@ export const githubAppsRouter = createTRPCRouter({ input.organizationId, input.organizationId ? ORGANIZATION_MANAGE_ROLES : undefined ); + if (owner.type === 'org' && !canOrganizationUseMultipleGitHubInstallations(owner.id)) { + const integrations = await githubAppsService.listIntegrations(owner); + if (integrations.length > 0) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'This organization already has a GitHub installation', + }); + } + } const appType = await getGitHubAppTypeForOrganization(input.organizationId ?? null); const token = await createInstallState({ From 0ecfb6f20f594ef9bbe472293f8fe860d9f1e9c5 Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Fri, 28 Aug 2026 11:11:44 +0200 Subject: [PATCH 2/3] fix(github): configure multi-install organization allowlist --- ENVIRONMENT.md | 1 + .../github/multiple-installations.test.ts | 48 ++++++++++++++++++- .../github/multiple-installations.ts | 28 +++++++++-- .../src/routers/github-apps-router.test.ts | 10 ++++ 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index 2b843cc7b2..c1867a968b 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -95,6 +95,7 @@ Manage shared web env var additions and rotations with `pnpm web:env set ({ + getEnvVariable: jest.fn(), +})); + +const mockedGetEnvVariable = jest.mocked(getEnvVariable); describe('canOrganizationUseMultipleGitHubInstallations', () => { + beforeEach(() => { + mockedGetEnvVariable.mockReturnValue( + '9d278969-5453-4ae3-a51f-a8d2274a7b56,30f1620a-4aad-4456-bf4d-550f335e6f55' + ); + }); + it.each(['9d278969-5453-4ae3-a51f-a8d2274a7b56', '30f1620a-4aad-4456-bf4d-550f335e6f55'])( 'enables multiple installations for %s', organizationId => { @@ -13,4 +29,34 @@ describe('canOrganizationUseMultipleGitHubInstallations', () => { canOrganizationUseMultipleGitHubInstallations('00000000-0000-4000-8000-000000000001') ).toBe(false); }); + + it('keeps multiple installations disabled when the environment variable is unset', () => { + mockedGetEnvVariable.mockReturnValue(''); + + expect( + canOrganizationUseMultipleGitHubInstallations('9d278969-5453-4ae3-a51f-a8d2274a7b56') + ).toBe(false); + }); +}); + +describe('parseMultipleGitHubInstallationOrganizationIds', () => { + it('trims and deduplicates comma-separated organization IDs', () => { + expect( + parseMultipleGitHubInstallationOrganizationIds( + ' 9d278969-5453-4ae3-a51f-a8d2274a7b56,30f1620a-4aad-4456-bf4d-550f335e6f55,9d278969-5453-4ae3-a51f-a8d2274a7b56 ' + ) + ).toEqual( + new Set(['9d278969-5453-4ae3-a51f-a8d2274a7b56', '30f1620a-4aad-4456-bf4d-550f335e6f55']) + ); + }); + + it('returns an empty set for an empty value', () => { + expect(parseMultipleGitHubInstallationOrganizationIds('')).toEqual(new Set()); + }); + + it('rejects malformed organization IDs', () => { + expect(() => parseMultipleGitHubInstallationOrganizationIds('not-an-organization-id')).toThrow( + 'GITHUB_MULTIPLE_INSTALLATION_ORGANIZATION_IDS must be a comma-separated list of UUIDs' + ); + }); }); diff --git a/apps/web/src/lib/integrations/github/multiple-installations.ts b/apps/web/src/lib/integrations/github/multiple-installations.ts index c3ddbe172c..574f57e277 100644 --- a/apps/web/src/lib/integrations/github/multiple-installations.ts +++ b/apps/web/src/lib/integrations/github/multiple-installations.ts @@ -1,8 +1,26 @@ -const MULTIPLE_GITHUB_INSTALLATION_ORGANIZATION_IDS = new Set([ - '9d278969-5453-4ae3-a51f-a8d2274a7b56', - '30f1620a-4aad-4456-bf4d-550f335e6f55', -]); +import 'server-only'; + +import { z } from 'zod'; +import { getEnvVariable } from '@/lib/dotenvx'; + +export function parseMultipleGitHubInstallationOrganizationIds(value: string): Set { + const organizationIds = value + .split(',') + .map(organizationId => organizationId.trim()) + .filter(Boolean); + + const result = z.array(z.uuid()).safeParse(organizationIds); + if (!result.success) { + throw new Error( + 'GITHUB_MULTIPLE_INSTALLATION_ORGANIZATION_IDS must be a comma-separated list of UUIDs' + ); + } + + return new Set(result.data); +} export function canOrganizationUseMultipleGitHubInstallations(organizationId: string): boolean { - return MULTIPLE_GITHUB_INSTALLATION_ORGANIZATION_IDS.has(organizationId); + return parseMultipleGitHubInstallationOrganizationIds( + getEnvVariable('GITHUB_MULTIPLE_INSTALLATION_ORGANIZATION_IDS') + ).has(organizationId); } diff --git a/apps/web/src/routers/github-apps-router.test.ts b/apps/web/src/routers/github-apps-router.test.ts index f4f7e94faf..82379a415a 100644 --- a/apps/web/src/routers/github-apps-router.test.ts +++ b/apps/web/src/routers/github-apps-router.test.ts @@ -6,6 +6,7 @@ import type { Owner } from '@/lib/integrations/core/types'; import type { GitHubAppType } from '@/lib/integrations/platforms/github/app-selector'; import type { UpsertPlatformIntegrationResult } from '@/lib/integrations/db/platform-integrations'; import type { OrganizationRole } from '@/lib/organizations/organization-types'; +import { getEnvVariable } from '@/lib/dotenvx'; type TestIntegration = { id: string; @@ -105,6 +106,12 @@ jest.mock('@/lib/github-pr-review/dev-seed', () => ({ seedUserGithubToken: (...args: [Record]) => mockSeedUserGithubToken(...args), })); +jest.mock('@/lib/dotenvx', () => ({ + getEnvVariable: jest.fn(), +})); + +const mockGetEnvVariable = jest.mocked(getEnvVariable); + let createCaller: (ctx: { user: User }) => { listOrganizationInstallations: (input: { organizationId: string }) => Promise<{ canAdd: boolean; @@ -173,6 +180,9 @@ function organizationIntegration(): PlatformIntegration { describe('githubAppsRouter organization install capability', () => { beforeEach(() => { jest.clearAllMocks(); + mockGetEnvVariable.mockReturnValue( + '9d278969-5453-4ae3-a51f-a8d2274a7b56,30f1620a-4aad-4456-bf4d-550f335e6f55' + ); mockEnsureOrganizationAccess.mockResolvedValue('member'); mockGetGitHubAppTypeForOrganization.mockResolvedValue('standard'); mockCreateInstallState.mockResolvedValue('install-token'); From 6545c3b53a197f2a6f47c3eb8a5619d12310e6c1 Mon Sep 17 00:00:00 2001 From: Remon Oldenbeuving Date: Fri, 28 Aug 2026 12:09:09 +0200 Subject: [PATCH 3/3] test(github): configure multi-install allowlist via env --- apps/web/src/routers/github-apps-router.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/web/src/routers/github-apps-router.test.ts b/apps/web/src/routers/github-apps-router.test.ts index 82379a415a..6530ed6a80 100644 --- a/apps/web/src/routers/github-apps-router.test.ts +++ b/apps/web/src/routers/github-apps-router.test.ts @@ -6,7 +6,6 @@ import type { Owner } from '@/lib/integrations/core/types'; import type { GitHubAppType } from '@/lib/integrations/platforms/github/app-selector'; import type { UpsertPlatformIntegrationResult } from '@/lib/integrations/db/platform-integrations'; import type { OrganizationRole } from '@/lib/organizations/organization-types'; -import { getEnvVariable } from '@/lib/dotenvx'; type TestIntegration = { id: string; @@ -106,12 +105,6 @@ jest.mock('@/lib/github-pr-review/dev-seed', () => ({ seedUserGithubToken: (...args: [Record]) => mockSeedUserGithubToken(...args), })); -jest.mock('@/lib/dotenvx', () => ({ - getEnvVariable: jest.fn(), -})); - -const mockGetEnvVariable = jest.mocked(getEnvVariable); - let createCaller: (ctx: { user: User }) => { listOrganizationInstallations: (input: { organizationId: string }) => Promise<{ canAdd: boolean; @@ -180,9 +173,8 @@ function organizationIntegration(): PlatformIntegration { describe('githubAppsRouter organization install capability', () => { beforeEach(() => { jest.clearAllMocks(); - mockGetEnvVariable.mockReturnValue( - '9d278969-5453-4ae3-a51f-a8d2274a7b56,30f1620a-4aad-4456-bf4d-550f335e6f55' - ); + process.env.GITHUB_MULTIPLE_INSTALLATION_ORGANIZATION_IDS = + '9d278969-5453-4ae3-a51f-a8d2274a7b56,30f1620a-4aad-4456-bf4d-550f335e6f55'; mockEnsureOrganizationAccess.mockResolvedValue('member'); mockGetGitHubAppTypeForOrganization.mockResolvedValue('standard'); mockCreateInstallState.mockResolvedValue('install-token');