68 lines
2.7 KiB
JavaScript
68 lines
2.7 KiB
JavaScript
import { createHmac } from 'node:crypto';
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { syncImageRepository } from './sync-image-repository.mjs';
|
|
|
|
test('signs a scoped Core2026 fallback sync without exposing the secret', async (t) => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), 'sammo-image-sync-'));
|
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
const secretFile = path.join(directory, 'secret');
|
|
const secret = 's'.repeat(32);
|
|
await writeFile(secretFile, `${secret}\n`, { mode: 0o600 });
|
|
let captured;
|
|
const result = await syncImageRepository({
|
|
baseUrl: 'https://sam-image.hided.net/',
|
|
secretFile,
|
|
commit: 'a'.repeat(40),
|
|
now: () => Date.parse('2026-08-08T00:00:00Z'),
|
|
requestIdFactory: () => 'request-1',
|
|
fetchImpl: async (url, init) => {
|
|
captured = { url, init };
|
|
return new Response(JSON.stringify({ ok: true, changed: false }), { status: 200 });
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(result, { ok: true, changed: false });
|
|
assert.equal(captured.url, 'https://sam-image.hided.net/v1/sync');
|
|
assert.equal(captured.init.body, JSON.stringify({ commit: 'a'.repeat(40) }));
|
|
assert.equal(captured.init.headers['x-image-client'], 'core2026');
|
|
assert.equal(
|
|
captured.init.headers['x-image-signature'],
|
|
createHmac('sha256', secret)
|
|
.update(`${captured.init.headers['x-image-timestamp']}.request-1.${captured.init.body}`)
|
|
.digest('hex')
|
|
);
|
|
assert.equal(Object.values(captured.init.headers).includes(secret), false);
|
|
});
|
|
|
|
test('rejects invalid commits before reading the secret or making a request', async () => {
|
|
await assert.rejects(
|
|
syncImageRepository({
|
|
secretFile: '/does/not/exist',
|
|
commit: 'main',
|
|
fetchImpl: async () => {
|
|
throw new Error('must not fetch');
|
|
},
|
|
}),
|
|
/Commit must be a full/
|
|
);
|
|
});
|
|
|
|
test('reports authentication failures without returning a response body', async (t) => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), 'sammo-image-sync-'));
|
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
const secretFile = path.join(directory, 'secret');
|
|
await writeFile(secretFile, 's'.repeat(32), { mode: 0o600 });
|
|
await assert.rejects(
|
|
syncImageRepository({
|
|
secretFile,
|
|
fetchImpl: async () => new Response('{"reason":"sensitive detail"}', { status: 401 }),
|
|
}),
|
|
/HTTP 401/
|
|
);
|
|
});
|