import test from 'node:test'; import assert from 'node:assert/strict'; import validation from '../src/shared/validation.cjs'; import redaction from '../src/main/log-redaction.cjs'; const { normalizeBaseUrl, assertRepositoryRelativePath, assertRepositoryRelativePaths, assertFullCommitSha, assertWorkflowFile, assertWorkflowFileName, assertBranchName, assertEnvironmentName, assertHttpUrl, assertCloneRemote } = validation; const { redactSecrets } = redaction; test('rejects credentials embedded in service URLs', () => { assert.throws(() => normalizeBaseUrl(`https://${['jens', 'secret'].join(':')}@gitea.example.test`), /credentials/i); assert.throws(() => assertHttpUrl(`https://${['user', 'secret'].join(':')}@app.example.test/health`), /credentials/i); }); test('accepts repository-relative paths but blocks escapes and absolute paths', () => { assert.equal(assertRepositoryRelativePath('./src/main.ts'), 'src/main.ts'); assert.deepEqual(assertRepositoryRelativePaths(['src/main.ts', 'src/main.ts', 'docs/readme.md']), ['src/main.ts', 'docs/readme.md']); assert.throws(() => assertRepositoryRelativePath('../secrets.txt'), /escape/i); assert.throws(() => assertRepositoryRelativePath('/etc/passwd'), /absolute/i); assert.throws(() => assertRepositoryRelativePath('C:\\Windows\\win.ini'), /absolute/i); }); test('validates full commit SHAs and workflow filenames', () => { const sha = 'A'.repeat(40); assert.equal(assertFullCommitSha(sha), 'a'.repeat(40)); assert.equal(assertWorkflowFile('.gitea/workflows/deploy.yml'), '.gitea/workflows/deploy.yml'); assert.throws(() => assertFullCommitSha('abc1234'), /full commit SHA/i); assert.throws(() => assertWorkflowFile('../deploy.yml'), /escape/i); assert.throws(() => assertWorkflowFile('deploy.sh'), /YAML/i); assert.equal(assertWorkflowFileName('deploy.yml'), 'deploy.yml'); assert.throws(() => assertWorkflowFileName('.gitea/workflows/deploy.yml'), /filename/i); }); test('allows supported Git remotes and rejects unsafe protocols/passwords', () => { assert.equal(assertCloneRemote('git@gitea.example.test:jens/app.git'), 'git@gitea.example.test:jens/app.git'); assert.equal(assertCloneRemote('ssh://git@gitea.example.test/jens/app.git'), 'ssh://git@gitea.example.test/jens/app.git'); assert.throws(() => assertCloneRemote('file:///tmp/repo.git'), /unsupported/i); assert.throws(() => assertCloneRemote(`https://${['jens', 'secret'].join(':')}@gitea.example.test/jens/app.git`), /password/i); }); test('redacts known tokens, authorization headers, query tokens and URL passwords', () => { const token = 'super-secret-token'; const source = `Authorization: token ${token}\nhttps://gitea.test/api?access_token=${token}\nhttps://${['jens', 'password'].join(':')}@gitea.test\n${token}`; const result = redactSecrets(source, [token]); assert.doesNotMatch(result, /super-secret-token|password/); assert.match(result, /\[REDACTED\]/); }); test('validates deployment branch and environment identifiers', () => { assert.equal(assertBranchName('release/staging'), 'release/staging'); assert.equal(assertEnvironmentName('Production-EU'), 'production-eu'); assert.throws(() => assertBranchName('-dangerous'), /invalid/i); assert.throws(() => assertBranchName('main..backup'), /invalid/i); assert.throws(() => assertEnvironmentName('production eu'), /environment/i); });