OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / storage-locator.test.js

622 lines · javascript ref:
/**
 * 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)
})