Skip to content
Snippets Groups Projects
  • Martin Lowe's avatar
    81d44c03
    feat: Add non-project managed orgs to static GH team sync · 81d44c03
    Martin Lowe authored
    To resolves this issue, the already existing org to API wrapper for installation mapping is used. We extract the keys from the mapping, and pass them along to the static team sync. This allows non-project managed organizations like eclipse-ide to still have EF service teams added.
    
    Resolves #291
    81d44c03
    History
    feat: Add non-project managed orgs to static GH team sync
    Martin Lowe authored
    To resolves this issue, the already existing org to API wrapper for installation mapping is used. We extract the keys from the mapping, and pass them along to the static team sync. This allows non-project managed organizations like eclipse-ide to still have EF service teams added.
    
    Resolves #291
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
SyncTest.js 31.30 KiB
const chai = require('chai');
const ChaiAsPromised = require('chai-as-promised');
const expect = chai.expect;
// Chai deferred assertions break without this: https://github.com/domenic/chai-as-promised/issues/41#issuecomment-208068569
chai.should();
// set chai as promised into chai framework
chai.use(ChaiAsPromised);

const Wrapper = require('../src/GitWrapper.js');
const { GithubSync, NOT_FOUND_MESSAGE } = require('../src/Sync.js');
const { UserPermissionsOverride } = require('../src/teams/UserPermissionsOverride.js');
const { StaticTeamManager, ServiceTypes } = require('../src/teams/StaticTeamManager.js');
const { HttpWrapper } = require('../src/HttpWrapper.js');
const EclipseAPI = require('../src/EclipseAPI.js');

const { AxiosError } = require('axios');

const sampleUserStore = generateSampleUserStore();

describe('GithubSync', function () {
  describe('#updateProjectTeam', function () {
    describe('success', function () {
      const baseProject = generateSampleProjects()[0];
      const testOrgName = 'eclipsefdn-webdev';
      const Sync = setupBasicSync();

      afterEach(() => {
        jest.restoreAllMocks();
      });
      afterAll(() => {
        jest.resetAllMocks();
      });

      it('should update project team to be public', done => {
        const grouping = 'committers';
        const teamName = 'sample-committers';
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([
          {
            login: 'epoirier'
          }
        ]);
        reposForOrgMock({ [testOrgName]: [{ name: 'spider-pig' }, { name: '.eclipsefdn' }] });
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateProjectTeam(testOrgName, baseProject, grouping)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['malowe', 'epoirier']);
          chai.expect(updatedTeams).to.deep.equal([teamName]);
          chai.expect(removedUsers).to.be.empty;
          chai.expect(teamUpdates[teamName]).to.deep.equal({
            privacy: 'closed',
            description: generateExpectedTeamDescription(baseProject, grouping)
          });
        }).should.notify(done);
      });

      it('should allow override project team name', done => {
        const grouping = 'committers';
        const defaultTeamName = 'sample-committers';
        const overriddenTeamName = 'sample-team-name';
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([
          {
            login: 'epoirier'
          }
        ]);
        reposForOrgMock({ [testOrgName]: [{ name: 'spider-pig' }, { name: '.eclipsefdn' }] });
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateProjectTeam(testOrgName, baseProject, grouping, overriddenTeamName)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['malowe', 'epoirier']);
          chai.expect(updatedTeams).to.deep.equal([overriddenTeamName]);
          chai.expect(removedUsers).to.be.empty;
          chai.expect(teamUpdates[overriddenTeamName]).to.deep.equal({
            privacy: 'closed',
            description: generateExpectedTeamDescription(baseProject, grouping)
          });
          chai.expect(teamUpdates[defaultTeamName]).to.be.undefined;
        }).should.notify(done);
      });

      it('should support security_team processing', done => {
        const grouping = 'security_team';
        const teamName = 'sample-security_team';
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock();
        reposForOrgMock({ [testOrgName]: [{ name: 'spider-pig' }, { name: '.eclipsefdn' }] });
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateProjectTeam(testOrgName, baseProject, grouping)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['malowe', 'epoirier']);
          chai.expect(updatedTeams).to.deep.equal([teamName]);
          chai.expect(removedUsers).to.be.empty;
          chai.expect(teamUpdates[teamName]).to.deep.equal({
            privacy: 'closed',
            description: generateExpectedTeamDescription(baseProject, grouping)
          });
        }).should.notify(done);
      });
      it('should keep team secret with no .eclipsefdn repo', done => {
        const grouping = 'committers';
        const teamName = 'sample-committers';
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([
          {
            login: 'epoirier'
          }
        ]);
        reposForOrgMock({ [testOrgName]: [{ name: 'spider-pig' }] });
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateProjectTeam(testOrgName, baseProject, grouping)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['malowe', 'epoirier']);
          chai.expect(updatedTeams).to.deep.equal([teamName]);
          chai.expect(removedUsers).to.be.empty;
          chai.expect(teamUpdates[teamName]).to.deep.equal({
            privacy: 'secret',
            description: generateExpectedTeamDescription(baseProject, grouping)
          });
        }).should.notify(done);
      });
    });
  });
  describe('#updateTeam', function () {
    describe('success', function () {
      const baseProject = generateSampleProjects()[0];
      const baseTeamName = 'sample-team';
      const Sync = setupBasicSync();

      afterEach(() => {
        jest.restoreAllMocks();
      });
      afterAll(() => {
        jest.resetAllMocks();
      });

      it('should update team on call', done => {
        const teamProps = {
          privacy: 'closed',
          description: 'Sample description for team'
        };
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([]);
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateTeam('sample', { ...teamProps, teamName: baseTeamName }, [], baseProject)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.be.empty;
          chai.expect(updatedTeams).to.be.empty;
          chai.expect(removedUsers).to.be.empty;
          chai.expect(teamUpdates[baseTeamName]).to.deep.equal(teamProps);
        }).should.notify(done);
      });

      it('should add new team members', done => {
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([]);
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateTeam('sample', {
          privacy: 'closed',
          description: 'Sample description for team',
          teamName: baseTeamName
        }, [{
          username: 'test-user-1',
          url: 'https://api.eclipse.org/account/profile/test-user-1',
        }], baseProject)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['test-user-1']);
          chai.expect(updatedTeams).to.deep.equal([baseTeamName]);
          chai.expect(removedUsers).to.be.empty;
        }).should.notify(done);
      });

      it('should remove no longer present team members', done => {
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([{
          login: 'test-user-1'
        }, {
          login: 'test-user-2'
        }]);
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateTeam('sample', {
          privacy: 'closed',
          description: 'Sample description for team',
          teamName: baseTeamName
        }, [{
          username: 'test-user-1',
          url: 'https://api.eclipse.org/account/profile/test-user-1',
        }], baseProject)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['test-user-1']);
          chai.expect(updatedTeams).to.deep.equal([baseTeamName]);
          chai.expect(removedUsers).to.deep.equal(['test-user-2']);
        }).should.notify(done);
      });

      it('should remove expired team members', done => {
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([{
          login: 'test-user-1'
        }, {
          login: 'test-user-2'
        }]);
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateTeam('sample', {
          privacy: 'closed',
          description: 'Sample description for team',
          teamName: baseTeamName
        }, [{
          username: 'test-user-1',
          url: 'https://api.eclipse.org/account/profile/test-user-1',
          expiration: '2020-01-01'
        }, {
          username: 'test-user-2',
          url: 'https://api.eclipse.org/account/profile/test-user-2',
          expiration: '2099-01-01'
        }], baseProject)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['test-user-2']);
          chai.expect(updatedTeams).to.deep.equal([baseTeamName]);
          chai.expect(removedUsers).to.deep.equal(['test-user-1']);
        }).should.notify(done);
      });

      it('should remove users when handled not found response is returned', done => {
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([{
          login: 'test-user-1'
        }, {
          login: 'no-matching-user'
        }]);
        // this test assumes that the 'no-matching-user' that returns was deleted account
        chai.expect(Sync.updateTeam('sample', {
          privacy: 'closed',
          description: 'Sample description for team',
          teamName: baseTeamName
        }, [{
          username: 'test-user-1',
          url: 'https://api.eclipse.org/account/profile/test-user-1'
        }, {
          username: 'no-matching-user',
          url: 'https://api.eclipse.org/account/profile/no-matching-user',
        }], baseProject)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.deep.equal(['test-user-1']);
          chai.expect(updatedTeams).to.deep.equal([baseTeamName]);
          chai.expect(removedUsers).to.deep.equal(['no-matching-user']);
        }).should.notify(done);
      });

      it('should skip removal of team members on errors', done => {
        const { teamUpdates, removedUsers, updatedUsers, updatedTeams } = updateTeamCommonMock([{
          login: 'test-user-1'
        }, {
          login: 'test-user-2'
        }], false);

        jest.spyOn(HttpWrapper.prototype, 'getRaw').mockImplementationOnce((url) => {
          // close enough representation to the service being down
          return new AxiosError('An error has occurred', '500', undefined, undefined, { data: { message: 'some error' } });
        });
        // do the assertions w/ some deferred to after the call completes
        chai.expect(Sync.updateTeam('sample', {
          privacy: 'closed',
          description: 'Sample description for team',
          teamName: baseTeamName
        }, [{
          username: 'test-user-1',
          url: 'https://api.eclipse.org/account/profile/test-user-1',
        }], baseProject)).fulfilled.then(function () {
          chai.expect(updatedUsers).to.be.empty;
          chai.expect(updatedTeams).to.be.empty;
          chai.expect(removedUsers).to.be.empty;
        }).should.notify(done);
      });
    });
  });

  describe('#removeOrgExternalContributors', function () {
    describe('success', function () {
      const testOrgName = 'eclipsefdn-webdev';
      const sampleProjects = generateSampleProjects();
      const Sync = setupBasicSync();

      // required to clean up between tests
      afterEach(() => {
        jest.restoreAllMocks();
      });
      afterAll(() => {
        jest.resetAllMocks();
      });

      it('should remove standard users on normal run', done => {
        // set a few sample users as contributors
        const usernames = removeOrgExternalContributorsMock([{ login: 'dummy' }, { login: 'doofenshmirtz' }]);

        chai.expect(Sync.removeOrgExternalContributors(sampleProjects, testOrgName)).fulfilled.then(function () {
          chai.expect(usernames).to.deep.equal(['dummy', 'doofenshmirtz']);
        }).should.notify(done);
      });
      it('should remove users that are Project Leads', done => {
        // set a few sample users as contributors
        const usernames = removeOrgExternalContributorsMock([{ login: 'malowe' }, { login: 'doofenshmirtz' }]);

        chai.expect(Sync.removeOrgExternalContributors(sampleProjects, testOrgName)).fulfilled.then(function () {
          chai.expect(usernames).to.deep.equal(['malowe', 'doofenshmirtz']);
        }).should.notify(done);
      });
      it('should not remove bot users', done => {
        // set a few sample users as contributors
        const usernames = removeOrgExternalContributorsMock([{ login: 'sample-bot' }, { login: 'doofenshmirtz' }]);

        chai.expect(Sync.removeOrgExternalContributors(sampleProjects, testOrgName)).fulfilled.then(function () {
          chai.expect(usernames).to.deep.equal(['doofenshmirtz']);
        }).should.notify(done);
      });
      it('should remove no one when error fetching contributors', done => {
        // set a few sample users as contributors
        const usernames = removeOrgExternalContributorsMock(undefined);

        chai.expect(Sync.removeOrgExternalContributors(sampleProjects, testOrgName)).fulfilled.then(function () {
          chai.expect(usernames).to.be.empty;
        }).should.notify(done);
      });
      it('should finish quietly if there is no contributors', done => {
        // set a few sample users as contributors
        const usernames = removeOrgExternalContributorsMock([]);
        chai.expect(Sync.removeOrgExternalContributors(sampleProjects, testOrgName)).fulfilled.then(function () {
          chai.expect(usernames).to.be.empty;
        }).should.notify(done);
      });
    });
  });

  describe('#removeRepoExternalContributors', function () {
    describe('success', function () {
      const testOrgName = 'eclipsefdn-webdev';
      const testRepo = 'spider-pig';
      const sampleProject = generateSampleProjects()[0];
      const Sync = setupBasicSync();

      // required to clean up between tests
      afterEach(() => {
        jest.restoreAllMocks();
      });
      afterAll(() => {
        jest.resetAllMocks();
      });

      it('should remove standard users on normal run', done => {
        // set a few sample users as contributors
        const usernames = removeRepoExternalContributorsMock([{ login: 'dummy' }, { login: 'doofenshmirtz' }]);

        chai.expect(Sync.removeRepoExternalContributors(sampleProject, testOrgName, testRepo)).fulfilled.then(function () {
          chai.expect(usernames).to.deep.equal({ [testRepo]: ['dummy', 'doofenshmirtz'] });
        }).should.notify(done);
      });
      it('should keep users that are Project Leads', done => {
        // set a few sample users as contributors
        const usernames = removeRepoExternalContributorsMock([{ login: 'malowe' }, { login: 'doofenshmirtz' }]);

        chai.expect(Sync.removeRepoExternalContributors(sampleProject, testOrgName, testRepo)).fulfilled.then(function () {
          chai.expect(usernames).to.deep.equal({ [testRepo]: ['doofenshmirtz'] });
        }).should.notify(done);
      });
      it('should not remove bot users', done => {
        // set a few sample users as contributors
        const usernames = removeRepoExternalContributorsMock([{ login: 'sample-bot' }, { login: 'doofenshmirtz' }]);
        chai.expect(Sync.removeRepoExternalContributors(sampleProject, testOrgName, testRepo)).fulfilled.then(function () {
          chai.expect(usernames).to.deep.equal({ [testRepo]: ['doofenshmirtz'] });
        }).should.notify(done);
      });
      it('should not remove eclipsewebmaster', done => {
        // set a few sample users as contributors
        const usernames = removeRepoExternalContributorsMock([{ login: 'eclipsewebmaster' }, { login: 'doofenshmirtz' }]);

        chai.expect(Sync.removeRepoExternalContributors(sampleProject, testOrgName, testRepo)).fulfilled.then(function () {
          chai.expect(usernames).to.deep.equal({ [testRepo]: ['doofenshmirtz'] });
        }).should.notify(done);
      });
      it('should remove no one when error fetching contributors', done => {
        // set an effective error as the contributors
        const usernames = removeRepoExternalContributorsMock(undefined);

        chai.expect(Sync.removeRepoExternalContributors(sampleProject, testOrgName, testRepo)).fulfilled.then(function () {
          chai.expect(usernames).to.be.empty;
        }).should.notify(done);
      });
      it('should finish quietly if there is no contributors', done => {
        // set a empty users as contributors
        const usernames = removeRepoExternalContributorsMock([]);
        chai.expect(Sync.removeRepoExternalContributors(sampleProject, testOrgName, testRepo)).fulfilled.then(function () {
          chai.expect(usernames).to.be.empty;
        }).should.notify(done);
      });
      it('should skip removal if there is a server error looking up the user', done => {
        // set up the normal test, minus the user lookup
        const usernames = removeRepoExternalContributorsMock([{ login: 'doofenshmirtz' }], false);
        // manually setup the pseudo-error state for the user lookup mechanic
        jest.spyOn(HttpWrapper.prototype, 'getRaw').mockImplementationOnce((url) => {
          // close enough representation to the service being down
          return new AxiosError('An error has occurred', '500', undefined, undefined, { data: { message: 'some error' } });
        });
        chai.expect(Sync.removeRepoExternalContributors(sampleProject, testOrgName, testRepo)).fulfilled.then(function () {
          chai.expect(usernames).to.be.empty;
        }).should.notify(done);
      });
    });
  });

  describe('#processProjects', function () {
    describe('success', function () {
      // setup with no overrides for base usage
      const Sync = new GithubSync({
        V: true,
        d: false
      });
      Sync.upo = new UserPermissionsOverride(`${__dirname}/perms/empty_overrides.json`);
      const baseProjects = generateSampleProjects();
      let result;
      beforeAll(async function () {
        result = await Sync.processProjects(JSON.parse(JSON.stringify(baseProjects)));
      });

      it('should contain JSON data', function () {
        expect(result).to.be.an('array');
      });
      it('should be the same size as the input', function () {
        expect(result.length).equal(baseProjects.length);
      });
      it('should have not change users with no overrides', function () {
        const firstResult = result[0];
        const firstIn = baseProjects[0];
        expect(firstResult.contributors).deep.equal(firstIn.contributors);
        expect(firstResult.committers).deep.equal(firstIn.committers);
        expect(firstResult.project_leads).deep.equal(firstIn.project_leads);
      });
    });
    describe('success w/ user overrides', function () {
      // setup with no overrides for base usage
      const Sync = new GithubSync({
        V: true,
        d: false
      });
      Sync.upo = new UserPermissionsOverride(`${__dirname}/perms/test.json`);
      const baseProjects = generateSampleProjects();
      var result;
      beforeAll(async function () {
        result = await Sync.processProjects(JSON.parse(JSON.stringify(baseProjects)));
      });

      it('should contain JSON data', function () {
        expect(result).to.be.an('array');
      });
      it('should be the same size as the input', function () {
        expect(result.length).equal(baseProjects.length);
      });
      it('should remove excluded users', function () {
        const firstResult = result[0];
        const firstIn = baseProjects[0];
        expect(firstIn.contributors).to.not.be.empty;
        expect(firstResult.contributors).to.be.empty;
      });
      it('should update users with overridden permissions', function () {
        const firstResult = result[0];
        const firstIn = baseProjects[0];
        const expectedLeads = [
          ...firstIn.project_leads,
          {
            username: 'epoirier',
            url: 'https://api.eclipse.org/account/profile/epoirier',
          }
        ];
        // should be 1 additional entry
        expect(firstResult.project_leads.length).equal(expectedLeads.length);
        expect(firstResult.project_leads).to.deep.equal(expectedLeads);
      });
    });
  });

  describe('#checkIfTeamCanBePublic', function () {
    describe('success', function () {
      const testOrgName = 'eclipsefdn-webdev';
      const testRepo = 'spider-pig';
      const sampleProject = generateSampleProjects()[0];
      const Sync = setupBasicSync();

      // required to clean up between tests
      afterEach(() => {
        jest.restoreAllMocks();
      });
      afterAll(() => {
        jest.resetAllMocks();
      });

      it('should indicate true when eclipsefdn repo is present', async () => {
        // this is a "good enough" mocking as we only check what is present by name
        reposForOrgMock({ [testOrgName]: [{ name: 'spider-pig' }, { name: '.eclipsefdn' }] });

        chai.expect(await Sync.checkIfTeamCanBePublic(testOrgName)).to.equal(true);
      });

      it('should indicate false when eclipsefdn repo is not present', async () => {
        // this is a "good enough" mocking as we only check what is present by name
        reposForOrgMock({ [testOrgName]: [{ name: 'spider-pig' }] });

        chai.expect(await Sync.checkIfTeamCanBePublic(testOrgName)).to.equal(false);
      });

      it('should indicate false when no repos are found', async () => {
        // this is a "good enough" mocking as we only check what is present by name
        reposForOrgMock({ [testOrgName]: [{ name: 'spider-pig' }] });

        chai.expect(await Sync.checkIfTeamCanBePublic('invalid-org-name')).to.equal(false);
      });
    });
  });
  describe('#checkIfUserIsMissing', function () {
    describe('success', function () {
      const Sync = setupBasicSync();
      const baseResponse = {
        status: 404,
        data:new Array(NOT_FOUND_MESSAGE)
      };

      // required to clean up between tests
      afterEach(() => {
        jest.restoreAllMocks();
      });
      afterAll(() => {
        jest.resetAllMocks();
      });

      it('should return true if response message is in array format', () => {
        chai.expect(Sync.checkIfUserIsMissing(baseResponse)).to.equal(true);
      });
      it('should return true if response message is in object format', () => {
        chai.expect(Sync.checkIfUserIsMissing({ ...baseResponse, data: { message: NOT_FOUND_MESSAGE } })).to.equal(true);
      });
      it('should return false if response has any other response', () => {
        chai.expect(Sync.checkIfUserIsMissing({ ...baseResponse, data: "<html><body><h1>Yay!</h1></body></html>" })).to.equal(false);
      });
      it('should return false if response isn\'t a 404', () => {
        // show some example, as only the 404 status should match
        chai.expect(Sync.checkIfUserIsMissing({ ...baseResponse, status: 200 })).to.equal(false);
        chai.expect(Sync.checkIfUserIsMissing({ ...baseResponse, status: 400 })).to.equal(false);
        chai.expect(Sync.checkIfUserIsMissing({ ...baseResponse, status: 500 })).to.equal(false);
      });
    });
  });
  describe('#filterDuplicateRepositories', function () {
    describe('success', function () {
      const goodRepos = [{ 'org': '1', 'repo': 'sample' }, { 'org': '2', 'repo': 'test' }, { 'org': '1', 'repo': 'example' }];
      const Sync = setupBasicSync();

      // required to clean up between tests
      afterEach(() => {
        jest.restoreAllMocks();
      });
      afterAll(() => {
        jest.resetAllMocks();
      });

      it('should not modify a unique list of repos', async () => {
        // none should be removed from the base list
        chai.expect(Sync.filterDuplicateRepositories(goodRepos)).to.deep.equal(goodRepos);
      });
      it('should remove duplicate repos based on name and organization', async () => {
        const testRepos = [...goodRepos, goodRepos[1]];
        // should remove the added duplicate
        chai.expect(Sync.filterDuplicateRepositories(testRepos)).to.deep.equal(goodRepos);
      });
      it('should retain repos with duplicated names in different organizations', async () => {
        const tweakedRepo = { ...goodRepos[0] };
        tweakedRepo.org = 'other';
        const testRepos = [...goodRepos, tweakedRepo];
        // should not remove the tweaked repo as it's in a different org even if repo name is same
        chai.expect(Sync.filterDuplicateRepositories(testRepos)).to.deep.equal(testRepos);
      });
    });
  });
});

function setupBasicSync() {
  const Sync = new GithubSync({
    V: true,
    d: false
  });
  Sync.wrap = new Wrapper('secret');
  Sync.cHttp = new HttpWrapper();
  Sync.stm = new StaticTeamManager();
  Sync.eclipseApi = new EclipseAPI();
  Sync.bots = {
    'sample': ['sample-bot'],
    'other-project': ['project-bot']
  };
  Sync.upo = new UserPermissionsOverride(`${__dirname}/perms/empty_overrides.json`);
  return Sync;
}

function reposForOrgMock(repoMapping) {
  jest.spyOn(Wrapper.prototype, 'getReposForOrg').mockImplementation((org) => {
    return repoMapping[org];
  });
}

function removeOrgExternalContributorsMock(contributors) {
  jest.spyOn(Wrapper.prototype, 'getOrgCollaborators').mockImplementationOnce((org) => {
    return contributors;
  });
  const usernames = [];
  jest.spyOn(Wrapper.prototype, 'removeUserAsOutsideCollaborator').mockImplementation((org, username) => {
    usernames.push(username);
  });
  return usernames;
}

function removeRepoExternalContributorsMock(contributors, includeEclipseAccountMocking = true) {
  jest.spyOn(Wrapper.prototype, 'getRepoCollaborators').mockImplementationOnce((org) => {
    return contributors;
  });
  const usernames = {};
  jest.spyOn(Wrapper.prototype, 'removeUserAsCollaborator').mockImplementation((org, repo, username) => {
    if (usernames[repo] === undefined) {
      usernames[repo] = [];
    }
    usernames[repo].push(username);
  });
  if (includeEclipseAccountMocking) {
    jest.spyOn(HttpWrapper.prototype, 'getRaw').mockImplementationOnce((url) => {
      if (sampleUserStore[url] !== undefined) {
        return { data: sampleUserStore[url] };
      }
      // error returned by EF API when user is missing
      return new AxiosError('', 404, undefined, undefined, { data: ['User not found.'], status: 404 });
    });
  }
  return usernames;
}

/**
 * Some of the unchanging mock calls, made to simplify the tests
 * @returns 
 */
function updateTeamCommonMock(teamMembers, includeEclipseAccountMocking = true) {
  jest.spyOn(Wrapper.prototype, 'addTeam').mockImplementation((org, teamName) => {
    return {};
  });
  const updatedUsers = [];
  const updatedTeams = [];
  jest.spyOn(Wrapper.prototype, 'inviteUserToTeam').mockImplementation((org, teamName, user) => {
    // will be gh handle, not EF username
    updatedUsers.push(user);
    if (updatedTeams.indexOf(teamName) === -1) {
      updatedTeams.push(teamName);
    }
  });
  const removedUsers = [];
  jest.spyOn(Wrapper.prototype, 'removeUserFromTeam').mockImplementation((org, teamName, user) => {
    removedUsers.push(user);
    if (updatedTeams.indexOf(teamName) === -1) {
      updatedTeams.push(teamName);
    }
  });
  const teamUpdates = {};
  jest.spyOn(Wrapper.prototype, 'editTeam').mockImplementation((org, team, options) => {
    teamUpdates[team] = options;
  });
  jest.spyOn(Wrapper.prototype, 'getTeamMembers').mockImplementation((org, team) => {
    // uses passed param to allow for different team setups to be tested
    return teamMembers;
  });
  if (includeEclipseAccountMocking) {
    jest.spyOn(HttpWrapper.prototype, 'getRaw').mockImplementation((url) => {
      if (sampleUserStore[url] !== undefined) {
        return { data: sampleUserStore[url] };
      }
      // error returned by EF API when user is missing
      return new AxiosError('', 404, undefined, undefined, { data: ['User not found.'], status: 404 });
    });
  }
  return { teamUpdates, updatedUsers, updatedTeams, removedUsers };
}
function generateExpectedTeamDescription(project, grouping) {
  return `The ${grouping} access team for the ${project.name} (${project.project_id}) project: ${project.url}`;
}
/**
 * Generates fake users to be used in HTTP stubbing in tests.
 */
function generateSampleUserStore() {
  return {
    'https://api.eclipse.org/account/profile/test-user-1': generateSampleUser('test-user-1'),
    'https://api.eclipse.org/account/profile/test-user-2': generateSampleUser('test-user-2'),
    'https://api.eclipse.org/account/profile/epoirier': generateSampleUser('epoirier'),
    'https://api.eclipse.org/account/profile/malowe': generateSampleUser('malowe'),
    'https://api.eclipse.org/github/profile/malowe': generateSampleUser('malowe'),
    'https://api.eclipse.org/github/profile/epoirier': generateSampleUser('epoirier'),
    'https://api.eclipse.org/github/profile/doofenshmirtz': generateSampleUser('doofenshmirtz')
  };
}

function generateSampleUser(uname, ghHandle, hasSignedECA = true, isCommitter = true) {
  return {
    "uid": "",
    "name": uname,
    "picture": "",
    "mail": `${uname}@eclipse-foundation.org`,
    "eca": {
      "signed": hasSignedECA,
      "can_contribute_spec_project": hasSignedECA
    },
    "is_committer": isCommitter,
    "first_name": "Sample",
    "last_name": "Test1",
    "full_name": "Sample Test1",
    "publisher_agreements": {
      "open-vsx": { "version": "1" }
    },
    "github_handle": ghHandle || uname,
    "twitter_handle": "",
    "org": "EF Sample 01",
    "org_id": 1660,
    "job_title": "Software Developer",
    "website": "",
    "country": {
      "code": null,
      "name": null
    }, "bio": null,
    "interests": ["Music", "Video games", "cats", "Accessibility"],
    "working_groups_interests": [],
    "public_fields": [],
    "mail_history": [],
    "mail_alternate": [],
    "eca_url": `https://api.eclipse.org/account/profile/${uname}/eca`,
    "projects_url": `https://api.eclipse.org/account/profile/${uname}/projects`,
    "gerrit_url": `https://api.eclipse.org/account/profile/${uname}/gerrit`,
    "mailinglist_url": `https://api.eclipse.org/account/profile/${uname}/mailing-list`,
    "mpc_favorites_url": `https://api.eclipse.org/marketplace/favorites?name=${uname}`
  };
}

function generateSampleProjects() {
  return [
    {
      project_id: 'sample',
      short_project_id: 'sample',
      pp_orgs: ['eclipsefdn-webdev'],
      github_repos: [{
        url: 'https://github.com/eclipsefdn-webdev/spider-pig',
      }],
      github: {
        org: 'eclipsefdn-webdev',
        ignored_repos: [],
      },
      contributors: [{
        username: 'webdev_2',
        url: 'https://api.eclipse.org/account/profile/webdev_2',
      }],
      committers: [{
        username: 'malowe',
        url: 'https://api.eclipse.org/account/profile/malowe',
      }, {
        username: 'epoirier',
        url: 'https://api.eclipse.org/account/profile/epoirier',
      }],
      project_leads: [{
        username: 'malowe',
        url: 'https://api.eclipse.org/account/profile/malowe',
      }, {
        username: 'cguindon',
        url: 'https://api.eclipse.org/account/profile/cguindon',
      }],
      working_groups: [{
        name: 'Cloud Development Tools',
        id: 'cloud-development-tools',
      }],
      spec_project_working_group: [],
      state: 'Regular',
      security_team: {
        individual_members: [],
        groups: {
          include_committers: true,
          include_project_leads: false
        }
      },
    }
  ];
}