/**
* Unit tests for lib/storage-locator.js
*
* Tests read/write of storage locator breadcrumb file.
*/
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
// We test the module's logic directly rather than importing it,
// because the real module hardcodes ~/.config/pear-git/ paths.
// Instead we replicate the logic with temp directories.
function tmpdir () {
const dir = path.join(os.tmpdir(), 'pear-git-test-locator-' + Date.now() + '-' + Math.random().toString(36).slice(2))
fs.mkdirSync(dir, { recursive: true })
return dir
}
function cleanup (dir) {
try { fs.rmSync(dir, { recursive: true }) } catch {}
}
// ========================================
// Locator Write
// ========================================
test('write - creates JSON locator file', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const storagePath = path.join(dir, 'my-storage')
fs.mkdirSync(storagePath, { recursive: true })
// Simulate write
const data = { storagePath: path.resolve(storagePath), updatedAt: Date.now() }
fs.writeFileSync(locatorFile, JSON.stringify(data, null, 2) + '\n')
// Verify
const read = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
t.is(read.storagePath, path.resolve(storagePath), 'storagePath is absolute')
t.ok(read.updatedAt > 0, 'has updatedAt timestamp')
cleanup(dir)
})
// ========================================
// Locator Read — valid
// ========================================
test('read - returns path when identity file exists', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const storagePath = path.join(dir, 'storage')
// Create storage with identity file
fs.mkdirSync(storagePath, { recursive: true })
fs.writeFileSync(path.join(storagePath, 'server-identity.json'), '{"publicKey":"abc"}')
// Write locator
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath, updatedAt: Date.now() }))
// Simulate read logic
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
let result = null
try {
fs.statSync(path.join(data.storagePath, 'server-identity.json'))
result = data.storagePath
} catch {
result = null
}
t.is(result, storagePath, 'returns storage path')
cleanup(dir)
})
// ========================================
// Locator Read — identity missing
// ========================================
test('read - returns null when identity file missing', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const storagePath = path.join(dir, 'empty-storage')
// Create storage WITHOUT identity file
fs.mkdirSync(storagePath, { recursive: true })
// Write locator pointing to it
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath, updatedAt: Date.now() }))
// Simulate read logic
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
let result = null
try {
fs.statSync(path.join(data.storagePath, 'server-identity.json'))
result = data.storagePath
} catch {
result = null
}
t.is(result, null, 'returns null when identity missing')
cleanup(dir)
})
// ========================================
// Locator Read — no locator file
// ========================================
test('read - returns null when locator file does not exist', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'nonexistent', 'storage-locator.json')
let result = null
try {
JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
} catch {
result = null
}
t.is(result, null, 'returns null for missing locator')
cleanup(dir)
})
// ========================================
// Constructor Priority
// ========================================
test('priority - explicit --storage wins over locator and default', async (t) => {
const dir = tmpdir()
const explicitPath = path.join(dir, 'explicit')
const locatorPath = path.join(dir, 'locator')
const defaultPath = path.join(dir, 'default')
// Setup all three
for (const p of [explicitPath, locatorPath, defaultPath]) {
fs.mkdirSync(p, { recursive: true })
fs.writeFileSync(path.join(p, 'server-identity.json'), '{"publicKey":"test"}')
}
// Simulate priority: explicit || locator || default
const explicit = explicitPath
const locator = locatorPath
const fallback = defaultPath
const result = explicit || locator || fallback
t.is(result, explicitPath, 'explicit wins')
cleanup(dir)
})
test('priority - locator wins over default when no explicit', async (t) => {
const dir = tmpdir()
const locatorPath = path.join(dir, 'locator')
const defaultPath = path.join(dir, 'default')
for (const p of [locatorPath, defaultPath]) {
fs.mkdirSync(p, { recursive: true })
fs.writeFileSync(path.join(p, 'server-identity.json'), '{"publicKey":"test"}')
}
// Simulate: undefined || locator || default
const explicit = undefined
const locator = locatorPath
const fallback = defaultPath
const result = explicit || locator || fallback
t.is(result, locatorPath, 'locator wins over default')
cleanup(dir)
})
test('priority - default used when no explicit and locator returns null', async (t) => {
const dir = tmpdir()
const defaultPath = path.join(dir, 'default')
fs.mkdirSync(defaultPath, { recursive: true })
// Simulate: undefined || null || default
const explicit = undefined
const locator = null
const fallback = defaultPath
const result = explicit || locator || fallback
t.is(result, defaultPath, 'falls back to default')
cleanup(dir)
})
// ========================================
// Import the actual module
// ========================================
test('StorageLocator module - exports read/write/filePath', async (t) => {
const { StorageLocator } = await import('../../lib/storage-locator.js')
t.is(typeof StorageLocator.read, 'function', 'has read()')
t.is(typeof StorageLocator.write, 'function', 'has write()')
t.ok(StorageLocator.filePath.endsWith('storage-locator.json'), 'has filePath getter')
})
test('resolveDefaultStorage logic - returns absolute path under ~/.config', async (t) => {
// We can't import PearGitServer directly (it runs main() as side effect),
// so we verify the logic independently.
const homedir = os.homedir()
const expected = path.join(homedir, '.config', 'pear-git', 'server-data')
t.ok(path.isAbsolute(expected), 'default path is absolute')
t.ok(expected.includes('pear-git'), 'path contains pear-git')
t.ok(expected.endsWith('server-data'), 'path ends with server-data')
})
// ========================================
// Actual Filesystem Roundtrip (StorageLocator module)
// ========================================
test('StorageLocator.write + read roundtrip on real filesystem', async (t) => {
const { StorageLocator } = await import('../../lib/storage-locator.js')
const dir = tmpdir()
const storagePath = path.join(dir, 'real-storage')
// Create storage with identity file
fs.mkdirSync(storagePath, { recursive: true })
fs.writeFileSync(path.join(storagePath, 'server-identity.json'), '{"publicKey":"roundtrip-test"}')
// We can't change the module's LOCATOR_FILE constant, so we test
// the underlying logic: write JSON, then read it back and validate.
const locatorFile = path.join(dir, 'storage-locator.json')
const data = { storagePath: path.resolve(storagePath), updatedAt: Date.now() }
// Write
fs.writeFileSync(locatorFile, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 })
// Read back
const readBack = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
t.is(readBack.storagePath, path.resolve(storagePath), 'path matches after roundtrip')
t.ok(readBack.updatedAt > 0, 'timestamp preserved')
// Validate the path has identity file
const identityExists = fs.existsSync(path.join(readBack.storagePath, 'server-identity.json'))
t.ok(identityExists, 'identity file exists at stored path')
cleanup(dir)
})
test('write - creates parent directories if missing', async (t) => {
const dir = tmpdir()
const deepDir = path.join(dir, 'a', 'b', 'c')
const locatorFile = path.join(deepDir, 'storage-locator.json')
// Create the nested directory
fs.mkdirSync(deepDir, { recursive: true })
// Write locator
const data = { storagePath: '/some/path', updatedAt: Date.now() }
fs.writeFileSync(locatorFile, JSON.stringify(data))
// Read back
const readBack = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
t.is(readBack.storagePath, '/some/path', 'data written to nested path')
cleanup(dir)
})
// ========================================
// Corrupted/Invalid Locator File
// ========================================
test('read - returns null for corrupted JSON', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
// Write invalid JSON
fs.writeFileSync(locatorFile, 'not valid json {{{')
let result = null
try {
JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
} catch {
result = null
}
t.is(result, null, 'corrupted JSON returns null')
cleanup(dir)
})
test('read - returns null for empty file', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
// Write empty file
fs.writeFileSync(locatorFile, '')
let result = null
try {
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
result = data?.storagePath || null
} catch {
result = null
}
t.is(result, null, 'empty file returns null')
cleanup(dir)
})
test('read - returns null when storagePath field is missing', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
// Write valid JSON but missing storagePath
fs.writeFileSync(locatorFile, JSON.stringify({ updatedAt: Date.now() }))
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
const result = data?.storagePath || null
t.is(result, null, 'missing storagePath returns null')
cleanup(dir)
})
test('read - returns null when storage directory was deleted', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const storagePath = path.join(dir, 'deleted-storage')
// Create then delete storage
fs.mkdirSync(storagePath, { recursive: true })
fs.writeFileSync(path.join(storagePath, 'server-identity.json'), '{}')
fs.rmSync(storagePath, { recursive: true })
// Write locator pointing to deleted path
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath, updatedAt: Date.now() }))
// Simulate read logic
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
let result = null
try {
fs.statSync(path.join(data.storagePath, 'server-identity.json'))
result = data.storagePath
} catch {
result = null
}
t.is(result, null, 'returns null when storage directory deleted')
cleanup(dir)
})
test('write - resolves relative paths to absolute', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const relativePath = './my-storage'
const resolved = path.resolve(relativePath)
const data = { storagePath: resolved, updatedAt: Date.now() }
fs.writeFileSync(locatorFile, JSON.stringify(data))
const readBack = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
t.ok(path.isAbsolute(readBack.storagePath), 'stored path is absolute')
t.is(readBack.storagePath, resolved, 'relative path was resolved')
cleanup(dir)
})
// ========================================
// Guard: readRaw() (module export)
// ========================================
test('StorageLocator module - exports readRaw()', async (t) => {
const { StorageLocator } = await import('../../lib/storage-locator.js')
t.is(typeof StorageLocator.readRaw, 'function', 'has readRaw()')
})
// ========================================
// Guard: readRaw() returns path without validation
// ========================================
test('readRaw - returns path even when identity file missing', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const storagePath = path.join(dir, 'no-identity-here')
// Create storage WITHOUT identity file
fs.mkdirSync(storagePath, { recursive: true })
// Write locator
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath, updatedAt: Date.now() }))
// Simulate readRaw() logic — returns path without checking identity
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
const rawResult = data.storagePath || null
// Compare: read() would return null (no identity), readRaw() returns path
let readResult = null
try {
fs.statSync(path.join(data.storagePath, 'server-identity.json'))
readResult = data.storagePath
} catch {
readResult = null
}
t.is(readResult, null, 'read() returns null — no identity')
t.is(rawResult, storagePath, 'readRaw() returns path — no validation')
cleanup(dir)
})
// ========================================
// Guard: write() refuses to overwrite valid identity
// ========================================
test('write guard - refuses overwrite when existing path has valid identity', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const existingPath = path.join(dir, 'existing-storage')
const newPath = path.join(dir, 'new-storage')
// Create existing storage WITH identity
fs.mkdirSync(existingPath, { recursive: true })
fs.writeFileSync(path.join(existingPath, 'server-identity.json'), '{"publicKey":"original"}')
// Write locator pointing to existing
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath: existingPath, updatedAt: Date.now() }))
// Create new storage (different path)
fs.mkdirSync(newPath, { recursive: true })
// Simulate write() guard logic
const resolved = path.resolve(newPath)
const existing = JSON.parse(fs.readFileSync(locatorFile, 'utf-8')).storagePath || null
let refused = false
if (existing && existing !== resolved) {
try {
fs.statSync(path.join(existing, 'server-identity.json'))
refused = true // Would refuse
} catch {
// Safe to overwrite
}
}
t.ok(refused, 'write() refuses when existing has valid identity')
// Verify locator still points to original
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
t.is(data.storagePath, existingPath, 'locator unchanged')
cleanup(dir)
})
test('write guard - allows overwrite when existing path has no identity', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const existingPath = path.join(dir, 'stale-storage')
const newPath = path.join(dir, 'fresh-storage')
// Create existing storage WITHOUT identity (stale/deleted)
fs.mkdirSync(existingPath, { recursive: true })
// Write locator pointing to stale path
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath: existingPath, updatedAt: Date.now() }))
// Simulate write() guard logic
const resolved = path.resolve(newPath)
const existing = JSON.parse(fs.readFileSync(locatorFile, 'utf-8')).storagePath || null
let refused = false
if (existing && existing !== resolved) {
try {
fs.statSync(path.join(existing, 'server-identity.json'))
refused = true
} catch {
// No identity, safe to overwrite
}
}
t.ok(!refused, 'write() allows overwrite when no identity at old path')
cleanup(dir)
})
test('write guard - allows write when paths are the same', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const storagePath = path.join(dir, 'same-storage')
// Create storage with identity
fs.mkdirSync(storagePath, { recursive: true })
fs.writeFileSync(path.join(storagePath, 'server-identity.json'), '{"publicKey":"test"}')
// Write locator
const resolved = path.resolve(storagePath)
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath: resolved, updatedAt: Date.now() }))
// Simulate write() guard logic — same path, should be allowed
const existing = JSON.parse(fs.readFileSync(locatorFile, 'utf-8')).storagePath || null
let refused = false
if (existing && existing !== resolved) {
try {
fs.statSync(path.join(existing, 'server-identity.json'))
refused = true
} catch {}
}
t.ok(!refused, 'write() allows when path matches (idempotent)')
cleanup(dir)
})
// ========================================
// Guard: _generateIdentity() aborts when locator has identity
// ========================================
test('generateIdentity guard - aborts when locator has valid identity at different path', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const locatorStorage = path.join(dir, 'locator-storage')
const newStorage = path.join(dir, 'new-storage')
// Create locator-referenced storage WITH identity
fs.mkdirSync(locatorStorage, { recursive: true })
fs.writeFileSync(path.join(locatorStorage, 'server-identity.json'), '{"publicKey":"existing"}')
// Write locator
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath: locatorStorage, updatedAt: Date.now() }))
// Simulate _generateIdentity() guard:
// Trying to generate at newStorage, but locator points to locatorStorage with valid identity
fs.mkdirSync(newStorage, { recursive: true })
const keyFile = path.join(newStorage, 'server-identity.json')
const thisDir = path.resolve(path.dirname(keyFile))
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
const locatorPath = data.storagePath || null
let aborted = false
if (locatorPath && path.resolve(locatorPath) !== thisDir) {
try {
fs.statSync(path.join(locatorPath, 'server-identity.json'))
aborted = true // Would throw [Identity] ABORT
} catch (err) {
if (err.message && err.message.startsWith('[Identity] ABORT')) aborted = true
}
}
t.ok(aborted, '_generateIdentity() aborts — existing identity at locator path')
cleanup(dir)
})
test('generateIdentity guard - succeeds when no locator exists', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'nonexistent', 'storage-locator.json')
const newStorage = path.join(dir, 'fresh-storage')
fs.mkdirSync(newStorage, { recursive: true })
const keyFile = path.join(newStorage, 'server-identity.json')
const thisDir = path.resolve(path.dirname(keyFile))
// No locator file exists
let locatorPath = null
try {
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
locatorPath = data.storagePath || null
} catch {
locatorPath = null
}
let aborted = false
if (locatorPath && path.resolve(locatorPath) !== thisDir) {
try {
fs.statSync(path.join(locatorPath, 'server-identity.json'))
aborted = true
} catch {}
}
t.ok(!aborted, '_generateIdentity() proceeds — no locator')
cleanup(dir)
})
test('generateIdentity guard - succeeds when locator identity was deleted', async (t) => {
const dir = tmpdir()
const locatorFile = path.join(dir, 'storage-locator.json')
const locatorStorage = path.join(dir, 'old-storage')
const newStorage = path.join(dir, 'new-storage')
// Create locator path WITHOUT identity (identity was deleted)
fs.mkdirSync(locatorStorage, { recursive: true })
// Write locator pointing to path with no identity
fs.writeFileSync(locatorFile, JSON.stringify({ storagePath: locatorStorage, updatedAt: Date.now() }))
fs.mkdirSync(newStorage, { recursive: true })
const keyFile = path.join(newStorage, 'server-identity.json')
const thisDir = path.resolve(path.dirname(keyFile))
const data = JSON.parse(fs.readFileSync(locatorFile, 'utf-8'))
const locatorPath = data.storagePath || null
let aborted = false
if (locatorPath && path.resolve(locatorPath) !== thisDir) {
try {
fs.statSync(path.join(locatorPath, 'server-identity.json'))
aborted = true
} catch {}
}
t.ok(!aborted, '_generateIdentity() proceeds — locator identity was deleted')
cleanup(dir)
})