Skip to content

Commit 5ab654a

Browse files
authored
fix: improve code quality and test coverage (#25)
- Add comprehensive tests for transfer permissions with fetch mocking - Add workflow timeouts (5-15 min) to prevent hanging jobs - Add file existence validation in parser before reading
1 parent 49f32c5 commit 5ab654a

5 files changed

Lines changed: 211 additions & 82 deletions

File tree

.github/workflows/drift-detection.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
jobs:
77
detect-drift:
88
runs-on: ubuntu-latest
9+
timeout-minutes: 10
910
permissions:
1011
pull-requests: write
1112
contents: read

.github/workflows/sync-repositories.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
jobs:
99
sync:
1010
runs-on: ubuntu-latest
11+
timeout-minutes: 15
1112
# Only run on the main worlddriven organization, not on forks
1213
if: github.repository_owner == 'worlddriven'
1314
permissions:

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
jobs:
1414
test:
1515
runs-on: ubuntu-latest
16+
timeout-minutes: 5
1617

1718
steps:
1819
- name: Checkout code
Lines changed: 198 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,213 @@
11
#!/usr/bin/env node
22

3-
import { describe, test } from 'node:test';
3+
import { describe, test, mock, beforeEach, afterEach } from 'node:test';
44
import assert from 'node:assert';
5-
import { checkTransferPermission } from './check-transfer-permissions.js';
5+
import {
6+
checkTransferPermission,
7+
checkMultipleTransferPermissions,
8+
} from './check-transfer-permissions.js';
69

710
describe('checkTransferPermission', () => {
8-
test('should throw error if token is missing', async () => {
9-
await assert.rejects(
10-
async () => await checkTransferPermission(null, 'owner/repo'),
11-
{ message: 'GitHub token is required' }
12-
);
11+
describe('input validation', () => {
12+
test('should throw error if token is missing', async () => {
13+
await assert.rejects(
14+
async () => await checkTransferPermission(null, 'owner/repo'),
15+
{ message: 'GitHub token is required' }
16+
);
17+
});
18+
19+
test('should throw error if originRepo is missing', async () => {
20+
await assert.rejects(
21+
async () => await checkTransferPermission('token', ''),
22+
{ message: 'Origin repository must be in format "owner/repo-name"' }
23+
);
24+
});
25+
26+
test('should throw error if originRepo format is invalid', async () => {
27+
await assert.rejects(
28+
async () => await checkTransferPermission('token', 'invalid-format'),
29+
{ message: 'Origin repository must be in format "owner/repo-name"' }
30+
);
31+
});
32+
33+
test('should throw error if originRepo has empty owner or repo', async () => {
34+
await assert.rejects(
35+
async () => await checkTransferPermission('token', '/repo'),
36+
{ message: 'Invalid origin repository format' }
37+
);
38+
39+
await assert.rejects(
40+
async () => await checkTransferPermission('token', 'owner/'),
41+
{ message: 'Invalid origin repository format' }
42+
);
43+
});
1344
});
1445

15-
test('should throw error if originRepo is missing', async () => {
16-
await assert.rejects(
17-
async () => await checkTransferPermission('token', ''),
18-
{ message: 'Origin repository must be in format "owner/repo-name"' }
19-
);
46+
describe('API response handling', () => {
47+
let originalFetch;
48+
49+
beforeEach(() => {
50+
originalFetch = globalThis.fetch;
51+
});
52+
53+
afterEach(() => {
54+
globalThis.fetch = originalFetch;
55+
});
56+
57+
test('should return hasPermission true when user has admin access', async () => {
58+
globalThis.fetch = mock.fn(() =>
59+
Promise.resolve({
60+
ok: true,
61+
status: 200,
62+
json: () =>
63+
Promise.resolve({
64+
permissions: { admin: true, push: true, pull: true },
65+
}),
66+
})
67+
);
68+
69+
const result = await checkTransferPermission('test-token', 'owner/repo');
70+
71+
assert.strictEqual(result.hasPermission, true);
72+
assert.strictEqual(result.permissionLevel, 'admin');
73+
assert.ok(result.details.includes('admin access'));
74+
});
75+
76+
test('should return hasPermission false when user has write access only', async () => {
77+
globalThis.fetch = mock.fn(() =>
78+
Promise.resolve({
79+
ok: true,
80+
status: 200,
81+
json: () =>
82+
Promise.resolve({
83+
permissions: { admin: false, push: true, pull: true },
84+
}),
85+
})
86+
);
87+
88+
const result = await checkTransferPermission('test-token', 'owner/repo');
89+
90+
assert.strictEqual(result.hasPermission, false);
91+
assert.strictEqual(result.permissionLevel, 'write');
92+
assert.ok(result.details.includes('admin required'));
93+
});
94+
95+
test('should return hasPermission false when user has read access only', async () => {
96+
globalThis.fetch = mock.fn(() =>
97+
Promise.resolve({
98+
ok: true,
99+
status: 200,
100+
json: () =>
101+
Promise.resolve({
102+
permissions: { admin: false, push: false, pull: true },
103+
}),
104+
})
105+
);
106+
107+
const result = await checkTransferPermission('test-token', 'owner/repo');
108+
109+
assert.strictEqual(result.hasPermission, false);
110+
assert.strictEqual(result.permissionLevel, 'read');
111+
});
112+
113+
test('should handle 404 response for non-existent repository', async () => {
114+
globalThis.fetch = mock.fn(() =>
115+
Promise.resolve({
116+
ok: false,
117+
status: 404,
118+
})
119+
);
120+
121+
const result = await checkTransferPermission('test-token', 'owner/repo');
122+
123+
assert.strictEqual(result.hasPermission, false);
124+
assert.strictEqual(result.permissionLevel, 'none');
125+
assert.ok(result.details.includes('not found'));
126+
});
127+
128+
test('should handle API errors gracefully', async () => {
129+
globalThis.fetch = mock.fn(() =>
130+
Promise.resolve({
131+
ok: false,
132+
status: 403,
133+
text: () => Promise.resolve('Rate limit exceeded'),
134+
})
135+
);
136+
137+
const result = await checkTransferPermission('test-token', 'owner/repo');
138+
139+
assert.strictEqual(result.hasPermission, false);
140+
assert.strictEqual(result.permissionLevel, 'unknown');
141+
assert.ok(result.details.includes('403'));
142+
});
143+
144+
test('should handle network errors gracefully', async () => {
145+
globalThis.fetch = mock.fn(() =>
146+
Promise.reject(new Error('Network error'))
147+
);
148+
149+
const result = await checkTransferPermission('test-token', 'owner/repo');
150+
151+
assert.strictEqual(result.hasPermission, false);
152+
assert.strictEqual(result.permissionLevel, 'error');
153+
assert.ok(result.details.includes('Network error'));
154+
});
155+
156+
test('should handle missing permissions object in response', async () => {
157+
globalThis.fetch = mock.fn(() =>
158+
Promise.resolve({
159+
ok: true,
160+
status: 200,
161+
json: () => Promise.resolve({}),
162+
})
163+
);
164+
165+
const result = await checkTransferPermission('test-token', 'owner/repo');
166+
167+
assert.strictEqual(result.hasPermission, false);
168+
assert.strictEqual(result.permissionLevel, 'none');
169+
});
170+
});
171+
});
172+
173+
describe('checkMultipleTransferPermissions', () => {
174+
let originalFetch;
175+
176+
beforeEach(() => {
177+
originalFetch = globalThis.fetch;
20178
});
21179

22-
test('should throw error if originRepo format is invalid', async () => {
23-
await assert.rejects(
24-
async () => await checkTransferPermission('token', 'invalid-format'),
25-
{ message: 'Origin repository must be in format "owner/repo-name"' }
26-
);
180+
afterEach(() => {
181+
globalThis.fetch = originalFetch;
27182
});
28183

29-
test('should throw error if originRepo has empty owner or repo', async () => {
30-
await assert.rejects(
31-
async () => await checkTransferPermission('token', '/repo'),
32-
{ message: 'Invalid origin repository format' }
33-
);
184+
test('should return a Map with results for all repositories', async () => {
185+
let callCount = 0;
186+
globalThis.fetch = mock.fn(() => {
187+
callCount++;
188+
return Promise.resolve({
189+
ok: true,
190+
status: 200,
191+
json: () =>
192+
Promise.resolve({
193+
permissions: { admin: callCount === 1, push: true, pull: true },
194+
}),
195+
});
196+
});
34197

35-
await assert.rejects(
36-
async () => await checkTransferPermission('token', 'owner/'),
37-
{ message: 'Invalid origin repository format' }
38-
);
198+
const repos = ['owner/repo1', 'owner/repo2'];
199+
const results = await checkMultipleTransferPermissions('test-token', repos);
200+
201+
assert.ok(results instanceof Map);
202+
assert.strictEqual(results.size, 2);
203+
assert.strictEqual(results.get('owner/repo1').hasPermission, true);
204+
assert.strictEqual(results.get('owner/repo2').hasPermission, false);
39205
});
40206

41-
// Note: The following tests would require mocking the fetch API
42-
// or using a test GitHub token with known repositories.
43-
// For now, we document the expected behavior:
44-
45-
/**
46-
* Test case for admin permission (success scenario):
47-
* - Repository exists
48-
* - worlddriven has admin access
49-
* - Expected result:
50-
* {
51-
* hasPermission: true,
52-
* permissionLevel: 'admin',
53-
* details: '✅ worlddriven has admin access to owner/repo'
54-
* }
55-
*/
56-
57-
/**
58-
* Test case for write permission (insufficient):
59-
* - Repository exists
60-
* - worlddriven has write (but not admin) access
61-
* - Expected result:
62-
* {
63-
* hasPermission: false,
64-
* permissionLevel: 'write',
65-
* details: '❌ worlddriven has "write" access to owner/repo (admin required)'
66-
* }
67-
*/
68-
69-
/**
70-
* Test case for non-existent repository:
71-
* - Repository doesn't exist or worlddriven has no access
72-
* - API returns 404
73-
* - Expected result:
74-
* {
75-
* hasPermission: false,
76-
* permissionLevel: 'none',
77-
* details: 'Repository owner/repo not found or worlddriven has no access'
78-
* }
79-
*/
80-
81-
/**
82-
* Test case for API errors:
83-
* - Network errors, rate limits, etc.
84-
* - Expected result:
85-
* {
86-
* hasPermission: false,
87-
* permissionLevel: 'error' or 'unknown',
88-
* details: 'Error checking permissions: ...'
89-
* }
90-
*/
91-
});
207+
test('should return empty Map for empty input array', async () => {
208+
const results = await checkMultipleTransferPermissions('test-token', []);
92209

93-
// To run integration tests with actual GitHub API:
94-
// 1. Set WORLDDRIVEN_GITHUB_TOKEN environment variable
95-
// 2. Create test repositories with known permission levels
96-
// 3. Run: node --test scripts/check-transfer-permissions.test.js
210+
assert.ok(results instanceof Map);
211+
assert.strictEqual(results.size, 0);
212+
});
213+
});

scripts/parse-repositories.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* Returns an array of repository objects
66
*/
77

8-
import { readFile } from 'fs/promises';
8+
import { readFile, access } from 'fs/promises';
9+
import { constants } from 'fs';
910
import { fileURLToPath } from 'url';
1011
import { dirname, join } from 'path';
1112

@@ -102,6 +103,14 @@ function parseRepositories(content) {
102103
async function parseRepositoriesFile() {
103104
const repoFilePath = join(__dirname, '..', 'REPOSITORIES.md');
104105

106+
// Validate file exists before attempting to read
107+
try {
108+
await access(repoFilePath, constants.R_OK);
109+
} catch {
110+
console.error(`Error: REPOSITORIES.md not found at ${repoFilePath}`);
111+
process.exit(1);
112+
}
113+
105114
try {
106115
const content = await readFile(repoFilePath, 'utf-8');
107116
return parseRepositories(content);

0 commit comments

Comments
 (0)