Files

53 lines
2.0 KiB
TypeScript

/**
* Per-operation SSH key path handling (#1413): concurrent syncs of the same
* credential must not share one key file, and cleaning up one must never delete
* another operation's key mid-clone.
*/
import { describe, it, expect } from 'bun:test';
import { existsSync, writeFileSync, rmSync } from 'node:fs';
import {
GIT_SSH_KEY_PATH_ENV,
makeSshKeyPath,
resolveSshKeyPathForCleanup,
removeSshKey
} from '../src/lib/server/git-ssh-key';
describe('git ssh key paths (#1413)', () => {
it('each operation gets a UNIQUE path, even for the same credential id', () => {
const paths = new Set(Array.from({ length: 50 }, () => makeSshKeyPath(1)));
expect(paths.size).toBe(50); // no collisions across concurrent ops of credential 1
for (const p of paths) expect(p.startsWith('/tmp/.ssh-key-1-')).toBe(true);
});
it('cleanup targets the exact path stashed in env, not a shared per-credential path', () => {
const opPath = makeSshKeyPath(7);
expect(resolveSshKeyPathForCleanup(7, { [GIT_SSH_KEY_PATH_ENV]: opPath })).toBe(opPath);
});
it('falls back to the legacy deterministic path only when no env is given', () => {
expect(resolveSshKeyPathForCleanup(7)).toBe('/tmp/.ssh-key-7');
expect(resolveSshKeyPathForCleanup(7, {})).toBe('/tmp/.ssh-key-7');
});
it('removing one operation key leaves a concurrent operation key intact (the race)', () => {
const a = makeSshKeyPath(3);
const b = makeSshKeyPath(3);
expect(a).not.toBe(b);
writeFileSync(a, 'KEY-A');
writeFileSync(b, 'KEY-B');
try {
// Operation A finishes and cleans up its key...
removeSshKey(3, { [GIT_SSH_KEY_PATH_ENV]: a });
expect(existsSync(a)).toBe(false);
// ...while operation B is still cloning: its key MUST survive.
expect(existsSync(b)).toBe(true);
} finally {
for (const p of [a, b]) { try { rmSync(p); } catch { /* ignore */ } }
}
});
it('removeSshKey is a no-op (no throw) when the file is already gone', () => {
expect(() => removeSshKey(99, { [GIT_SSH_KEY_PATH_ENV]: makeSshKeyPath(99) })).not.toThrow();
});
});