OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident / test / file-ops.test.js

295 lines · javascript ref:
import test from 'brittle'
import { FileManager } from '../lib/file-ops.js'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'

// Test directory for isolated testing
const TEST_DIR = path.join(os.tmpdir(), 'hyperident-test-' + Date.now())

// Helper function to ensure test directory exists
async function ensureTestDir() {
  try {
    await fs.mkdir(TEST_DIR, { recursive: true })
  } catch (error) {
    if (error.code !== 'EEXIST') {
      throw error
    }
  }
}

test.hook('after', async () => {
  // Clean up test directory
  try {
    await fs.rm(TEST_DIR, { recursive: true, force: true })
  } catch (error) {
    // Ignore cleanup errors
  }
})

test('FileManager - get hyperident path', async (t) => {
  t.plan(2)
  
  const hyperidentPath = FileManager.getHyperidentPath()
  const expectedPath = path.join(os.homedir(), '.hyperident')
  
  t.is(typeof hyperidentPath, 'string', 'Should return a string')
  t.is(hyperidentPath, expectedPath, 'Should return correct path')
})

test('FileManager - file exists check', async (t) => {
  t.plan(3)
  
  await ensureTestDir()
  const testFile = path.join(TEST_DIR, 'test-file.txt')
  
  // File doesn't exist initially
  const existsBefore = await FileManager.fileExists(testFile)
  t.is(existsBefore, false, 'Should return false for non-existent file')
  
  // Create file
  await fs.writeFile(testFile, 'test content')
  
  // File should exist now
  const existsAfter = await FileManager.fileExists(testFile)
  t.is(existsAfter, true, 'Should return true for existing file')
  
  // Test validation
  try {
    await FileManager.fileExists(123)
    t.fail('Should throw error for non-string path')
  } catch (error) {
    t.is(error.message, 'File path must be a string', 'Should validate path type')
  }
})

test('FileManager - atomic write', async (t) => {
  t.plan(3)
  
  await ensureTestDir()
  const testFile = path.join(TEST_DIR, 'atomic-test.txt')
  const testData = 'test atomic write'
  
  await FileManager.atomicWrite(testFile, testData, 0o644)
  
  // Verify file was created
  const exists = await FileManager.fileExists(testFile)
  t.ok(exists, 'File should exist after atomic write')
  
  // Verify content
  const content = await fs.readFile(testFile, 'utf8')
  t.is(content, testData, 'File content should match')
  
  // Verify no temp file remains
  const tempExists = await FileManager.fileExists(testFile + '.tmp')
  t.is(tempExists, false, 'Temp file should not exist')
})

test('FileManager - atomic write validation', async (t) => {
  t.plan(3)
  
  try {
    await FileManager.atomicWrite(123, 'data', 0o644)
    t.fail('Should throw error for non-string path')
  } catch (error) {
    t.is(error.message, 'File path must be a string', 'Should validate path type')
  }
  
  try {
    await FileManager.atomicWrite('/test/path', 123, 0o644)
    t.fail('Should throw error for non-string data')
  } catch (error) {
    t.is(error.message, 'Data must be a string', 'Should validate data type')
  }
  
  try {
    await FileManager.atomicWrite('/test/path', 'data', 'invalid')
    t.fail('Should throw error for non-number permissions')
  } catch (error) {
    t.is(error.message, 'Permissions must be a number', 'Should validate permissions type')
  }
})

test('FileManager - set permissions', async (t) => {
  t.plan(2)
  
  await ensureTestDir()
  const testFile = path.join(TEST_DIR, 'permissions-test.txt')
  await fs.writeFile(testFile, 'test')
  
  await FileManager.setPermissions(testFile, 0o600)
  
  const stats = await fs.stat(testFile)
  // On some systems, the actual permissions might differ slightly
  t.ok(stats.mode & 0o600, 'Should set permissions')
  
  // Test validation
  try {
    await FileManager.setPermissions(123, 0o600)
    t.fail('Should throw error for non-string path')
  } catch (error) {
    t.is(error.message, 'File path must be a string', 'Should validate path type')
  }
})

test('FileManager - write and read profiles', async (t) => {
  t.plan(4)
  
  // Mock getHyperidentPath to use test directory
  const originalGetPath = FileManager.getHyperidentPath
  FileManager.getHyperidentPath = () => TEST_DIR
  
  try {
    const testProfiles = {
      version: 1,
      profiles: {
        work: {
          created: '2024-01-15T10:30:00Z',
          description: 'Work projects'
        }
      }
    }
    
    await FileManager.writeProfiles(testProfiles)
    
    const profilesPath = path.join(TEST_DIR, 'profiles.json')
    const exists = await FileManager.fileExists(profilesPath)
    t.ok(exists, 'Profiles file should be created')
    
    const readProfiles = await FileManager.readProfiles()
    t.alike(readProfiles, testProfiles, 'Read profiles should match written profiles')
    
    // Test reading non-existent profiles (should return empty structure)
    await fs.unlink(profilesPath)
    const emptyProfiles = await FileManager.readProfiles()
    t.alike(emptyProfiles, { version: '1.0.0', profiles: {} }, 'Should return empty structure for missing file')
    
    // Test validation
    try {
      await FileManager.writeProfiles('not an object')
      t.fail('Should throw error for non-object data')
    } catch (error) {
      t.is(error.message, 'Profiles data must be an object', 'Should validate data type')
    }
  } finally {
    // Restore original method
    FileManager.getHyperidentPath = originalGetPath
  }
})

test('FileManager - write and read public key', async (t) => {
  t.plan(3)
  
  // Mock getHyperidentPath to use test directory
  const originalGetPath = FileManager.getHyperidentPath
  FileManager.getHyperidentPath = () => TEST_DIR
  
  try {
    const testPublicKey = 'abc123def456789'
    
    await FileManager.writePublicKey(testPublicKey)
    
    const pubKeyPath = path.join(TEST_DIR, 'identity.pub')
    const exists = await FileManager.fileExists(pubKeyPath)
    t.ok(exists, 'Public key file should be created')
    
    const readPublicKey = await FileManager.readPublicKey()
    t.is(readPublicKey, testPublicKey, 'Read public key should match written key')
    
    // Test validation
    try {
      await FileManager.writePublicKey(123)
      t.fail('Should throw error for non-string key')
    } catch (error) {
      t.is(error.message, 'Public key must be a string', 'Should validate key type')
    }
  } finally {
    // Restore original method
    FileManager.getHyperidentPath = originalGetPath
  }
})

test('FileManager - write and read identity file', async (t) => {
  t.plan(4)
  
  // Mock getHyperidentPath to use test directory
  const originalGetPath = FileManager.getHyperidentPath
  FileManager.getHyperidentPath = () => TEST_DIR
  
  try {
    const testIdentity = {
      version: 1,
      encrypted: false,
      data: 'test mnemonic words'
    }
    
    await FileManager.writeIdentityFile(testIdentity, false)
    
    const identityPath = path.join(TEST_DIR, 'identity')
    const exists = await FileManager.fileExists(identityPath)
    t.ok(exists, 'Identity file should be created')
    
    const readIdentity = await FileManager.readIdentityFile()
    t.alike(readIdentity, testIdentity, 'Read identity should match written identity')
    
    // Test validation
    try {
      await FileManager.writeIdentityFile('not an object', false)
      t.fail('Should throw error for non-object data')
    } catch (error) {
      t.is(error.message, 'Data must be an object', 'Should validate data type')
    }
    
    try {
      await FileManager.writeIdentityFile({}, 'not boolean')
      t.fail('Should throw error for non-boolean encrypted flag')
    } catch (error) {
      t.is(error.message, 'Encrypted flag must be a boolean', 'Should validate encrypted flag type')
    }
  } finally {
    // Restore original method
    FileManager.getHyperidentPath = originalGetPath
  }
})

test('FileManager - ensure hyperident directory', async (t) => {
  t.plan(2)
  
  const testHyperidentDir = path.join(TEST_DIR, 'test-hyperident')
  
  // Mock getHyperidentPath to use test directory
  const originalGetPath = FileManager.getHyperidentPath
  FileManager.getHyperidentPath = () => testHyperidentDir
  
  try {
    await FileManager.ensureHyperidentDir()
    
    const exists = await FileManager.fileExists(testHyperidentDir)
    t.ok(exists, 'Directory should be created')
    
    const stats = await fs.stat(testHyperidentDir)
    t.ok(stats.isDirectory(), 'Should be a directory')
  } finally {
    // Restore original method
    FileManager.getHyperidentPath = originalGetPath
  }
})

test('FileManager - read missing identity file', async (t) => {
  t.plan(1)
  
  // Mock getHyperidentPath to use test directory
  const originalGetPath = FileManager.getHyperidentPath
  FileManager.getHyperidentPath = () => path.join(TEST_DIR, 'missing')
  
  try {
    await FileManager.readIdentityFile()
    t.fail('Should throw error for missing identity file')
  } catch (error) {
    t.is(error.message, 'Identity not found. Run "hyperident init" to create one.', 'Should provide helpful error message')
  } finally {
    // Restore original method
    FileManager.getHyperidentPath = originalGetPath
  }
})