A copy binding grants the repositories its projects' remotes name when a credential is issued, and its decision records only project names. repo set (CLI, route, Settings tab) is now refused with project-repo-bound while a pending or bound copy binding names the project, before any check, origin change or write, and again under the lock. The message names the copies and the next step: revoke the binding, set the repository, raise a new binding decision. An unbound binding, or none, behaves as before. Bindings are read from the loader-validated document; binding, launcher and broker code are unchanged. New refusal-table entry for project-repo-bound.
92 lines
7.7 KiB
JavaScript
92 lines
7.7 KiB
JavaScript
// #1873 P1-6 and P1-7: `org project repo init --remote` sets and checks origin; `org project repo
|
|
// set` changes the repository both ways. The remotes point at 127.0.0.1 port 1, which refuses at
|
|
// once, so the check runs for real and answers "unreachable" without leaving the host.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { createHash } from 'node:crypto';
|
|
import { join, resolve } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
const repo = resolve(import.meta.dirname, '../../..');
|
|
const run = (env, ...args) => spawnSync(join(repo, 'scripts/mosaic'), args, { cwd: repo, env, encoding: 'utf8' });
|
|
const git = (cwd, ...args) => execFileSync('git', ['-C', cwd, ...args], { env: { PATH: process.env.PATH, HOME: '/nonexistent', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' }, encoding: 'utf8' }).trim();
|
|
|
|
test('org project repo init --remote and repo set: origin follows, the check is reported, refusals are plain', () => {
|
|
const home = mkdtempSync(join(tmpdir(), 'org-repo-')); const env = { ...process.env, HOME: home, MOSAIC_CONFIG: join(home, '.config/mosaic-orgtest/config.json') };
|
|
const boot = spawnSync(join(repo, 'scripts/bootstrap.sh'), ['--instance', 'orgtest'], { cwd: repo, env, encoding: 'utf8' }); assert.equal(boot.status, 0, boot.stderr);
|
|
const ok = (...args) => { const r = run(env, ...args); assert.equal(r.status, 0, r.stderr); return JSON.parse(r.stdout); };
|
|
const file = join(home, 'alpha.json');
|
|
run(env, 'org', 'template', 'render', 'alpha', '--fixture', 'local', '--output', file); ok('org', 'create', 'alpha', '--from', file);
|
|
|
|
const ssh = 'ssh://[email protected]:1/team/work.git';
|
|
const init = ok('org', 'project', 'repo', 'init', 'alpha', 'work', '--remote', ssh);
|
|
assert.equal(init.origin, ssh); assert.equal(init.check.result, 'unreachable'); assert.match(init.check.message, /port 1|refused/i);
|
|
assert.equal(git(init.root, 'remote', '-v'), `origin\t${ssh} (fetch)\norigin\t${ssh} (push)`);
|
|
|
|
// Usage errors come first and change nothing.
|
|
for (const args of [['--local-only', '--accept-local-loss-risk'], ['--remote', ssh, '--local-only', '--if-revision', '9'], ['--if-revision', '9'], ['--remote', ssh, '--if-revision', '0']]) {
|
|
const r = run(env, 'org', 'project', 'repo', 'set', 'alpha', 'work', ...args); assert.equal(r.status, 4, `${args}: ${r.stderr}`);
|
|
}
|
|
assert.equal(run(env, 'org', 'queue', 'set', 'alpha', '--local-only', '--accept-local-loss-risk', '--if-revision', '1').status, 4);
|
|
let rev = ok('org', 'show', 'alpha', '--json').revision;
|
|
for (const bad of ['ext::sh -c id', 'file:///etc', '/srv/repo', 'https://user:[email protected]:1/o/r', 'http://127.0.0.1:1/o/r']) {
|
|
const r = run(env, 'org', 'project', 'repo', 'set', 'alpha', 'work', '--remote', bad, '--if-revision', String(rev));
|
|
assert.equal(r.status, 2, bad); assert.match(r.stderr, /org-invalid/, bad);
|
|
}
|
|
const stale = run(env, 'org', 'project', 'repo', 'set', 'alpha', 'work', '--local-only', '--accept-local-loss-risk', '--if-revision', String(rev + 5));
|
|
assert.equal(stale.status, 2); assert.match(stale.stderr, /revision-conflict/);
|
|
assert.equal(git(init.root, 'remote', 'get-url', 'origin'), ssh);
|
|
|
|
const local = ok('org', 'project', 'repo', 'set', 'alpha', 'work', '--local-only', '--accept-local-loss-risk', '--if-revision', String(rev));
|
|
assert.equal(local.op, 'org.project.repo.set'); assert.equal(local.revision, rev + 1); assert.equal(local.origin, null); assert.equal(git(init.root, 'remote', '-v'), '');
|
|
rev = local.revision;
|
|
const https = 'https://127.0.0.1:1/team/work.git';
|
|
const back = ok('org', 'project', 'repo', 'set', 'alpha', 'work', '--remote', https, '--if-revision', String(rev), '--json');
|
|
assert.equal(back.revision, rev + 1); assert.equal(back.check.result, 'unreachable'); assert.match(back.check.message, /Failed to connect|Could not connect/);
|
|
assert.equal(git(init.root, 'remote', 'get-url', 'origin'), https);
|
|
assert.deepEqual(ok('org', 'project', 'repo', 'show', 'alpha', 'work', '--json').current, { mode: 'remote', remote: https, origin: https, revision: back.revision, check: back.check, changedAt: back.at });
|
|
assert.equal(back.check.url, https);
|
|
|
|
// The acceptance URL (#1873 P1-6): an .invalid host never resolves, so the check is unreachable
|
|
// and origin is still set to it.
|
|
const invalid = 'https://git.example.invalid/acme/work';
|
|
const named = ok('org', 'project', 'repo', 'set', 'alpha', 'work', '--remote', invalid, '--if-revision', String(back.revision), '--json');
|
|
assert.equal(named.origin, invalid); assert.equal(named.check.result, 'unreachable'); assert.equal(named.check.url, invalid);
|
|
assert.match(named.check.message, /resolve|timed out/i);
|
|
assert.equal(git(init.root, 'remote', '-v'), `origin\t${invalid} (fetch)\norigin\t${invalid} (push)`);
|
|
});
|
|
|
|
// #1873 F7: while a copy binding that names the project is approved or active, repo set is refused
|
|
// with exit 2 and the next step; once the binding is unbound (revoked) the same set goes through.
|
|
// The bindings are written into the document as the CB1 writer would leave them.
|
|
test('org project repo set is refused under a copy binding that names the project, and allowed after revoke', () => {
|
|
const home = mkdtempSync(join(tmpdir(), 'org-repo-')); const env = { ...process.env, HOME: home, MOSAIC_CONFIG: join(home, '.config/mosaic-orgtest/config.json') };
|
|
const boot = spawnSync(join(repo, 'scripts/bootstrap.sh'), ['--instance', 'orgtest'], { cwd: repo, env, encoding: 'utf8' }); assert.equal(boot.status, 0, boot.stderr);
|
|
const ok = (...args) => { const r = run(env, ...args); assert.equal(r.status, 0, r.stderr); return JSON.parse(r.stdout); };
|
|
const file = join(home, 'alpha.json');
|
|
run(env, 'org', 'template', 'render', 'alpha', '--fixture', 'local', '--output', file); ok('org', 'create', 'alpha', '--from', file);
|
|
const init = ok('org', 'project', 'repo', 'init', 'alpha', 'work', '--local-only', '--accept-local-loss-risk');
|
|
const configDir = join(home, '.config/mosaic-orgtest');
|
|
const seed = (state) => {
|
|
const docFile = join(configDir, 'businesses/alpha.json'); const doc = JSON.parse(readFileSync(docFile, 'utf8'));
|
|
doc.copyBindings = { 'rev.work': { copyId: '1'.repeat(32), orgIncarnation: 'a'.repeat(32), source: { level: 'project', name: 'rev', project: 'work' },
|
|
definition: { ref: 'reviewer', rev: null, sha256: 'b'.repeat(64) }, generation: 1, state,
|
|
services: state === 'unbound' ? {} : { gitea: { slot: 'self', scopes: ['read:repository'], resources: { projects: ['work'] } } },
|
|
decision: '11111111-1111-4111-8111-111111111111' } };
|
|
const raw = JSON.stringify(doc, null, 2) + '\n'; writeFileSync(docFile, raw);
|
|
const regFile = join(configDir, 'org-runtime.json'); const reg = JSON.parse(readFileSync(regFile, 'utf8'));
|
|
reg.orgs.alpha.digest = createHash('sha256').update(raw).digest('hex'); writeFileSync(regFile, JSON.stringify(reg, null, 2) + '\n');
|
|
};
|
|
const invalid = 'https://git.example.invalid/acme/other';
|
|
seed('pending');
|
|
const rev = ok('org', 'show', 'alpha', '--json').revision;
|
|
const refused = run(env, 'org', 'project', 'repo', 'set', 'alpha', 'work', '--remote', invalid, '--if-revision', String(rev));
|
|
assert.equal(refused.status, 2, refused.stderr);
|
|
assert.match(refused.stderr, /project-repo-bound: project work is in a copy binding that still grants it: rev\.work \(approved, not yet verified\)\. Revoke that copy binding first/);
|
|
assert.equal(git(init.root, 'remote', '-v'), '');
|
|
seed('unbound');
|
|
const set = ok('org', 'project', 'repo', 'set', 'alpha', 'work', '--remote', invalid, '--if-revision', String(rev), '--json');
|
|
assert.equal(set.revision, rev + 1); assert.equal(git(init.root, 'remote', 'get-url', 'origin'), invalid);
|
|
});
|