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>/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 }); } } }, );