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
}
})