/** * Tests for `storage add local`'s pre-save write probe. * * Background: an operator on celilo-mgmt typed `/var/backups/celilo` * for the path. The CLI accepted it, saved a storage row, then the * heavyweight verify step failed with EACCES — leaving an unverified * row that confused subsequent `system update` runs. The probe * catches unwriteable paths before any persistent state is created. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { probePathWriteable } from './storage-add-local'; describe('probePathWriteable', () => { let tempRoot: string; beforeEach(() => { tempRoot = mkdtempSync(join(tmpdir(), 'celilo-probe-test-')); }); afterEach(() => { rmSync(tempRoot, { recursive: true, force: true }); }); test('returns null for a writeable existing directory', () => { expect(probePathWriteable(tempRoot)).toBeNull(); }); test("creates parent dirs that don't exist yet (mkdir -p semantics)", () => { // The probe's mkdirSync uses `recursive: true`, so a path several // levels below the temp root works on the first try. This is the // common case for the user's typed path under their data dir. const deep = join(tempRoot, 'a', 'b', 'c', 'backups'); expect(probePathWriteable(deep)).toBeNull(); }); test('returns an error message for a path under a read-only ancestor', () => { const readOnly = join(tempRoot, 'readonly'); mkdirSync(readOnly); // 0o500 = read+execute for owner, no write. mkdirSync into it // should EACCES. require('node:fs').chmodSync(readOnly, 0o500); try { const target = join(readOnly, 'celilo-backups'); const err = probePathWriteable(target); expect(err).not.toBeNull(); expect(err).toContain('EACCES'); } finally { // Restore permissions so afterEach can clean up. require('node:fs').chmodSync(readOnly, 0o700); } }); test('cleans up the probe directory on success (no leftover state)', () => { const dir = join(tempRoot, 'check'); expect(probePathWriteable(dir)).toBeNull(); // The probe creates `/.celilo-write-probe` then removes it. // The directory itself stays (mkdir -p), but the probe sentinel // doesn't. const { existsSync } = require('node:fs'); expect(existsSync(join(dir, '.celilo-write-probe'))).toBe(false); }); });