Skip to content

Commit f634d56

Browse files
committed
test: add authentication functions and tests for GitHub App integration in code scanning coverage report
1 parent d16195b commit f634d56

File tree

2 files changed

+179
-2
lines changed

2 files changed

+179
-2
lines changed

scripts/code-scanning-coverage-report/code-scanning-coverage-report.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1086,5 +1086,12 @@ export {
10861086
generateCSV,
10871087
generateSubReports,
10881088
CODEQL_LANGUAGES,
1089-
LANGUAGE_NORMALIZE
1089+
LANGUAGE_NORMALIZE,
1090+
// Auth functions
1091+
isGitHubAppAuth,
1092+
getInstallationIdForOrg,
1093+
createOctokitForOrg,
1094+
createTokenOctokit,
1095+
createAppOctokit,
1096+
createInstallationOctokit
10901097
};

scripts/code-scanning-coverage-report/code-scanning-coverage-report.test.js

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
generateCSV,
1515
generateSubReports,
1616
CODEQL_LANGUAGES,
17-
LANGUAGE_NORMALIZE
17+
LANGUAGE_NORMALIZE,
18+
isGitHubAppAuth,
19+
getInstallationIdForOrg
1820
} from './code-scanning-coverage-report.js';
1921
import fs from 'fs';
2022

@@ -1057,3 +1059,171 @@ describe('Constants', () => {
10571059
expect(LANGUAGE_NORMALIZE['cpp']).toBe('c-cpp');
10581060
});
10591061
});
1062+
1063+
// ============================================================================
1064+
// Authentication Tests
1065+
// ============================================================================
1066+
1067+
describe('isGitHubAppAuth', () => {
1068+
const originalEnv = process.env;
1069+
1070+
beforeEach(() => {
1071+
jest.resetModules();
1072+
process.env = { ...originalEnv };
1073+
});
1074+
1075+
afterAll(() => {
1076+
process.env = originalEnv;
1077+
});
1078+
1079+
// Note: isGitHubAppAuth reads from module-level constants that are set at import time,
1080+
// so we test the function's logic indirectly by checking the current state
1081+
test('returns false when GITHUB_APP_ID is not set', () => {
1082+
// The function checks the module-level appId and privateKeyPath constants
1083+
// Since these are set at module load time, we test the current state
1084+
// In a real scenario without env vars set, this should return false
1085+
const result = isGitHubAppAuth();
1086+
// The result depends on whether env vars were set when the module loaded
1087+
expect(typeof result).toBe('boolean');
1088+
});
1089+
});
1090+
1091+
describe('getInstallationIdForOrg', () => {
1092+
test('returns installation ID when app is installed on org', async () => {
1093+
const mockAppOctokit = {
1094+
rest: {
1095+
apps: {
1096+
getOrgInstallation: jest.fn().mockResolvedValue({
1097+
data: { id: 12345678 }
1098+
})
1099+
}
1100+
}
1101+
};
1102+
1103+
const installationId = await getInstallationIdForOrg(mockAppOctokit, 'test-org');
1104+
expect(installationId).toBe(12345678);
1105+
expect(mockAppOctokit.rest.apps.getOrgInstallation).toHaveBeenCalledWith({ org: 'test-org' });
1106+
});
1107+
1108+
test('throws error when app is not installed on org (404)', async () => {
1109+
const mockAppOctokit = {
1110+
rest: {
1111+
apps: {
1112+
getOrgInstallation: jest.fn().mockRejectedValue({ status: 404 })
1113+
}
1114+
}
1115+
};
1116+
1117+
await expect(getInstallationIdForOrg(mockAppOctokit, 'uninstalled-org'))
1118+
.rejects.toThrow('GitHub App is not installed on organization: uninstalled-org');
1119+
});
1120+
1121+
test('re-throws other errors', async () => {
1122+
const mockAppOctokit = {
1123+
rest: {
1124+
apps: {
1125+
getOrgInstallation: jest.fn().mockRejectedValue(new Error('Network error'))
1126+
}
1127+
}
1128+
};
1129+
1130+
await expect(getInstallationIdForOrg(mockAppOctokit, 'test-org'))
1131+
.rejects.toThrow('Network error');
1132+
});
1133+
1134+
test('handles multiple orgs with different installation IDs', async () => {
1135+
const mockAppOctokit = {
1136+
rest: {
1137+
apps: {
1138+
getOrgInstallation: jest.fn()
1139+
.mockImplementation(({ org }) => {
1140+
const installations = {
1141+
'org-one': { data: { id: 11111111 } },
1142+
'org-two': { data: { id: 22222222 } },
1143+
'org-three': { data: { id: 33333333 } }
1144+
};
1145+
if (installations[org]) {
1146+
return Promise.resolve(installations[org]);
1147+
}
1148+
return Promise.reject({ status: 404 });
1149+
})
1150+
}
1151+
}
1152+
};
1153+
1154+
const id1 = await getInstallationIdForOrg(mockAppOctokit, 'org-one');
1155+
const id2 = await getInstallationIdForOrg(mockAppOctokit, 'org-two');
1156+
const id3 = await getInstallationIdForOrg(mockAppOctokit, 'org-three');
1157+
1158+
expect(id1).toBe(11111111);
1159+
expect(id2).toBe(22222222);
1160+
expect(id3).toBe(33333333);
1161+
1162+
// Verify each org was called
1163+
expect(mockAppOctokit.rest.apps.getOrgInstallation).toHaveBeenCalledTimes(3);
1164+
});
1165+
});
1166+
1167+
describe('Authentication scenarios', () => {
1168+
test('multi-org scenario: each org gets its own installation lookup', async () => {
1169+
const orgs = ['org-alpha', 'org-beta', 'org-gamma'];
1170+
const callLog = [];
1171+
1172+
const mockAppOctokit = {
1173+
rest: {
1174+
apps: {
1175+
getOrgInstallation: jest.fn().mockImplementation(({ org }) => {
1176+
callLog.push(org);
1177+
return Promise.resolve({ data: { id: Math.random() } });
1178+
})
1179+
}
1180+
}
1181+
};
1182+
1183+
// Simulate what main() does for multi-org
1184+
for (const org of orgs) {
1185+
await getInstallationIdForOrg(mockAppOctokit, org);
1186+
}
1187+
1188+
expect(callLog).toEqual(['org-alpha', 'org-beta', 'org-gamma']);
1189+
expect(mockAppOctokit.rest.apps.getOrgInstallation).toHaveBeenCalledTimes(3);
1190+
});
1191+
1192+
test('single org scenario: only one installation lookup needed', async () => {
1193+
const mockAppOctokit = {
1194+
rest: {
1195+
apps: {
1196+
getOrgInstallation: jest.fn().mockResolvedValue({ data: { id: 99999999 } })
1197+
}
1198+
}
1199+
};
1200+
1201+
const installationId = await getInstallationIdForOrg(mockAppOctokit, 'single-org');
1202+
1203+
expect(installationId).toBe(99999999);
1204+
expect(mockAppOctokit.rest.apps.getOrgInstallation).toHaveBeenCalledTimes(1);
1205+
});
1206+
1207+
test('partial failure: some orgs have app installed, some do not', async () => {
1208+
const mockAppOctokit = {
1209+
rest: {
1210+
apps: {
1211+
getOrgInstallation: jest.fn().mockImplementation(({ org }) => {
1212+
if (org === 'org-with-app') {
1213+
return Promise.resolve({ data: { id: 12345 } });
1214+
}
1215+
return Promise.reject({ status: 404 });
1216+
})
1217+
}
1218+
}
1219+
};
1220+
1221+
// First org succeeds
1222+
const id = await getInstallationIdForOrg(mockAppOctokit, 'org-with-app');
1223+
expect(id).toBe(12345);
1224+
1225+
// Second org fails
1226+
await expect(getInstallationIdForOrg(mockAppOctokit, 'org-without-app'))
1227+
.rejects.toThrow('GitHub App is not installed on organization: org-without-app');
1228+
});
1229+
});

0 commit comments

Comments
 (0)