forked from heavy-duty/stoke
Validate non-APPROVED reviews using trim().length, but send the original unmodified body to the API so Markdown whitespace is preserved.
181 lines
7.9 KiB
JavaScript
181 lines
7.9 KiB
JavaScript
const { test, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { ForgejoClient } = require('../src/api');
|
|
const pkg = require('../package.json');
|
|
|
|
const realFetch = global.fetch;
|
|
|
|
afterEach(() => {
|
|
global.fetch = realFetch;
|
|
});
|
|
|
|
function mockFetch(handler) {
|
|
const calls = [];
|
|
global.fetch = async (url, opts) => {
|
|
calls.push({ url, opts });
|
|
return handler(url, opts, calls.length);
|
|
};
|
|
return calls;
|
|
}
|
|
|
|
function jsonResponse(body, status = 200) {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
text: async () => JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
test('fromConfig rejects missing credentials with a stoke-branded hint', () => {
|
|
assert.throws(() => ForgejoClient.fromConfig(null), /stoke auth login/);
|
|
assert.throws(() => ForgejoClient.fromConfig({ url: 'https://x' }), /stoke auth login/);
|
|
});
|
|
|
|
test('trailing slash in base URL is normalized', async () => {
|
|
const calls = mockFetch(() => jsonResponse({ ok: true }));
|
|
const client = new ForgejoClient('https://forge.test/', 'tok');
|
|
await client.get('/user');
|
|
assert.equal(calls[0].url, 'https://forge.test/api/v1/user');
|
|
});
|
|
|
|
test('token auth wins and User-Agent matches the package', async () => {
|
|
const calls = mockFetch(() => jsonResponse({}));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await client.get('/user');
|
|
const headers = calls[0].opts.headers;
|
|
assert.equal(headers.Authorization, 'token tok');
|
|
assert.equal(headers['User-Agent'], `stoke/${pkg.version}`);
|
|
});
|
|
|
|
test('withBasicAuth sends Basic credentials and drops the token', async () => {
|
|
const calls = mockFetch(() => jsonResponse({}));
|
|
const client = new ForgejoClient('https://forge.test', 'tok').withBasicAuth('user', 'pass');
|
|
await client.get('/user');
|
|
const expected = `Basic ${Buffer.from('user:pass').toString('base64')}`;
|
|
assert.equal(calls[0].opts.headers.Authorization, expected);
|
|
});
|
|
|
|
test('deleteToken uses Basic auth (Forgejo rejects token auth on token endpoints)', async () => {
|
|
const calls = mockFetch(() => jsonResponse(null, 204));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await client.deleteToken('user@example.test', 'pass', 'user', 42);
|
|
const { url, opts } = calls[0];
|
|
assert.equal(url, 'https://forge.test/api/v1/users/user/tokens/42');
|
|
assert.equal(opts.method, 'DELETE');
|
|
assert.match(opts.headers.Authorization, /^Basic /);
|
|
});
|
|
|
|
test('API errors carry message, status and body', async () => {
|
|
mockFetch(() => jsonResponse({ message: 'user does not exist', url: 'https://forge.test/api/swagger' }, 404));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await assert.rejects(() => client.get('/users/ghost'), (err) => {
|
|
assert.equal(err.message, 'user does not exist');
|
|
assert.equal(err.status, 404);
|
|
assert.equal(err.body.url, 'https://forge.test/api/swagger');
|
|
return true;
|
|
});
|
|
});
|
|
|
|
test('non-JSON error bodies are surfaced raw', async () => {
|
|
mockFetch(() => ({ ok: false, status: 502, text: async () => 'Bad Gateway' }));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await assert.rejects(() => client.get('/user'), /Bad Gateway/);
|
|
});
|
|
|
|
test('network failures are wrapped with the base URL', async () => {
|
|
global.fetch = async () => { throw new Error('ECONNREFUSED'); };
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await assert.rejects(() => client.get('/user'), /Network error reaching https:\/\/forge\.test/);
|
|
});
|
|
|
|
test('getAll paginates until a short page', async () => {
|
|
const pageOf = (n, count) => Array.from({ length: count }, (_, i) => ({ id: (n - 1) * 50 + i }));
|
|
mockFetch((url) => {
|
|
const page = Number(new URL(url).searchParams.get('page'));
|
|
if (page === 1) return jsonResponse(pageOf(1, 50));
|
|
if (page === 2) return jsonResponse(pageOf(2, 3));
|
|
throw new Error('should not fetch beyond a short page');
|
|
});
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
const all = await client.getAll('/user/repos');
|
|
assert.equal(all.length, 53);
|
|
});
|
|
|
|
test('getAll stops on an empty first page', async () => {
|
|
const calls = mockFetch(() => jsonResponse([]));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
const all = await client.getAll('/user/repos');
|
|
assert.deepEqual(all, []);
|
|
assert.equal(calls.length, 1);
|
|
});
|
|
|
|
test('searchUsers unwraps the {data: []} envelope and paginates', async () => {
|
|
mockFetch((url) => {
|
|
const page = Number(new URL(url).searchParams.get('page'));
|
|
if (page === 1) return jsonResponse({ data: Array.from({ length: 50 }, (_, i) => ({ login: `u${i}` })) });
|
|
return jsonResponse({ data: [{ login: 'last' }] });
|
|
});
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
const users = await client.searchUsers('u');
|
|
assert.equal(users.length, 51);
|
|
assert.equal(users.at(-1).login, 'last');
|
|
});
|
|
|
|
test('mergePullRequest posts the merge payload to the merge endpoint', async () => {
|
|
const calls = mockFetch(() => jsonResponse(null, 200));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await client.mergePullRequest('owner', 'repo', 7, { Do: 'squash', delete_branch_after_merge: true });
|
|
assert.equal(calls[0].url, 'https://forge.test/api/v1/repos/owner/repo/pulls/7/merge');
|
|
assert.equal(calls[0].opts.method, 'POST');
|
|
assert.deepEqual(JSON.parse(calls[0].opts.body), { Do: 'squash', delete_branch_after_merge: true });
|
|
});
|
|
|
|
test('createIssue and createPullRequest hit the expected endpoints', async () => {
|
|
const calls = mockFetch(() => jsonResponse({ number: 1 }));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await client.createIssue('own/er', 'repo', { title: 't' });
|
|
await client.createPullRequest('owner', 're po', { title: 't', head: 'h', base: 'b' });
|
|
assert.equal(calls[0].url, 'https://forge.test/api/v1/repos/own%2Fer/repo/issues');
|
|
assert.equal(calls[1].url, 'https://forge.test/api/v1/repos/owner/re%20po/pulls');
|
|
assert.equal(calls[0].opts.method, 'POST');
|
|
assert.equal(JSON.parse(calls[1].opts.body).head, 'h');
|
|
});
|
|
|
|
test('getPullRequest fetches a single pull request', async () => {
|
|
const calls = mockFetch(() => jsonResponse({ number: 7, title: 'Fix' }));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
const pr = await client.getPullRequest('owner', 'repo', 7);
|
|
assert.equal(pr.number, 7);
|
|
assert.equal(calls[0].url, 'https://forge.test/api/v1/repos/owner/repo/pulls/7');
|
|
assert.equal(calls[0].opts.method, 'GET');
|
|
});
|
|
|
|
test('createPullRequestComment posts to the issue comments endpoint', async () => {
|
|
const calls = mockFetch(() => jsonResponse({ id: 99, html_url: 'https://forge.test/comment/99' }));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await client.createPullRequestComment('owner', 'repo', 7, 'Looks good.');
|
|
assert.equal(calls[0].url, 'https://forge.test/api/v1/repos/owner/repo/issues/7/comments');
|
|
assert.equal(calls[0].opts.method, 'POST');
|
|
assert.equal(JSON.parse(calls[0].opts.body).body, 'Looks good.');
|
|
});
|
|
|
|
test('createPullRequestReview posts the review event and body', async () => {
|
|
const calls = mockFetch(() => jsonResponse({ id: 88 }));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
await client.createPullRequestReview('owner', 'repo', 7, 'APPROVED', 'Ship it.');
|
|
assert.equal(calls[0].url, 'https://forge.test/api/v1/repos/owner/repo/pulls/7/reviews');
|
|
assert.equal(calls[0].opts.method, 'POST');
|
|
const body = JSON.parse(calls[0].opts.body);
|
|
assert.equal(body.event, 'APPROVED');
|
|
assert.equal(body.body, 'Ship it.');
|
|
});
|
|
|
|
test('createPullRequestReview preserves leading and trailing whitespace in the body', async () => {
|
|
const calls = mockFetch(() => jsonResponse({ id: 89 }));
|
|
const client = new ForgejoClient('https://forge.test', 'tok');
|
|
const rawBody = ' code block prefix\n';
|
|
await client.createPullRequestReview('owner', 'repo', 8, 'REQUEST_CHANGES', rawBody);
|
|
const body = JSON.parse(calls[0].opts.body);
|
|
assert.equal(body.body, rawBody);
|
|
});
|