add github/specToProject

This module builds on the project logic added in #1238, and makes it
easy to create projects based on a simple string configuration.
Basically, the spec `foo/bar` creates a project containing just the repo
foo/bar, and the spec `@foo` creates a project containing all of the
repos from the user/organization named foo.

This is pulled out of #1233, but I've enhanced it to support
organizations out of the box.

The method is thoroughly tested.
This commit is contained in:
Dandelion Mané 2019-07-18 17:10:15 +01:00
parent daa7409abb
commit 0a34c8b036
2 changed files with 111 additions and 0 deletions

View File

@ -0,0 +1,48 @@
// @flow
import {type Project} from "../../core/project";
import {
stringToRepoId,
githubOwnerPattern,
githubRepoPattern,
} from "../../core/repoId";
import {fetchGithubOrg} from "./fetchGithubOrg";
/**
* Convert a string repository spec into a project.
*
* The spec may take one of two forms:
* - $REPO_OWNER/$REPO_NAME, as in 'sourcecred/example-github'
* - @$REPO_OWNER, as in '@sourcecred'
*
* In either case, we will create a project with the spec as its
* id. In the first construction, the project will have a single
* RepoId, matching the spec string. In the second construction,
* the project will have a RepoId for every repository owned by that
* owner.
*
* A valid GitHub token must be provided, so that it's possible to
* enumerate the repos for an org.
*/
export async function specToProject(
spec: string,
token: string
): Promise<Project> {
const repoSpecMatcher = new RegExp(
`^${githubOwnerPattern}/${githubRepoPattern}$`
);
const ownerSpecMatcher = new RegExp(`^@${githubOwnerPattern}$`);
if (spec.match(repoSpecMatcher)) {
const project: Project = {
id: spec,
repoIds: [stringToRepoId(spec)],
};
return project;
} else if (spec.match(ownerSpecMatcher)) {
const owner = spec.slice(1);
const org = await fetchGithubOrg(owner, token);
const project: Project = {id: spec, repoIds: org.repos};
return project;
}
throw new Error(`invalid spec: ${spec}`);
}

View File

@ -0,0 +1,63 @@
// @flow
import {specToProject} from "./specToProject";
import {stringToRepoId} from "../../core/repoId";
jest.mock("./fetchGithubOrg", () => ({fetchGithubOrg: jest.fn()}));
type JestMockFn = $Call<typeof jest.fn>;
const fetchGithubOrg: JestMockFn = (require("./fetchGithubOrg")
.fetchGithubOrg: any);
describe("plugins/github/specToProject", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("works for a repoId", async () => {
const spec = "foo/bar";
const expected = {
id: spec,
repoIds: [stringToRepoId(spec)],
};
const actual = await specToProject(spec, "FAKE_TOKEN");
expect(expected).toEqual(actual);
expect(fetchGithubOrg).not.toHaveBeenCalled();
});
it("works for an owner", async () => {
const repos = [stringToRepoId("foo/bar"), stringToRepoId("foo/zod")];
const spec = "@foo";
const token = "FAKE_TOKEN";
const fakeOrg = {name: "foo", repos};
fetchGithubOrg.mockResolvedValueOnce(fakeOrg);
const actual = await specToProject(spec, token);
expect(fetchGithubOrg).toHaveBeenCalledWith(fakeOrg.name, token);
const expected = {id: spec, repoIds: repos};
expect(actual).toEqual(expected);
});
describe("fails for malformed spec strings", () => {
const bad = [
"foo",
"foo_bar",
"@@foo",
" @foo ",
"foo / bar",
"",
"@foo/bar",
];
for (const b of bad) {
it(`fails for "${b}"`, () => {
expect.assertions(2);
const fail = specToProject(b, "FAKE_TOKEN");
return (
expect(fail)
.rejects.toThrow(`invalid spec: ${b}`)
// The typedef says toThrow returns void, but this promise chain does
// actually work. We don't need help from flow, since tests will fail
// if the type is wrong.
// $ExpectFlowError
.then(() => {
expect(fetchGithubOrg).toHaveBeenCalledTimes(0);
})
);
});
}
});
});