197 lines
8.8 KiB
JavaScript
197 lines
8.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import giteaModule from '../src/main/gitea-service.cjs';
|
|
|
|
const { GiteaService } = giteaModule;
|
|
|
|
function makeStore() {
|
|
return { data: { gitea: { baseUrl: 'https://gitea.example.test' } }, getToken: () => 'demo-token' };
|
|
}
|
|
|
|
test('normalizes run payloads from different Actions API shapes', () => {
|
|
const service = new GiteaService(makeStore());
|
|
const run = service.normalizeRun({
|
|
task_id: 42,
|
|
index: 7,
|
|
workflow_name: 'Deploy',
|
|
status: 'success',
|
|
commit: { sha: 'a'.repeat(40) },
|
|
ref: 'refs/heads/main',
|
|
workflow_file: '.gitea/workflows/deploy.yml',
|
|
start_time: '2026-07-24T12:00:00Z'
|
|
});
|
|
assert.equal(run.id, 42);
|
|
assert.equal(run.runNumber, 7);
|
|
assert.equal(run.conclusion, 'success');
|
|
assert.equal(run.headSha, 'a'.repeat(40));
|
|
assert.equal(run.headBranch, 'refs/heads/main');
|
|
});
|
|
|
|
test('retries Actions runs without optional filters when a server rejects them', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
const calls = [];
|
|
service.request = async (pathname) => {
|
|
calls.push(pathname);
|
|
if (calls.length === 1) {
|
|
const error = new Error('Unsupported query');
|
|
error.status = 422;
|
|
throw error;
|
|
}
|
|
return { data: { workflow_runs: [{ id: 11, run_number: 11, status: 'queued', head_sha: 'b'.repeat(40), head_branch: 'main' }] } };
|
|
};
|
|
const result = await service.listWorkflowRuns({ owner: 'jens', repo: 'app', sha: 'b'.repeat(40), branch: 'main' });
|
|
assert.equal(calls.length, 2);
|
|
assert.match(calls[0], /head_sha=/);
|
|
assert.doesNotMatch(calls[1], /head_sha=/);
|
|
assert.equal(result.source, 'runs');
|
|
assert.equal(result.runs[0].runNumber, 11);
|
|
});
|
|
|
|
test('falls back to legacy Actions tasks endpoint when runs is unavailable', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
const calls = [];
|
|
service.request = async (pathname) => {
|
|
calls.push(pathname);
|
|
if (pathname.includes('/runs?')) {
|
|
const error = new Error('Not found');
|
|
error.status = 404;
|
|
throw error;
|
|
}
|
|
return { data: [{ task_id: 9, index: 3, status: 'running', commit_sha: 'c'.repeat(40), branch: 'main' }] };
|
|
};
|
|
const result = await service.listWorkflowRuns({ owner: 'jens', repo: 'app' });
|
|
assert.equal(result.source, 'tasks');
|
|
assert.equal(result.runs[0].id, 9);
|
|
assert.ok(calls.some((pathname) => pathname.includes('/tasks?')));
|
|
});
|
|
|
|
test('selects the newest matching workflow run', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
service.listWorkflowRuns = async () => ({ source: 'runs', runs: [
|
|
{ id: 1, headSha: 'd'.repeat(40), headBranch: 'main', workflowPath: 'deploy.yml', createdAt: '2026-07-24T10:00:00Z' },
|
|
{ id: 2, headSha: 'd'.repeat(40), headBranch: 'main', workflowPath: '.gitea/workflows/deploy.yml', createdAt: '2026-07-24T11:00:00Z' },
|
|
{ id: 3, headSha: 'e'.repeat(40), headBranch: 'main', workflowPath: 'deploy.yml', createdAt: '2026-07-24T12:00:00Z' }
|
|
] });
|
|
const found = await service.findWorkflowRun({ owner: 'jens', repo: 'app', sha: 'd'.repeat(40), branch: 'main', workflowFile: 'deploy.yml' });
|
|
assert.equal(found.run.id, 2);
|
|
const excludingNewest = await service.findWorkflowRun({ owner: 'jens', repo: 'app', sha: 'd'.repeat(40), branch: 'main', workflowFile: 'deploy.yml', excludeRunIds: [2] });
|
|
assert.equal(excludingNewest.run.id, 1);
|
|
});
|
|
|
|
test('checks repository workflow files through the contents API', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
const calls = [];
|
|
service.request = async (pathname) => { calls.push(pathname); return { data: { type: 'file' } }; };
|
|
assert.equal(await service.repositoryFileExists({ owner: 'jens', repo: 'app', filePath: '.gitea/workflows/deploy.yml', ref: 'main' }), true);
|
|
assert.match(calls[0], /contents\/\.gitea\/workflows\/deploy\.yml\?ref=main/);
|
|
|
|
service.request = async () => { const error = new Error('missing'); error.status = 404; throw error; };
|
|
assert.equal(await service.repositoryFileExists({ owner: 'jens', repo: 'app', filePath: '.gitea/workflows/missing.yml', ref: 'main' }), false);
|
|
});
|
|
|
|
test('creates controlled pull requests and reads branch protection', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
const calls = [];
|
|
service.request = async (pathname, options = {}) => {
|
|
calls.push({ pathname, options });
|
|
if (pathname.includes('/branches/main')) return { data: { name: 'main', protected: true } };
|
|
if (pathname.endsWith('/branch_protections')) return { data: [{ branch_name: 'main', required_approvals: 2, require_signed_commits: true }] };
|
|
return { data: { number: 12, html_url: 'https://gitea.test/owner/app/pulls/12' } };
|
|
};
|
|
const protection = await service.getBranchProtection('owner', 'app', 'main');
|
|
assert.equal(protection.protected, true);
|
|
assert.equal(protection.requiredApprovals, 2);
|
|
const pull = await service.createPullRequest({ owner: 'owner', repo: 'app', head: 'feature', base: 'main', title: 'Release feature', body: 'Summary' });
|
|
assert.equal(pull.number, 12);
|
|
const create = calls.find((call) => call.options.method === 'POST');
|
|
assert.deepEqual(create.options.body, { head: 'feature', base: 'main', title: 'Release feature', body: 'Summary' });
|
|
await assert.rejects(() => service.createPullRequest({ owner: 'owner', repo: 'app', head: 'main', base: 'main', title: 'Invalid' }), /different/);
|
|
});
|
|
|
|
test('resolves release attachment metadata before downloading the actual asset', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
let metadataPath = '';
|
|
let requested = '';
|
|
service.request = async (pathname) => {
|
|
metadataPath = pathname;
|
|
return {
|
|
data: {
|
|
id: 412,
|
|
browser_download_url: 'https://gitea.example.test/attachments/release.exe',
|
|
},
|
|
};
|
|
};
|
|
service.downloadAuthenticated = async (pathname) => {
|
|
requested = pathname;
|
|
return Buffer.from('asset');
|
|
};
|
|
const asset = await service.downloadReleaseAsset('Jens', 'ForgeFlow', 107, 412);
|
|
assert.equal(asset.toString(), 'asset');
|
|
assert.equal(
|
|
metadataPath,
|
|
'/repos/Jens/ForgeFlow/releases/107/assets/412',
|
|
);
|
|
assert.equal(
|
|
requested,
|
|
'https://gitea.example.test/attachments/release.exe',
|
|
);
|
|
await assert.rejects(
|
|
() => service.downloadReleaseAsset('Jens', 'ForgeFlow', null, 412),
|
|
/invalid release ID/,
|
|
);
|
|
});
|
|
|
|
test('uses a release-provided browser download URL without requesting metadata again', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
service.request = async () => { throw new Error('metadata lookup should not run'); };
|
|
let requested = '';
|
|
service.downloadAuthenticated = async (pathname) => {
|
|
requested = pathname;
|
|
return Buffer.from('asset');
|
|
};
|
|
const asset = await service.downloadReleaseAsset('Jens', 'ForgeFlow', 107, 412, {
|
|
downloadUrl: 'https://gitea.example.test/attachments/direct.exe',
|
|
});
|
|
assert.equal(asset.toString(), 'asset');
|
|
assert.equal(requested, 'https://gitea.example.test/attachments/direct.exe');
|
|
});
|
|
|
|
test('creates conservative default branch protection rules', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
let request = null;
|
|
service.request = async (pathname, options) => {
|
|
request = { pathname, options };
|
|
return { data: { rule_name: 'main' } };
|
|
};
|
|
const result = await service.createBranchProtection('jens', 'app', 'main');
|
|
assert.equal(result.rule_name, 'main');
|
|
assert.equal(request.options.method, 'POST');
|
|
assert.equal(request.options.body.enable_push, false);
|
|
assert.equal(request.options.body.enable_force_push, false);
|
|
assert.equal(request.options.body.rule_name, 'main');
|
|
});
|
|
|
|
test('creates repository-scoped read-only deploy keys and reuses only safe matches', async () => {
|
|
const service = new GiteaService(makeStore());
|
|
const publicKey = `ssh-ed25519 ${Buffer.from('public-key-material').toString('base64')} forgeflow:test`;
|
|
const requests = [];
|
|
service.request = async (pathname, options = {}) => {
|
|
requests.push({ pathname, options });
|
|
if (!options.method) return { data: [] };
|
|
return { data: { id: 41, key: publicKey, read_only: true } };
|
|
};
|
|
const created = await service.ensureReadOnlyDeployKey({ owner: 'jens', repo: 'app', title: 'ForgeFlow', publicKey });
|
|
assert.equal(created.created, true);
|
|
assert.equal(requests[1].options.body.read_only, true);
|
|
|
|
service.request = async () => ({ data: [{ id: 41, key: publicKey, read_only: true }] });
|
|
const reused = await service.ensureReadOnlyDeployKey({ owner: 'jens', repo: 'app', title: 'ForgeFlow', publicKey });
|
|
assert.equal(reused.created, false);
|
|
|
|
service.request = async () => ({ data: [{ id: 41, key: publicKey, read_only: false }] });
|
|
await assert.rejects(
|
|
() => service.ensureReadOnlyDeployKey({ owner: 'jens', repo: 'app', title: 'ForgeFlow', publicKey }),
|
|
(error) => error.code === 'DEPLOY_KEY_NOT_READ_ONLY',
|
|
);
|
|
});
|