CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m5s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m34s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 5m16s
118 lines
4.1 KiB
JavaScript
118 lines
4.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import process from 'node:process';
|
|
import test from 'node:test';
|
|
import { conditionalEtag } from '../publish-macos.mjs';
|
|
|
|
test(
|
|
'Hetzner conditional writes accept a bare ETag and reject a stale ETag',
|
|
{
|
|
skip: process.env.KANBANGIT_S3_INTEGRATION !== '1',
|
|
},
|
|
() => {
|
|
const bucket = process.env.AWS_BUCKET;
|
|
assert(bucket, 'Set AWS_BUCKET to a test bucket');
|
|
const directory = mkdtempSync(join(tmpdir(), 'kanbangit-s3-etag-'));
|
|
const key = `kanbangit-integration-tests/${randomUUID()}.txt`;
|
|
const file = join(directory, 'object.txt');
|
|
const downloaded = join(directory, 'downloaded.txt');
|
|
const run = (operation, args) => {
|
|
const result = spawnSync(
|
|
'aws',
|
|
[
|
|
'--debug',
|
|
'--endpoint-url',
|
|
process.env.AWS_ENDPOINT,
|
|
'--region',
|
|
process.env.AWS_DEFAULT_REGION,
|
|
'--no-cli-pager',
|
|
's3api',
|
|
operation,
|
|
'--bucket',
|
|
bucket,
|
|
'--key',
|
|
key,
|
|
...args,
|
|
],
|
|
{ encoding: 'utf8', timeout: 30000 },
|
|
);
|
|
// Never expose debug headers: they contain the signed Authorization header.
|
|
return {
|
|
status: result.status,
|
|
stdout: result.stdout,
|
|
codes: [
|
|
...(result.stderr || '').matchAll(/<Code>([^<]*)<\/Code>/g),
|
|
].map((match) => match[1]),
|
|
};
|
|
};
|
|
let created = false;
|
|
try {
|
|
writeFileSync(file, 'first');
|
|
const initial = run('put-object', [
|
|
'--body',
|
|
file,
|
|
'--if-none-match',
|
|
'*',
|
|
]);
|
|
assert.equal(initial.status, 0, `Create failed: ${initial.codes}`);
|
|
created = true;
|
|
const read = run('get-object', ['--output', 'json', downloaded]);
|
|
assert.equal(read.status, 0, `Read failed: ${read.codes}`);
|
|
const etag = JSON.parse(read.stdout).ETag;
|
|
const quoted = run('put-object', [
|
|
'--body',
|
|
file,
|
|
'--if-match',
|
|
etag,
|
|
]);
|
|
console.log(
|
|
`Quoted ETag result: exit ${quoted.status}, S3 ${quoted.codes.join(', ')}`,
|
|
);
|
|
const bare = conditionalEtag(etag);
|
|
writeFileSync(file, 'second');
|
|
const updated = run('put-object', [
|
|
'--body',
|
|
file,
|
|
'--if-match',
|
|
bare,
|
|
]);
|
|
assert.equal(
|
|
updated.status,
|
|
0,
|
|
`Bare ETag replacement failed: ${updated.codes}`,
|
|
);
|
|
writeFileSync(file, 'stale writer');
|
|
const stale = run('put-object', [
|
|
'--body',
|
|
file,
|
|
'--if-match',
|
|
bare,
|
|
]);
|
|
assert.notEqual(stale.status, 0);
|
|
assert(
|
|
stale.codes.includes('PreconditionFailed'),
|
|
`Unexpected stale error: ${stale.codes}`,
|
|
);
|
|
assert.equal(run('get-object', [downloaded]).status, 0);
|
|
assert.equal(readFileSync(downloaded, 'utf8'), 'second');
|
|
} finally {
|
|
try {
|
|
if (created) {
|
|
const cleanup = run('delete-object', []);
|
|
assert.equal(
|
|
cleanup.status,
|
|
0,
|
|
`Cleanup failed: ${cleanup.codes}`,
|
|
);
|
|
}
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
},
|
|
);
|