Skip to content

Commit abc9e11

Browse files
committed
feat(organization): enable creating repository for organization
1 parent 1496369 commit abc9e11

File tree

8 files changed

+176
-96
lines changed

8 files changed

+176
-96
lines changed

package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"chai": "5.1.1",
6565
"cross-env": "7.0.3",
6666
"cz-conventional-changelog": "3.3.0",
67+
"debug": "4.3.5",
6768
"gherkin-lint": "4.2.4",
6869
"http-status-codes": "2.3.0",
6970
"husky": "9.0.11",

src/repository/scaffolder.js

+31-31
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {error, info, success, warn} from '@travi/cli-messages';
22

3-
// async function authenticatedUserIsMemberOfRequestedOrganization(account, octokit) {
4-
// const {data: organizations} = await octokit.orgs.listForAuthenticatedUser();
5-
//
6-
// return organizations.reduce((acc, organization) => acc || account === organization.login, false);
7-
// }
3+
async function authenticatedUserIsMemberOfRequestedOrganization(account, octokit) {
4+
const {data: organizations} = await octokit.orgs.listForAuthenticatedUser();
5+
6+
return organizations.reduce((acc, organization) => acc || account === organization.login, false);
7+
}
88

99
async function fetchDetailsForExistingRepository(owner, name, octokit) {
1010
const {data: {ssh_url: sshUrl, html_url: htmlUrl}} = await octokit.repos.get({owner, repo: name});
@@ -35,29 +35,29 @@ async function createForUser(octokit, owner, name, visibility) {
3535
}
3636
}
3737

38-
// async function createForOrganization(octokit, owner, name, visibility) {
39-
// try {
40-
// const repositoryDetails = await fetchDetailsForExistingRepository(owner, name, octokit);
41-
//
42-
// warn(`The repository named ${owner}/${name} already exists on GitHub`);
43-
//
44-
// return repositoryDetails;
45-
// } catch (e) {
46-
// if (404 === e.status) {
47-
// const {data: {ssh_url: sshUrl, html_url: htmlUrl}} = await octokit.repos.createInOrg({
48-
// org: owner,
49-
// name,
50-
// private: 'Private' === visibility
51-
// });
52-
//
53-
// success(`Repository ${name} created for organization ${owner} at ${htmlUrl}`);
54-
//
55-
// return {sshUrl, htmlUrl};
56-
// }
57-
//
58-
// throw e;
59-
// }
60-
// }
38+
async function createForOrganization(octokit, owner, name, visibility) {
39+
try {
40+
const repositoryDetails = await fetchDetailsForExistingRepository(owner, name, octokit);
41+
42+
warn(`The repository named ${owner}/${name} already exists on GitHub`);
43+
44+
return repositoryDetails;
45+
} catch (e) {
46+
if (404 === e.status) {
47+
const {data: {ssh_url: sshUrl, html_url: htmlUrl}} = await octokit.repos.createInOrg({
48+
org: owner,
49+
name,
50+
private: 'Private' === visibility
51+
});
52+
53+
success(`Repository ${name} created for organization ${owner} at ${htmlUrl}`);
54+
55+
return {sshUrl, htmlUrl};
56+
}
57+
58+
throw e;
59+
}
60+
}
6161

6262
export default async function ({name, owner, visibility, octokit}) {
6363
if (!octokit) {
@@ -72,9 +72,9 @@ export default async function ({name, owner, visibility, octokit}) {
7272

7373
if (owner === authenticatedUser) return createForUser(octokit, owner, name, visibility);
7474

75-
// if (await authenticatedUserIsMemberOfRequestedOrganization(owner, octokit)) {
76-
// return createForOrganization(octokit, owner, name, visibility);
77-
// }
75+
if (await authenticatedUserIsMemberOfRequestedOrganization(owner, octokit)) {
76+
return createForOrganization(octokit, owner, name, visibility);
77+
}
7878

7979
throw new Error(`User ${authenticatedUser} does not have access to create a repository in the ${owner} account`);
8080
}

src/repository/scaffolder.test.js

+64-60
Original file line numberDiff line numberDiff line change
@@ -79,66 +79,70 @@ describe('creation', () => {
7979
});
8080
});
8181

82-
// describe('for organization', () => {
83-
// let getAuthenticated, listForAuthenticatedUser;
84-
//
85-
// beforeEach(() => {
86-
// getAuthenticated = vi.fn();
87-
// listForAuthenticatedUser = vi.fn();
88-
//
89-
// getAuthenticated.mockResolvedValue({data: {login: any.word()}});
90-
// listForAuthenticatedUser.mockResolvedValue({
91-
// data: [
92-
// ...any.listOf(() => ({...any.simpleObject(), login: any.word})),
93-
// {...any.simpleObject(), login: account}
94-
// ]
95-
// });
96-
// });
97-
//
98-
// // it('should create the repository for the provided organization account', async () => {
99-
// // const createInOrg = vi.fn();
100-
// // const get = vi.fn();
101-
// // const client = {repos: {createInOrg, get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
102-
// // when(createInOrg).calledWith({org: account, name, private: false}).mockResolvedValue(repoDetailsResponse);
103-
// // get.mockImplementation(() => {
104-
// // throw repoNotFoundError;
105-
// // });
106-
// //
107-
// // expect(await scaffoldRepository(name, account, 'Public', client)).toEqual({sshUrl, htmlUrl});
108-
// // });
109-
//
110-
// // it('should not create the repository when it already exists', async () => {
111-
// // const createInOrg = vi.fn();
112-
// // const get = vi.fn();
113-
// // const client = {repos: {createInOrg, get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
114-
// // when(get).calledWith({owner: account, repo: name}).mockResolvedValue(repoDetailsResponse);
115-
// //
116-
// // expect(await scaffoldRepository(name, account, 'Public', client)).toEqual({sshUrl, htmlUrl});
117-
// // expect(createInOrg).not.toHaveBeenCalled();
118-
// // });
119-
//
120-
// // it('should create the repository as private when visibility is `Private`', async () => {
121-
// // const createInOrg = vi.fn();
122-
// // const get = vi.fn();
123-
// // const client = {repos: {createInOrg, get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
124-
// // when(createInOrg).calledWith({org: account, name, private: true}).mockResolvedValue(repoDetailsResponse);
125-
// // get.mockImplementation(() => {
126-
// // throw repoNotFoundError;
127-
// // });
128-
// //
129-
// // expect(await scaffoldRepository(name, account, 'Private', client)).toEqual({sshUrl, htmlUrl});
130-
// // });
131-
//
132-
// // it('should rethrow other errors', async () => {
133-
// // const get = vi.fn();
134-
// // const client = {repos: {get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
135-
// // get.mockImplementation(() => {
136-
// // throw fetchFailureError;
137-
// // });
138-
// //
139-
// // await expect(scaffoldRepository(name, account, 'Private', client)).rejects.toThrowError(fetchFailureError);
140-
// // });
141-
// });
82+
describe('for organization', () => {
83+
let getAuthenticated, listForAuthenticatedUser;
84+
85+
beforeEach(() => {
86+
getAuthenticated = vi.fn();
87+
listForAuthenticatedUser = vi.fn();
88+
89+
getAuthenticated.mockResolvedValue({data: {login: any.word()}});
90+
listForAuthenticatedUser.mockResolvedValue({
91+
data: [
92+
...any.listOf(() => ({...any.simpleObject(), login: any.word})),
93+
{...any.simpleObject(), login: account}
94+
]
95+
});
96+
});
97+
98+
it('should create the repository for the provided organization account', async () => {
99+
const createInOrg = vi.fn();
100+
const get = vi.fn();
101+
const client = {repos: {createInOrg, get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
102+
when(createInOrg).calledWith({org: account, name, private: false}).mockResolvedValue(repoDetailsResponse);
103+
get.mockImplementation(() => {
104+
throw repoNotFoundError;
105+
});
106+
107+
expect(await scaffoldRepository({name, owner: account, visibility: 'Public', octokit: client}))
108+
.toEqual({sshUrl, htmlUrl});
109+
});
110+
111+
it('should not create the repository when it already exists', async () => {
112+
const createInOrg = vi.fn();
113+
const get = vi.fn();
114+
const client = {repos: {createInOrg, get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
115+
when(get).calledWith({owner: account, repo: name}).mockResolvedValue(repoDetailsResponse);
116+
117+
expect(await scaffoldRepository({name, owner: account, visibility: 'Public', octokit: client}))
118+
.toEqual({sshUrl, htmlUrl});
119+
expect(createInOrg).not.toHaveBeenCalled();
120+
});
121+
122+
it('should create the repository as private when visibility is `Private`', async () => {
123+
const createInOrg = vi.fn();
124+
const get = vi.fn();
125+
const client = {repos: {createInOrg, get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
126+
when(createInOrg).calledWith({org: account, name, private: true}).mockResolvedValue(repoDetailsResponse);
127+
get.mockImplementation(() => {
128+
throw repoNotFoundError;
129+
});
130+
131+
expect(await scaffoldRepository({name, owner: account, visibility: 'Private', octokit: client}))
132+
.toEqual({sshUrl, htmlUrl});
133+
});
134+
135+
it('should rethrow other errors', async () => {
136+
const get = vi.fn();
137+
const client = {repos: {get}, users: {getAuthenticated}, orgs: {listForAuthenticatedUser}};
138+
get.mockImplementation(() => {
139+
throw fetchFailureError;
140+
});
141+
142+
await expect(scaffoldRepository({name, owner: account, visibility: 'Private', octokit: client}))
143+
.rejects.toThrowError(fetchFailureError);
144+
});
145+
});
142146

143147
describe('unauthorized account', () => {
144148
it('should throw an error if the authenticated user does not have access to the requested account', async () => {

test/integration/features/scaffold.feature

+24
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,27 @@ Feature: Scaffolder
3333
Then no repository is created on GitHub
3434
# But repository settings are configured
3535
And no repository details are returned
36+
37+
Scenario: user is a member of an organization and the project is new
38+
Given netrc contains a GitHub token
39+
And the user is a member of an organization
40+
And no repository exists for the "organization" on GitHub
41+
When the project is scaffolded
42+
# And repository settings are configured
43+
And repository details are returned
44+
45+
Scenario: user is a member of an organization and the repository exists
46+
Given netrc contains a GitHub token
47+
And the user is a member of an organization
48+
And a repository already exists for the "organization" on GitHub
49+
When the project is scaffolded
50+
# And repository settings are configured
51+
And repository details are returned
52+
53+
Scenario: user is not a member of the organization
54+
Given netrc contains a GitHub token
55+
And the user is not a member of the organization
56+
When the project is scaffolded
57+
Then no repository is created on GitHub
58+
# And repository settings are configured
59+
And and an authorization error is thrown

test/integration/features/step_definitions/common-steps.js

+13-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {fileURLToPath} from 'node:url';
44
import {After, Before, When} from '@cucumber/cucumber';
55
import stubbedFs from 'mock-fs';
66
import any from '@travi/any';
7+
import debugTest from 'debug';
78

9+
const debug = debugTest('test');
810
const __dirname = dirname(fileURLToPath(import.meta.url)); // eslint-disable-line no-underscore-dangle
911
const stubbedNodeModules = stubbedFs.load(resolve(__dirname, '..', '..', '..', '..', 'node_modules'));
1012

@@ -15,6 +17,7 @@ Before(async function () {
1517
({scaffold} = await import('@form8ion/github'));
1618

1719
this.projectName = any.word();
20+
this.projectRoot = process.cwd();
1821
});
1922

2023
After(function () {
@@ -28,9 +31,14 @@ When('the project is scaffolded', async function () {
2831
node_modules: stubbedNodeModules
2932
});
3033

31-
this.result = await scaffold({
32-
projectRoot: process.cwd(),
33-
name: this.projectName,
34-
owner: this.githubUser
35-
});
34+
try {
35+
this.result = await scaffold({
36+
projectRoot: this.projectRoot,
37+
name: this.projectName,
38+
owner: this.githubUser
39+
});
40+
} catch (err) {
41+
debug(err);
42+
this.scaffoldError = err;
43+
}
3644
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {Given, Then} from '@cucumber/cucumber';
2+
import {http, HttpResponse} from 'msw';
3+
import {StatusCodes} from 'http-status-codes';
4+
5+
import {authorizationHeaderIncludesToken} from './repository-steps.js';
6+
import {assert} from 'chai';
7+
8+
Given('the user is a member of an organization', async function () {
9+
this.githubUser = this.organizationAccount;
10+
11+
this.server.use(
12+
http.get('https://api.github.com/user/orgs', ({request}) => {
13+
if (authorizationHeaderIncludesToken(request)) {
14+
return HttpResponse.json([{login: this.organizationAccount}]);
15+
}
16+
17+
return new HttpResponse(null, {status: StatusCodes.UNAUTHORIZED});
18+
})
19+
);
20+
});
21+
22+
Given('the user is not a member of the organization', async function () {
23+
this.githubUser = this.organizationAccount;
24+
25+
this.server.use(
26+
http.get('https://api.github.com/user/orgs', ({request}) => {
27+
if (authorizationHeaderIncludesToken(request)) {
28+
return HttpResponse.json([]);
29+
}
30+
31+
return new HttpResponse(null, {status: StatusCodes.UNAUTHORIZED});
32+
})
33+
);
34+
});
35+
36+
Then('and an authorization error is thrown', async function () {
37+
assert.equal(
38+
this.scaffoldError.message,
39+
`User ${this.userAccount} does not have access to create a repository in the ${this.organizationAccount} account`
40+
);
41+
});

test/integration/features/step_definitions/repository-steps.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function authorizationHeaderIncludesToken(request) {
2020
Before(function () {
2121
this.server = server;
2222
this.userAccount = userAccount;
23+
this.organizationAccount = organizationAccount;
2324
this.githubToken = githubToken;
2425
});
2526

0 commit comments

Comments
 (0)