feat: add scoped image sync endpoint
This commit is contained in:
@@ -124,3 +124,19 @@ test('same-branch force pushes and payload SHA mismatches are rejected', async (
|
||||
/Payload commit does not match remote branch tip/,
|
||||
);
|
||||
});
|
||||
|
||||
test('signed sync callers can only fast-forward the active branch and requests are idempotent', async (t) => {
|
||||
const f = await fixture();
|
||||
t.after(() => rm(f.root, { recursive: true, force: true }));
|
||||
await writeFile(join(f.seed, 'icons', 'sync.jpg'), 'sync');
|
||||
await git(f.seed, 'add', '.');
|
||||
await git(f.seed, 'commit', '-m', 'sync target');
|
||||
await git(f.seed, 'push', 'origin', 'master');
|
||||
const target = await git(f.seed, 'rev-parse', 'HEAD');
|
||||
|
||||
const result = await f.service.deploySync({ requestKey: 'core:sync-request-1', expectedCommit: target });
|
||||
assert.equal(result.changed, true);
|
||||
assert.equal(await git(f.deployed, 'rev-parse', 'HEAD'), target);
|
||||
assert.equal((await f.service.deploySync({ requestKey: 'core:sync-request-1' })).duplicate, true);
|
||||
assert.equal(f.service.publicStatus().activeBranch, 'master');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { once } from 'node:events';
|
||||
import test from 'node:test';
|
||||
import { adminSignature } from '../src/auth.mjs';
|
||||
import { createApp } from '../src/server.mjs';
|
||||
|
||||
test('sync endpoint authenticates a scoped caller and passes only an optional commit', async (t) => {
|
||||
const calls = [];
|
||||
const service = {
|
||||
async initialize() {},
|
||||
async deploySync(value) {
|
||||
calls.push(value);
|
||||
return { changed: false };
|
||||
},
|
||||
async recordError() {},
|
||||
};
|
||||
const secret = 's'.repeat(32);
|
||||
const { server } = await createApp({
|
||||
maxBodyBytes: 4096,
|
||||
syncClientSecrets: { core: secret },
|
||||
}, { service });
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
t.after(() => server.close());
|
||||
const address = server.address();
|
||||
const body = Buffer.from(JSON.stringify({ commit: 'a'.repeat(40) }));
|
||||
const timestamp = String(Date.now());
|
||||
const requestId = 'sync-request-1234';
|
||||
const response = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-image-client': 'core',
|
||||
'x-image-timestamp': timestamp,
|
||||
'x-image-request-id': requestId,
|
||||
'x-image-signature': adminSignature(secret, timestamp, requestId, body),
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(calls, [{ requestKey: `core:${requestId}`, expectedCommit: 'a'.repeat(40) }]);
|
||||
});
|
||||
|
||||
test('sync endpoint rejects unknown callers and body fields outside the sync contract', async (t) => {
|
||||
const service = {
|
||||
async initialize() {},
|
||||
async deploySync() { throw new Error('must not deploy'); },
|
||||
async recordError() {},
|
||||
};
|
||||
const secret = 's'.repeat(32);
|
||||
const { server } = await createApp({ maxBodyBytes: 4096, syncClientSecrets: { core: secret } }, { service });
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
t.after(() => server.close());
|
||||
const address = server.address();
|
||||
const body = Buffer.from(JSON.stringify({ branch: 'preview' }));
|
||||
const timestamp = String(Date.now());
|
||||
const requestId = 'sync-request-5678';
|
||||
const signedHeaders = {
|
||||
'content-type': 'application/json',
|
||||
'x-image-timestamp': timestamp,
|
||||
'x-image-request-id': requestId,
|
||||
'x-image-signature': adminSignature(secret, timestamp, requestId, body),
|
||||
};
|
||||
|
||||
const unknown = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
||||
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'unknown' }, body,
|
||||
});
|
||||
assert.equal(unknown.status, 401);
|
||||
const prototypeName = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
||||
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'toString' }, body,
|
||||
});
|
||||
assert.equal(prototypeName.status, 401);
|
||||
const extraField = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
||||
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'core' }, body,
|
||||
});
|
||||
assert.equal(extraField.status, 400);
|
||||
});
|
||||
Reference in New Issue
Block a user