diff --git a/.gitignore b/.gitignore index 5b12744..2d106ce 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ what.json package-lock.json out.txt junit.xml +*.env* ## https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore diff --git a/jest.config.js b/jest.config.js index 13f53e1..dfe15a8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,5 +4,8 @@ module.exports = { transform: { "^.+.tsx?$": ["ts-jest",{}], }, - collectCoverageFrom: ['src/**/*.ts'] + collectCoverageFrom: ['src/**/*.ts'], + setupFiles: [ + "dotenv/config" + ] }; \ No newline at end of file diff --git a/package.json b/package.json index ca2448d..754ff10 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "main": "app.ts", "module": "es2015", "scripts": { - "test": "jest --coverage --reporters=jest-junit", + "test:unit": "jest test/unit --coverage --reporters=jest-junit", + "test:int": "jest test/integration", + "test:all": "jest --coverage --reporters=jest-junit", + "test":"npm run test:unit", "dev": "nodemon", "start": "node ./out/app.js", "build": "tsc && shx cp ./src/openapi.yaml ./out/openapi.yaml", diff --git a/src/services/gitHub.ts b/src/services/gitHub.ts index 90dce36..ca850fe 100644 --- a/src/services/gitHub.ts +++ b/src/services/gitHub.ts @@ -1,37 +1,17 @@ import { Octokit } from "octokit"; import { createAppAuth } from "@octokit/auth-app"; import { Config } from "../config"; -import { AddMemberResponse, CopilotAddResponse, GitHubClient, GitHubId, GitHubTeamId, InstalledClient, Org, OrgConfigResponse, OrgInvite, OrgRoles, RemoveMemberResponse, Response } from "./gitHubTypes"; +import { GitHubClient, InstalledClient, Org } from "./gitHubTypes"; import { AppConfig } from "./appConfig"; import yaml from "js-yaml"; import { throttling } from "@octokit/plugin-throttling"; -import { AsyncReturnType } from "../utility"; -import { Log, LogError, LoggerToUse } from "../logging"; +import { Log, LoggerToUse } from "../logging"; import { GitHubClientCache } from "./gitHubCache"; import { redisClient } from "../app"; -import { GitHubTeamName, OrgConfig, OrgConfigurationOptions } from "./orgConfig"; +import { InstalledGitHubClient } from "./installedGitHubClient"; const config = Config(); -// TODO: split into decorator so as to not mix responsibilities -function MakeTeamNameSafe(teamName: string) { - // There are most likely much more than this... - const specialCharacterRemoveRegexp = /[ &%#@!$]/g; - const saferName = teamName.replaceAll(specialCharacterRemoveRegexp, '-'); - - const multiReplaceRegexp = /(-){2,}/g; - const removeTrailingDashesRegexp = /-+$/g - - const withDuplicatesRemoved = saferName.replaceAll(multiReplaceRegexp, "-").replaceAll(removeTrailingDashesRegexp, ""); - - return withDuplicatesRemoved; -} - -// TODO: split into decorator so as to not mix responsibilities -function MakeTeamNameSafeAndApiFriendly(teamName: string) { - return MakeTeamNameSafe(teamName).replace(" ", "-"); -} - async function GetOrgClient(installationId: number): Promise { // TODO: look further into this... it seems like it would be best if // installation client was generated from the original client, and not @@ -220,489 +200,3 @@ export function GetClient(): GitHubClient { } } - -class InstalledGitHubClient implements InstalledClient { - gitHubClient: Octokit; - orgName: string; - - constructor(gitHubClient: Octokit, orgName: string) { - this.gitHubClient = gitHubClient; - this.orgName = orgName; - } - - async AddTeamsToCopilotSubscription(teamNames: string[]): Response { - // Such logic should not generally go in a facade, though the convenience - // and lack of actual problems makes this violation of pattern more "okay." - if (teamNames.length < 1) { - return { - // Should be "no op" - successful: true, - data: [] - } - } - - const responses: CopilotAddResponse[] = []; - - for (const team of teamNames) { - try { - const response = await this.gitHubClient.request("POST /orgs/{org}/copilot/billing/selected_teams", { - org: this.orgName, - selected_teams: [team], - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }); - - if (response.status < 200 || response.status > 299) { - responses.push({ - successful: false, - team: team, - message: response.status.toString() - }); - } - - responses.push({ - successful: true, - team: team - }); - } - catch (e) { - console.log(e); - responses.push({ - successful: false, - team: team, - message: JSON.stringify(e) - }); - } - } - - return { - successful: true, - data: responses - }; - } - - async ListPendingInvitesForTeam(teamName: string): Response { - const safeName = MakeTeamNameSafeAndApiFriendly(teamName); - - const response = await this.gitHubClient.rest.teams.listPendingInvitationsInOrg({ - org: this.orgName, - team_slug: safeName - }) - - if (response.status < 200 || response.status > 299) { - return { - successful: false - } - } - - return { - successful: true, - data: response.data.map(i => { - return { - GitHubUser: i.login!, - InviteId: i.id! - } - }) - } - } - - async CancelOrgInvite(invite: OrgInvite): Response { - const response = await this.gitHubClient.rest.orgs.cancelInvitation({ - invitation_id: invite.InviteId, - org: this.orgName - }) - - if (response.status < 200 || response.status > 299) { - return { - successful: false - } - } - - return { - successful: true, - data: null - } - } - - async GetPendingOrgInvites(): Response { - const response = await this.gitHubClient.paginate(this.gitHubClient.rest.orgs.listPendingInvitations, { - org: this.orgName, - role: "all", - headers: { - "x-github-api-version": "2022-11-28" - } - }) - - return { - successful: true, - data: (response)?.map(d => { - return { - InviteId: d.id, - GitHubUser: d.login! - } - }) ?? [] - } - } - - public async SetOrgRole(id: GitHubId, role: OrgRoles): Response { - const response = await this.gitHubClient.rest.orgs.setMembershipForUser({ - org: this.orgName, - username: id, - role: role - }) - - if (response.status > 200 && response.status < 300) { - return { - successful: true, - data: null - } - } - - return { - successful: false - } - } - - public GetCurrentOrgName(): string { - return this.orgName; - } - - public async GetCurrentRateLimit(): Promise<{ remaining: number; }> { - const limits = await this.gitHubClient.rest.rateLimit.get(); - - return { - remaining: limits.data.rate.remaining - } - } - - public async AddOrgMember(id: string): Response { - try { - const response = await this.gitHubClient.rest.orgs.setMembershipForUser({ - org: this.orgName, - username: id - }) - - if (response.status != 200) { - return { - successful: false - } - } - - return { - successful: false - } - } - catch (e) { - LogError(`Error adding Org Member ${id}: ${JSON.stringify(e)}`) - - return { - successful: false - } - } - } - - public async IsUserMember(id: string): Response { - try { - await this.gitHubClient.rest.orgs.checkMembershipForUser({ - org: this.orgName, - username: id - }) - } - catch { - // TODO: actually catch exception and investigate... - // not all exceptions could mean that the user is not a member - return { - successful: true, - data: false - } - } - - return { - successful: true, - data: true - } - } - - public async GetAllTeams(): Response { - const response = await this.gitHubClient.paginate(this.gitHubClient.rest.teams.list, { - org: this.orgName - }) - - const teams = response.map(i => { - return { - Id: i.id, - Name: i.name - } - }); - - return { - successful: true, - data: teams - } - } - - public async AddTeamMember(team: GitHubTeamName, id: GitHubId): AddMemberResponse { - const safeTeam = MakeTeamNameSafeAndApiFriendly(team); - - try { - await this.gitHubClient.rest.teams.addOrUpdateMembershipForUserInOrg({ - org: this.orgName, - team_slug: safeTeam, - username: id - }) - - return { - successful: true, - team: team, - user: id - } - } - catch (e) { - if (e instanceof Error) { - return { - successful: false, - team: team, - user: id, - message: e.message - } - } - - return { - successful: false, - team: team, - user: id, - message: JSON.stringify(e) - } - } - } - - public async CreateTeam(team: GitHubTeamName, description: string): Response { - try { - // TODO: submit bug for the method I was using because - // it always creates a team with '-' instead of spaces... - // this is NOT an opinion for a client library to make! - await this.gitHubClient.request('POST /orgs/{org}/teams', { - org: this.orgName, - name: team, - description: description, - // TODO: enable configuration of this item - notification_setting: 'notifications_enabled', - // TODO: enable configuration of this item - privacy: 'closed', - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }); - } - catch { - return { - successful: false - } - } - - return { - successful: true, - // TODO: make this type better to avoid nulls... - data: null - } - } - - public async DoesUserExist(gitHubId: string): Response { - try { - const response = await this.gitHubClient.rest.users.getByUsername({ - username: gitHubId - }) - - return { - successful: true, - data: response.data.login - } - } - catch { - return { - successful: false - } - } - } - - public async ListCurrentMembersOfGitHubTeam(team: GitHubTeamName): Response { - const safeTeam = MakeTeamNameSafeAndApiFriendly(team); - - try { - const response = await this.gitHubClient.paginate(this.gitHubClient.rest.teams.listMembersInOrg, { - org: this.orgName, - team_slug: safeTeam, - }) - - return { - successful: true, - data: response.map(i => { - return i.login - }) - } - } - catch { - return { - successful: false - } - } - } - - public async RemoveTeamMemberAsync(team: GitHubTeamName, user: GitHubId): RemoveMemberResponse { - const safeTeam = MakeTeamNameSafeAndApiFriendly(team); - - try { - await this.gitHubClient.rest.teams.removeMembershipForUserInOrg({ - team_slug: safeTeam, - org: this.orgName, - username: user - }) - - return { - successful: true, - team: team, - user: user - } - } - catch (e) { - if (e instanceof Error) { - return { - successful: false, - team: team, - user: user, - message: e.message - } - } - - return { - successful: false, - team: team, - user: user, - message: JSON.stringify(e) - } - } - } - - public async UpdateTeamDetails(team: GitHubTeamName, description: string): Response { - try { - await this.gitHubClient.rest.teams.updateInOrg({ - org: this.orgName, - privacy: "closed", - team_slug: MakeTeamNameSafeAndApiFriendly(team), - name: team, - description: description - }) - - return { - successful: true, - // TODO: make this type better to avoid nulls... - data: null - } - } - catch { - return { - successful: false - } - } - } - - public async AddSecurityManagerTeam(team: GitHubTeamName) { - const safeTeam = MakeTeamNameSafeAndApiFriendly(team); - - try { - await this.gitHubClient.rest.orgs.addSecurityManagerTeam({ - org: this.orgName, - team_slug: safeTeam - }) - return true; - } - catch { - Log(`Error adding ${team} as Security Managers for Org ${this.orgName}.`) - return false; - } - - } - - public async GetConfigurationForInstallation(): OrgConfigResponse { - // TODO: this function doesn't really belong on this class... - // i.e., it doesn't fit with a "GitHub Facade" - const getContentRequest = { - owner: this.orgName, - repo: ".github", - path: "" - }; - - let filesResponse: AsyncReturnType; - - try { - filesResponse = await this.gitHubClient.rest.repos.getContent(getContentRequest); - } - catch { - return { - successful: false, - state: "NoConfig" - } - } - - const potentialFiles = filesResponse.data; - - if (!Array.isArray(potentialFiles)) { - return { - successful: false, - state: "NoConfig" - } - } - - const onlyConfigFiles = potentialFiles - .filter(i => i.type == "file") - .filter(i => i.name == "team-sync-options.yml" || i.name == "team-sync-options.yaml"); - - if (onlyConfigFiles.length > 1) { - return { - successful: false, - state: "BadConfig", - message: "Multiple configuration files are not supported at this point in time." - } - } - - if(onlyConfigFiles.length < 1) { - return { - successful: false, - state: "NoConfig", - message: "No configuration file exists in the configuration repository (typically the .github repository)." - } - } - - const onlyFile = onlyConfigFiles[0]; - - const contentResponse = await this.gitHubClient.rest.repos.getContent({ - ...getContentRequest, - path: onlyFile.name - }) - - const contentData = contentResponse.data; - - if (Array.isArray(contentData) || contentData.type != "file") { - return { - successful: false, - state: "BadConfig" - } - } - - try { - const configuration = yaml.load(Buffer.from(contentData.content, 'base64').toString()) as OrgConfigurationOptions; - return { - successful: true, - data: new OrgConfig(configuration) - } - } - catch { - return { - successful: false, - state: "BadConfig", - message: "Error parsing configuration- check configuration file for validity: https://github.com/cloudpups/github-teams-user-sync/blob/main/docs/OrganizationConfiguration.md" - } - } - } -} \ No newline at end of file diff --git a/src/services/installedGitHubClient.ts b/src/services/installedGitHubClient.ts new file mode 100644 index 0000000..a2e161f --- /dev/null +++ b/src/services/installedGitHubClient.ts @@ -0,0 +1,492 @@ +import { Octokit } from "octokit"; +import { AddMemberResponse, CopilotAddResponse, GitHubClient, GitHubId, GitHubTeamId, InstalledClient, Org, OrgConfigResponse, OrgInvite, OrgRoles, RemoveMemberResponse, Response } from "./gitHubTypes"; +import yaml from "js-yaml"; +import { AsyncReturnType, MakeTeamNameSafeAndApiFriendly } from "../utility"; +import { Log, LogError } from "../logging"; +import { GitHubTeamName, OrgConfig, OrgConfigurationOptions } from "./orgConfig"; + +export class InstalledGitHubClient implements InstalledClient { + gitHubClient: Octokit; + orgName: string; + + constructor(gitHubClient: Octokit, orgName: string) { + this.gitHubClient = gitHubClient; + this.orgName = orgName; + } + + async AddTeamsToCopilotSubscription(teamNames: string[]): Response { + // Such logic should not generally go in a facade, though the convenience + // and lack of actual problems makes this violation of pattern more "okay." + if (teamNames.length < 1) { + return { + // Should be "no op" + successful: true, + data: [] + } + } + + const responses: CopilotAddResponse[] = []; + + for (const team of teamNames) { + try { + const response = await this.gitHubClient.request("POST /orgs/{org}/copilot/billing/selected_teams", { + org: this.orgName, + selected_teams: [team], + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + + if (response.status < 200 || response.status > 299) { + responses.push({ + successful: false, + team: team, + message: response.status.toString() + }); + } + + responses.push({ + successful: true, + team: team + }); + } + catch (e) { + console.log(e); + responses.push({ + successful: false, + team: team, + message: JSON.stringify(e) + }); + } + } + + return { + successful: true, + data: responses + }; + } + + async ListPendingInvitesForTeam(teamName: string): Response { + const safeName = MakeTeamNameSafeAndApiFriendly(teamName); + + const response = await this.gitHubClient.rest.teams.listPendingInvitationsInOrg({ + org: this.orgName, + team_slug: safeName + }) + + if (response.status < 200 || response.status > 299) { + return { + successful: false + } + } + + return { + successful: true, + data: response.data.map(i => { + return { + GitHubUser: i.login!, + InviteId: i.id! + } + }) + } + } + + async CancelOrgInvite(invite: OrgInvite): Response { + const response = await this.gitHubClient.rest.orgs.cancelInvitation({ + invitation_id: invite.InviteId, + org: this.orgName + }) + + if (response.status < 200 || response.status > 299) { + return { + successful: false + } + } + + return { + successful: true, + data: null + } + } + + async GetPendingOrgInvites(): Response { + const response = await this.gitHubClient.paginate(this.gitHubClient.rest.orgs.listPendingInvitations, { + org: this.orgName, + role: "all", + headers: { + "x-github-api-version": "2022-11-28" + } + }) + + return { + successful: true, + data: (response)?.map(d => { + return { + InviteId: d.id, + GitHubUser: d.login! + } + }) ?? [] + } + } + + public async SetOrgRole(id: GitHubId, role: OrgRoles): Response { + const response = await this.gitHubClient.rest.orgs.setMembershipForUser({ + org: this.orgName, + username: id, + role: role + }) + + if (response.status > 200 && response.status < 300) { + return { + successful: true, + data: null + } + } + + return { + successful: false + } + } + + public GetCurrentOrgName(): string { + return this.orgName; + } + + public async GetCurrentRateLimit(): Promise<{ remaining: number; }> { + const limits = await this.gitHubClient.rest.rateLimit.get(); + + return { + remaining: limits.data.rate.remaining + } + } + + public async AddOrgMember(id: string): Response { + try { + const response = await this.gitHubClient.rest.orgs.setMembershipForUser({ + org: this.orgName, + username: id + }) + + if (response.status != 200) { + return { + successful: false + } + } + + return { + successful: false + } + } + catch (e) { + LogError(`Error adding Org Member ${id}: ${JSON.stringify(e)}`) + + return { + successful: false + } + } + } + + public async IsUserMember(id: string): Response { + try { + await this.gitHubClient.rest.orgs.checkMembershipForUser({ + org: this.orgName, + username: id + }) + } + catch { + // TODO: actually catch exception and investigate... + // not all exceptions could mean that the user is not a member + return { + successful: true, + data: false + } + } + + return { + successful: true, + data: true + } + } + + public async GetAllTeams(): Response { + const response = await this.gitHubClient.paginate(this.gitHubClient.rest.teams.list, { + org: this.orgName + }) + + const teams = response.map(i => { + return { + Id: i.id, + Name: i.name + } + }); + + return { + successful: true, + data: teams + } + } + + public async AddTeamMember(team: GitHubTeamName, id: GitHubId): AddMemberResponse { + const safeTeam = MakeTeamNameSafeAndApiFriendly(team); + + try { + await this.gitHubClient.rest.teams.addOrUpdateMembershipForUserInOrg({ + org: this.orgName, + team_slug: safeTeam, + username: id + }) + + return { + successful: true, + team: team, + user: id + } + } + catch (e) { + if (e instanceof Error) { + return { + successful: false, + team: team, + user: id, + message: e.message + } + } + + return { + successful: false, + team: team, + user: id, + message: JSON.stringify(e) + } + } + } + + public async CreateTeam(team: GitHubTeamName, description: string): Response { + try { + // TODO: submit bug for the method I was using because + // it always creates a team with '-' instead of spaces... + // this is NOT an opinion for a client library to make! + await this.gitHubClient.request('POST /orgs/{org}/teams', { + org: this.orgName, + name: team, + description: description, + // TODO: enable configuration of this item + notification_setting: 'notifications_enabled', + // TODO: enable configuration of this item + privacy: 'closed', + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }); + } + catch { + return { + successful: false + } + } + + return { + successful: true, + // TODO: make this type better to avoid nulls... + data: null + } + } + + public async DoesUserExist(gitHubId: string): Response { + try { + const response = await this.gitHubClient.rest.users.getByUsername({ + username: gitHubId + }) + + return { + successful: true, + data: response.data.login + } + } + catch { + return { + successful: false + } + } + } + + public async ListCurrentMembersOfGitHubTeam(team: GitHubTeamName): Response { + const safeTeam = MakeTeamNameSafeAndApiFriendly(team); + + try { + const response = await this.gitHubClient.paginate(this.gitHubClient.rest.teams.listMembersInOrg, { + org: this.orgName, + team_slug: safeTeam, + }) + + return { + successful: true, + data: response.map(i => { + return i.login + }) + } + } + catch { + return { + successful: false + } + } + } + + public async RemoveTeamMemberAsync(team: GitHubTeamName, user: GitHubId): RemoveMemberResponse { + const safeTeam = MakeTeamNameSafeAndApiFriendly(team); + + try { + await this.gitHubClient.rest.teams.removeMembershipForUserInOrg({ + team_slug: safeTeam, + org: this.orgName, + username: user + }) + + return { + successful: true, + team: team, + user: user + } + } + catch (e) { + if (e instanceof Error) { + return { + successful: false, + team: team, + user: user, + message: e.message + } + } + + return { + successful: false, + team: team, + user: user, + message: JSON.stringify(e) + } + } + } + + public async UpdateTeamDetails(team: GitHubTeamName, description: string): Response { + try { + await this.gitHubClient.rest.teams.updateInOrg({ + org: this.orgName, + privacy: "closed", + team_slug: MakeTeamNameSafeAndApiFriendly(team), + name: team, + description: description + }) + + return { + successful: true, + // TODO: make this type better to avoid nulls... + data: null + } + } + catch { + return { + successful: false + } + } + } + + public async AddSecurityManagerTeam(team: GitHubTeamName) { + const safeTeam = MakeTeamNameSafeAndApiFriendly(team); + + try { + await this.gitHubClient.rest.orgs.addSecurityManagerTeam({ + org: this.orgName, + team_slug: safeTeam + }) + return true; + } + catch { + Log(`Error adding ${team} as Security Managers for Org ${this.orgName}.`) + return false; + } + + } + + public async GetConfigurationForInstallation(): OrgConfigResponse { + // TODO: this function doesn't really belong on this class... + // i.e., it doesn't fit with a "GitHub Facade" + const getContentRequest = { + owner: this.orgName, + repo: ".github", + path: "" + }; + + let filesResponse: AsyncReturnType; + + try { + filesResponse = await this.gitHubClient.rest.repos.getContent(getContentRequest); + } + catch { + return { + successful: false, + state: "NoConfig" + } + } + + const potentialFiles = filesResponse.data; + + if (!Array.isArray(potentialFiles)) { + return { + successful: false, + state: "NoConfig" + } + } + + const onlyConfigFiles = potentialFiles + .filter(i => i.type == "file") + .filter(i => i.name == "team-sync-options.yml" || i.name == "team-sync-options.yaml"); + + if (onlyConfigFiles.length > 1) { + return { + successful: false, + state: "BadConfig", + message: "Multiple configuration files are not supported at this point in time." + } + } + + if(onlyConfigFiles.length < 1) { + return { + successful: false, + state: "NoConfig", + message: "No configuration file exists in the configuration repository (typically the .github repository)." + } + } + + const onlyFile = onlyConfigFiles[0]; + + const contentResponse = await this.gitHubClient.rest.repos.getContent({ + ...getContentRequest, + path: onlyFile.name + }) + + const contentData = contentResponse.data; + + if (Array.isArray(contentData) || contentData.type != "file") { + return { + successful: false, + state: "BadConfig" + } + } + + try { + const configuration = yaml.load(Buffer.from(contentData.content, 'base64').toString()) as OrgConfigurationOptions; + return { + successful: true, + data: new OrgConfig(configuration) + } + } + catch { + return { + successful: false, + state: "BadConfig", + message: "Error parsing configuration- check configuration file for validity: https://github.com/cloudpups/github-teams-user-sync/blob/main/docs/OrganizationConfiguration.md" + } + } + } +} \ No newline at end of file diff --git a/src/utility.ts b/src/utility.ts index 205e292..1525f90 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -1,2 +1,21 @@ export type AsyncReturnType Promise> = - T extends (...args: any) => Promise ? R : any \ No newline at end of file + T extends (...args: any) => Promise ? R : any; + +// TODO: split into decorator so as to not mix responsibilities +export function MakeTeamNameSafeAndApiFriendly(teamName: string) { + return MakeTeamNameSafe(teamName).replace(" ", "-"); +} + +// TODO: split into decorator so as to not mix responsibilities +function MakeTeamNameSafe(teamName: string) { + // There are most likely much more than this... + const specialCharacterRemoveRegexp = /[ &%#@!$]/g; + const saferName = teamName.replaceAll(specialCharacterRemoveRegexp, '-'); + + const multiReplaceRegexp = /(-){2,}/g; + const removeTrailingDashesRegexp = /-+$/g + + const withDuplicatesRemoved = saferName.replaceAll(multiReplaceRegexp, "-").replaceAll(removeTrailingDashesRegexp, ""); + + return withDuplicatesRemoved; +} \ No newline at end of file diff --git a/test/integration/README.md b/test/integration/README.md new file mode 100644 index 0000000..7dc6ec1 --- /dev/null +++ b/test/integration/README.md @@ -0,0 +1,18 @@ +# Integration Tests + +For the tests in this folder to run, a .env file must be present in the root of the repo so that a GitHub Client can be initialized for testing against GitHub's actual APIs. + +The `.env` file must be located at the following path, and must have the following shape: + +## Path + +Path: `tests/integrations/.env` + +## Shape + +```.env +GitHubApp__AppId={number} +Tests__InstallationId={number} +Tests__OrgName={string} +GitHubApp__PrivateKey={multi-line-string} +``` \ No newline at end of file diff --git a/test/integration/installedGitHubClient.test.ts b/test/integration/installedGitHubClient.test.ts new file mode 100644 index 0000000..5bec8ca --- /dev/null +++ b/test/integration/installedGitHubClient.test.ts @@ -0,0 +1,53 @@ +import {beforeAll, describe, expect, test} from '@jest/globals'; +import { Octokit } from 'octokit'; +import { createAppAuth } from '@octokit/auth-app'; +import dotenv from 'dotenv'; +import { InstalledGitHubClient } from '../../src/services/installedGitHubClient'; + +describe('InstalledGitHubClient Class', () => { + let appConfig:{ + installationId: number, + appId: number, + privateKey: string, + orgName: string + }; + + beforeAll(() => { + dotenv.config({ + // See `tests/integration/README.md` for more information + path:"test/integration/.env.sync-bot.tests" + }) + + appConfig = { + appId: process.env.GitHubApp__AppId! as unknown as number, + installationId: process.env.Tests__InstallationId! as unknown as number, + privateKey: process.env.GitHubApp__PrivateKey!, + orgName: process.env.Tests__OrgName! + } + }); + + test('ListCurrentMembersOfGitHubTeam returns expected team members', async () => { + // Arrange + const octokit = new Octokit({ + authStrategy: createAppAuth, + auth: appConfig + }); + + const installedGitHubClient = new InstalledGitHubClient(octokit, appConfig.orgName); + + // TODO: extract out test team + const testTeam = "Team1" + + // Act + const response = await installedGitHubClient.ListCurrentMembersOfGitHubTeam(testTeam); + + const actualMembers = response.successful ? response.data : []; + + // Assert + expect(response.successful).toBeTruthy(); + expect(actualMembers).toHaveLength(1); + + // TODO: extract out test username + expect(actualMembers[0]).toEqual("JoshuaTheMiller"); + }); +}); \ No newline at end of file diff --git a/tests/orgConfig.test.ts b/test/unit/orgConfig.test.ts similarity index 99% rename from tests/orgConfig.test.ts rename to test/unit/orgConfig.test.ts index d3828f1..e0896d3 100644 --- a/tests/orgConfig.test.ts +++ b/test/unit/orgConfig.test.ts @@ -1,5 +1,5 @@ import {describe, expect, test} from '@jest/globals'; -import { ManagedGitHubTeam, OrgConfig, OrgConfigurationOptions } from '../src/services/orgConfig'; +import { ManagedGitHubTeam, OrgConfig, OrgConfigurationOptions } from '../../src/services/orgConfig'; describe('OrgConfigClass', () => { test('Sets proper defaults', () => {