Files
ansiblings/packages/keyman/tests/keys.test.ts
T
Benjamin Diedrichsen da84523a6d
Publish snapshot / snapshot (push) Successful in 1m4s
[fix] keyman: a permission-based test the CI runner is root for
The snapshot run for 0.7.0 failed on this one test and published nothing.
`scanPrivateKeys` classifies a file it cannot open as not-a-key, and the test
made the file unopenable with `chmod 0o000` — which stops nobody with uid 0,
and Gitea's act_runner is a container running as root. So the file was read,
recognised as a private key not named id_*, and reported as skipped.

A dangling symlink instead: ENOENT is not a permission anyone can override,
and it is a realistic ~/.ssh inhabitant. Verified by running the gate in a
node:22 container as root, where the whole workspace is now green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:32:36 +02:00

129 lines
4.2 KiB
TypeScript

/**
* Tests for private key discovery.
*
* Real files, because the classification is a bounded read of a real header.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { reportSkippedKeys, scanPrivateKeys } from '../src/keyman.keys.js';
const OPENSSH = '-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXk\n';
const RSA_PEM = '-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\n';
const PKCS8 = '-----BEGIN PRIVATE KEY-----\nMIIB\n';
describe('scanPrivateKeys', () => {
let dir: string;
const write = (name: string, contents: string) =>
fs.writeFileSync(path.join(dir, name), contents);
beforeEach(() => {
dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'keyman-keys-')));
});
afterEach(() => {
fs.rmSync(dir, { recursive: true, force: true });
});
it('returns nothing for a directory that does not exist', () => {
expect(scanPrivateKeys(path.join(dir, 'nope'))).toEqual({ keys: [], skipped: [] });
});
it('offers the id_ keys and not their public halves', () => {
write('id_prod', OPENSSH);
write('id_prod.pub', 'ssh-ed25519 AAAA');
expect(scanPrivateKeys(dir)).toEqual({ keys: ['id_prod'], skipped: [] });
});
it('sorts the keys, so the menu order does not come from the filesystem', () => {
for (const name of ['id_stage', 'id_alpha', 'id_prod']) {
write(name, OPENSSH);
}
expect(scanPrivateKeys(dir).keys).toEqual(['id_alpha', 'id_prod', 'id_stage']);
});
it('offers an id_ file without checking what is in it', () => {
// Unchanged from before the scan existed: whatever was offered still is.
write('id_prod', 'not a key at all');
expect(scanPrivateKeys(dir).keys).toEqual(['id_prod']);
});
it.each([
['an OpenSSH key', OPENSSH],
['an encrypted PEM key', RSA_PEM],
['a PKCS#8 key', PKCS8],
])('reports %s that is not named id_*', (_label, contents) => {
write('deploy_ed25519', contents);
expect(scanPrivateKeys(dir)).toEqual({ keys: [], skipped: ['deploy_ed25519'] });
});
it('ignores the other files a .ssh directory is full of', () => {
write('known_hosts', 'github.com ssh-ed25519 AAAA');
write('config', 'Host *\n AddKeysToAgent yes\n');
write('authorized_keys', 'ssh-ed25519 AAAA');
fs.mkdirSync(path.join(dir, 'sockets'));
expect(scanPrivateKeys(dir)).toEqual({ keys: [], skipped: [] });
});
it('ignores a path it cannot read', () => {
// A dangling symlink, not a 0o000 file: root reads a 0o000 file happily, so
// the mode-based version of this passed here and failed on the CI runner,
// which is a container running as root. ENOENT nobody can override.
fs.symlinkSync(path.join(dir, 'gone'), path.join(dir, 'secret'));
// Reported as not-a-key rather than crashing the menu it was building.
expect(scanPrivateKeys(dir).skipped).toEqual([]);
});
it('does not read past the header', () => {
// The marker is in the first line; a mention further down is not a key.
write('decoy', `${'x'.repeat(200)}\nPRIVATE KEY-----\n`);
expect(scanPrivateKeys(dir).skipped).toEqual([]);
});
});
describe('reportSkippedKeys', () => {
let logSpy: ReturnType<typeof vi.spyOn>;
const messages = () => logSpy.mock.calls.map((c) => c.join(' ')).join('\n');
beforeEach(() => {
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('says nothing when nothing was skipped', () => {
reportSkippedKeys([], '/home/alice/.ssh');
expect(logSpy).not.toHaveBeenCalled();
});
it('names the keys, the directory and the reason', () => {
reportSkippedKeys(['deploy_ed25519', 'backup_rsa'], '/home/alice/.ssh');
expect(messages()).toContain('deploy_ed25519, backup_rsa');
expect(messages()).toContain('/home/alice/.ssh');
expect(messages()).toContain('2 private keys');
// Without the reason the message is a complaint rather than an instruction.
expect(messages()).toContain('rename');
});
it('says key, singular, for one of them', () => {
reportSkippedKeys(['deploy_ed25519'], '/home/alice/.ssh');
expect(messages()).toContain('1 private key ');
});
});