Skip to content

Commit

Permalink
Merge pull request #129 from wintondeshong/main
Browse files Browse the repository at this point in the history
Github pull request functions
  • Loading branch information
wintondeshong committed Nov 24, 2020
2 parents 0dee82b + 088d842 commit dbfd744
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 9 deletions.
8 changes: 8 additions & 0 deletions and-cli-github.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ require("./command-runner").run(async () => {
"--add-topic <topic>",
`Add topic to specified repository, or all ${ANDCULTURE_CODE} repositories if no repo provided`
)
.option(
"--auth",
"Authenticate any github API requests. Can help avoid API limits"
)
.option("--list-repos", "Lists all andculture repos")
.option(
"--list-topics",
Expand Down Expand Up @@ -75,6 +79,10 @@ require("./command-runner").run(async () => {
StringUtils.hasValue(program.removeTopic) &&
StringUtils.hasValue(program.repo);

if (program.auth != null) {
await github.getToken();
}

if (listAndcultureRepos) {
echo.success(`${ANDCULTURE_CODE} Repositories`);
echo.byProperty(await github.repositoriesByAndculture(), "url");
Expand Down
84 changes: 78 additions & 6 deletions modules/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ const github = {
// -----------------------------------------------------------------------------------------

andcultureOrg: ANDCULTURE_CODE,
apiPullsRouteParam: "pulls",
apiRepositoriesRouteParam: "repos",
apiReviewsRouteParam: "reviews",
apiRootUrl: `https://${API_DOMAIN}`,
apiTopicsRouteParam: "topics",
configAuthConfigPath: upath.join(os.homedir(), ".netrc"), // Path to octokit-auth-netrc configuration
Expand Down Expand Up @@ -132,6 +134,64 @@ const github = {
return `Helpful github operations used at ${ANDCULTURE}`;
},

/**
* Retrieves list of pull requests for a repository
* @param {string} owner user or organization name owning the repo
* @param {string} repoName name of repository
* @param {string} state all, closed, open
*/
async getPullRequests(owner, repoName, state) {
if (!_validateInputOrExit(owner, repoName)) {
return null;
}

state = StringUtils.isEmpty(state) ? "all" : state;

try {
const response = await _client().pulls.list({
owner: owner,
repo: repoName,
state,
});
_throwIfApiError(response);

return response.data;
} catch (e) {
echo.error(
`Error retrieving pull requests for ${owner}/${repoName} - ${e}`
);
return null;
}
},

/**
* Retrieves list of reviews for a pull request
* @param {string} owner user or organization name owning the repo
* @param {string} repoName name of repository
* @param {number} number pull request number
*/
async getPullRequestReviews(owner, repoName, number) {
if (!_validateInputOrExit(owner, repoName)) {
return null;
}

try {
const response = await _client().pulls.listReviews({
owner: owner,
repo: repoName,
pull_number: number,
});
_throwIfApiError(response);

return response.data;
} catch (e) {
echo.error(
`Error retrieving reviews for ${owner}/${repoName}/pulls/${number} - ${e}`
);
return null;
}
},

/**
* Retrieves a repository
* @param {string} owner user or organization name owning the repo
Expand Down Expand Up @@ -567,6 +627,22 @@ const _updateTopicsForRepo = async (updateFunc, owner, repoName) => {
}
};

/**
* Validates standard user input
*
* @param {string} owner user or organization name owning the repo
* @param {string} repoName short name of repository (excluding user/organization)
*/
const _validateInputOrExit = (owner, repoName) => {
if (StringUtils.hasValue(owner) && StringUtils.hasValue(repoName)) {
return true;
}

echo.error("Owner and repository name must be provided");
shell.exit(1);
return false;
};

/**
* Validates user input for updating topics
*
Expand All @@ -575,15 +651,11 @@ const _updateTopicsForRepo = async (updateFunc, owner, repoName) => {
* @param {string} repoName short name of repository (excluding user/organization)
*/
const _validateTopicInputOrExit = (topic, owner, repoName) => {
if (
StringUtils.hasValue(topic) &&
StringUtils.hasValue(owner) &&
StringUtils.hasValue(repoName)
) {
if (_validateInputOrExit(owner, repoName) && StringUtils.hasValue(topic)) {
return true;
}

echo.error("Topic, owner, and repository name must be provided");
echo.error("Topic must be provided");
shell.exit(1);
return false;
};
Expand Down
166 changes: 166 additions & 0 deletions modules/github.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const github = require("./github");
const nock = require("nock");
const testUtils = require("../tests/test-utils");
const userPrompt = require("./user-prompt");
const { random } = require("faker");

// #endregion Imports

Expand All @@ -22,6 +23,34 @@ describe("github", () => {
// #region Setup
// -----------------------------------------------------------------------------------------

/**
* Utility function for generating the /repos/{owner}/{repoName}/pulls API route
*/
const getRepoPullRequestsRoute = (owner, repoName) =>
new RegExp(
[
github.apiRepositoriesRouteParam,
owner,
repoName,
github.apiPullsRouteParam,
].join("/")
);

/**
* Utility function for generating the /repos/{owner}/{repoName}/pulls/{pull_number}/reviews API route
*/
const getRepoPullRequestReviewsRoute = (owner, repoName, pullNumber) =>
new RegExp(
[
github.apiRepositoriesRouteParam,
owner,
repoName,
github.apiPullsRouteParam,
pullNumber,
github.apiReviewsRouteParam,
].join("/")
);

/**
* Utility function for generating the /{owner}/repos API route
*/
Expand Down Expand Up @@ -262,6 +291,143 @@ describe("github", () => {

//#endregion description

// -----------------------------------------------------------------------------------------
// #region getPullRequests
// -----------------------------------------------------------------------------------------

describe("getPullRequests", () => {
let shellExitSpy;
beforeEach(() => {
shellExitSpy = testUtils.spyOnShellExit();
});

test.each([undefined, null, "", " "])(
"given owner is %p, it outputs an error and calls shell.exit",
async (owner) => {
// Arrange
const repoName = testUtils.randomWord();
const echoErrorSpy = jest.spyOn(echo, "error");

// Act
await github.getPullRequests(owner, repoName);

// Assert
expect(echoErrorSpy).toHaveBeenCalled();
expect(shellExitSpy).toHaveBeenCalled();
}
);

test.each([undefined, null, "", " "])(
"given repoName is %p, it outputs an error and calls shell.exit",
async (repoName) => {
// Arrange
const owner = testUtils.randomWord();
const echoErrorSpy = jest.spyOn(echo, "error");

// Act
await github.getPullRequests(owner, repoName);

// Assert
expect(echoErrorSpy).toHaveBeenCalled();
expect(shellExitSpy).toHaveBeenCalled();
}
);

test("given username and repo exists, returns pull requests", async () => {
// Arrange
const owner = "AndcultureCode";
const repoName = "AndcultureCode.Cli";

const mockedPullRequests = [{ id: random.number() }];

nock(github.apiRootUrl)
.get(getRepoPullRequestsRoute(owner, repoName))
.reply(200, mockedPullRequests);

// Act
const results = await github.getPullRequests(owner, repoName);

// Assert
expect(results).not.toBeNull();
expect(results.length).toBeGreaterThan(0);
});
});

// #endregion getPullRequests

// -----------------------------------------------------------------------------------------
// #region getPullRequestReviews
// -----------------------------------------------------------------------------------------

describe("getPullRequestReviews", () => {
let shellExitSpy;
beforeEach(() => {
shellExitSpy = testUtils.spyOnShellExit();
});

test.each([undefined, null, "", " "])(
"given owner is %p, it outputs an error and calls shell.exit",
async (owner) => {
// Arrange
const repoName = testUtils.randomWord();
const echoErrorSpy = jest.spyOn(echo, "error");
const pullNumber = random.number();

// Act
await github.getPullRequestReviews(owner, repoName, pullNumber);

// Assert
expect(echoErrorSpy).toHaveBeenCalled();
expect(shellExitSpy).toHaveBeenCalled();
}
);

test.each([undefined, null, "", " "])(
"given repoName is %p, it outputs an error and calls shell.exit",
async (repoName) => {
// Arrange
const owner = testUtils.randomWord();
const echoErrorSpy = jest.spyOn(echo, "error");
const pullNumber = random.number();

// Act
await github.getPullRequestReviews(owner, repoName, pullNumber);

// Assert
expect(echoErrorSpy).toHaveBeenCalled();
expect(shellExitSpy).toHaveBeenCalled();
}
);

test("given username and repo exists, returns pull request reviews", async () => {
// Arrange
const owner = testUtils.randomWord();
const repoName = testUtils.randomWord();
const pullNumber = random.number();

const mockedReviews = [{}];

nock(github.apiRootUrl)
.get(
getRepoPullRequestReviewsRoute(owner, repoName, pullNumber)
)
.reply(200, mockedReviews);

// Act
const results = await github.getPullRequestReviews(
owner,
repoName,
pullNumber
);

// Assert
expect(results).not.toBeNull();
expect(results.length).toBeGreaterThan(0);
});
});

// #endregion getPullRequestReviews

// -----------------------------------------------------------------------------------------
// #region getRepo
// -----------------------------------------------------------------------------------------
Expand Down
13 changes: 10 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit dbfd744

Please sign in to comment.