/**
* Unit tests for GatewayIdentity
*/
import test from 'brittle'
import { GatewayIdentity } from '../../lib/gateway-identity.js'
import crypto from 'hypercore-crypto'
import { tmpdir } from 'os'
import { mkdtemp, writeFile, mkdir } from 'fs/promises'
import path from 'path'
test('GatewayIdentity - initialization', async (t) => {
const identity = new GatewayIdentity({
subnet: '10.22.0.0/16'
})
t.ok(identity, 'GatewayIdentity instance created')
t.is(identity.networkPrefix, '10.22', 'Network prefix parsed correctly')
t.is(identity.cidr, 16, 'CIDR parsed correctly')
t.is(identity.ipAssignmentStrategy, 'hash', 'Default strategy is hash')
})
test('GatewayIdentity - topic derivation is deterministic', async (t) => {
const identity = new GatewayIdentity()
const topic1 = await identity.deriveServiceTopic('test-service')
const topic2 = await identity.deriveServiceTopic('test-service')
t.ok(Buffer.isBuffer(topic1), 'Topic is a Buffer')
t.is(topic1.length, 32, 'Topic is 32 bytes')
t.alike(topic1, topic2, 'Same profile name generates same topic')
})
test('GatewayIdentity - different profiles get different topics', async (t) => {
const identity = new GatewayIdentity()
const topic1 = await identity.deriveServiceTopic('service-a')
const topic2 = await identity.deriveServiceTopic('service-b')
t.unlike(topic1, topic2, 'Different profiles generate different topics')
})
test('GatewayIdentity - IP assignment from topic (/16 subnet)', async (t) => {
const identity = new GatewayIdentity({
subnet: '10.22.0.0/16'
})
const topic = crypto.hash(Buffer.from('test-topic'))
const ip = identity.assignIP(topic)
t.ok(ip, 'IP assigned')
t.ok(ip.startsWith('10.22.'), 'IP in correct subnet')
const parts = ip.split('.')
t.is(parts.length, 4, 'Valid IPv4 format')
t.is(parts[0], '10', 'First octet correct')
t.is(parts[1], '22', 'Second octet correct')
})
test('GatewayIdentity - IP assignment is deterministic', async (t) => {
const identity = new GatewayIdentity({
subnet: '10.22.0.0/16'
})
const topic = crypto.hash(Buffer.from('deterministic-test'))
const ip1 = identity.assignIP(topic)
const ip2 = identity.assignIP(topic)
t.is(ip1, ip2, 'Same topic generates same IP')
})
test('GatewayIdentity - /24 subnet support', async (t) => {
const identity = new GatewayIdentity({
subnet: '10.22.0.0/24'
})
const topic = crypto.hash(Buffer.from('test-topic'))
const ip = identity.assignIP(topic)
t.ok(ip.startsWith('10.22.0.'), '/24 subnet IP correct')
const lastOctet = parseInt(ip.split('.')[3])
t.ok(lastOctet >= 1 && lastOctet <= 254, 'Last octet in valid range')
})
test('GatewayIdentity - collision detection', async (t) => {
const identity = new GatewayIdentity()
// Manually add a service to create collision
const topic = await identity.deriveServiceTopic('test-service')
const ip = identity.assignIP(topic)
identity.services.set(ip, {
name: 'existing-service',
ip,
topic: topic.toString('hex')
})
t.ok(identity.hasCollision(ip), 'Detects existing IP')
t.absent(identity.hasCollision('10.22.99.99'), 'Non-existing IP returns false')
})
test('GatewayIdentity - static IP mapping', async (t) => {
const identity = new GatewayIdentity({
subnet: '10.22.0.0/16',
staticMappings: {
'important-service': '10.22.0.100'
}
})
t.ok(identity.staticMappings['important-service'], 'Static mapping configured')
t.is(identity.staticMappings['important-service'], '10.22.0.100', 'Correct static IP')
})
test('GatewayIdentity - service lookup', async (t) => {
const identity = new GatewayIdentity()
const service = {
name: 'test-service',
ip: '10.22.0.50',
topic: 'abc123',
port: 9000
}
identity.services.set(service.ip, service)
const found = identity.lookup('10.22.0.50')
t.ok(found, 'Service found by IP')
t.is(found.name, 'test-service', 'Correct service returned')
const notFound = identity.lookup('10.22.0.99')
t.absent(notFound, 'Non-existing service returns undefined')
})
test('GatewayIdentity - list services', async (t) => {
const identity = new GatewayIdentity()
identity.services.set('10.22.0.1', { name: 'service-1' })
identity.services.set('10.22.0.2', { name: 'service-2' })
identity.services.set('10.22.0.3', { name: 'service-3' })
const services = identity.listServices()
t.is(services.length, 3, 'Returns all services')
})
test('GatewayIdentity - get service by name', async (t) => {
const identity = new GatewayIdentity()
identity.services.set('10.22.0.10', { name: 'git-daemon', port: 9418 })
identity.services.set('10.22.0.20', { name: 'fedicore', port: 3000 })
const service = identity.getServiceByName('git-daemon')
t.ok(service, 'Service found by name')
t.is(service.port, 9418, 'Correct service returned')
const notFound = identity.getServiceByName('non-existent')
t.absent(notFound, 'Non-existing service returns null')
})
test('GatewayIdentity - connection management', async (t) => {
const identity = new GatewayIdentity()
const service = {
name: 'test-service',
ip: '10.22.0.50',
connections: []
}
identity.services.set(service.ip, service)
const conn1 = { id: 'conn-1' }
const conn2 = { id: 'conn-2' }
identity.updateConnection(service.ip, conn1)
identity.updateConnection(service.ip, conn2)
const updated = identity.lookup(service.ip)
t.is(updated.connections.length, 2, 'Connections added')
identity.removeConnection(service.ip, conn1)
t.is(updated.connections.length, 1, 'Connection removed')
t.is(updated.connections[0].id, 'conn-2', 'Correct connection remains')
})
test('GatewayIdentity - export mappings', async (t) => {
const identity = new GatewayIdentity()
identity.services.set('10.22.0.10', {
name: 'service-1',
profile: 'profile-1',
topic: 'abc123',
port: 9000,
connections: [{ id: 'conn-1' }]
})
const mappings = identity.exportMappings()
t.ok(mappings['10.22.0.10'], 'Service mapping exported')
t.is(mappings['10.22.0.10'].name, 'service-1', 'Correct name')
t.is(mappings['10.22.0.10'].activeConnections, 1, 'Connection count correct')
})
// Integration test with mock hyperident profiles
test('GatewayIdentity - load profiles (mock)', async (t) => {
// Create temporary hyperident directory
const tempDir = await mkdtemp(path.join(tmpdir(), 'hyperident-test-'))
// Create mock profiles.json
const profilesData = {
version: '1.0.0',
profiles: {
'test-service': {
name: 'test-service',
description: 'Test service profile',
created: new Date().toISOString()
},
'another-service': {
name: 'another-service',
description: 'Another test service',
created: new Date().toISOString()
}
}
}
await writeFile(
path.join(tempDir, 'profiles.json'),
JSON.stringify(profilesData, null, 2)
)
const identity = new GatewayIdentity({
hyperidentPath: tempDir
})
const profiles = await identity.loadProfiles()
t.ok(profiles, 'Profiles loaded')
t.ok(profiles['test-service'], 'test-service profile exists')
t.ok(profiles['another-service'], 'another-service profile exists')
t.is(Object.keys(profiles).length, 2, 'Correct number of profiles')
})
test('GatewayIdentity - register service (mock hyperident)', async (t) => {
// Create temporary hyperident directory
const tempDir = await mkdtemp(path.join(tmpdir(), 'hyperident-test-'))
const profilesData = {
version: '1.0.0',
profiles: {
'git-daemon': {
name: 'git-daemon',
description: 'Git daemon service',
created: new Date().toISOString()
}
}
}
await writeFile(
path.join(tempDir, 'profiles.json'),
JSON.stringify(profilesData, null, 2)
)
const identity = new GatewayIdentity({
hyperidentPath: tempDir,
subnet: '10.22.0.0/16'
})
const service = await identity.registerService({
name: 'git-daemon',
profile: 'git-daemon',
port: 9418
})
t.ok(service, 'Service registered')
t.is(service.name, 'git-daemon', 'Service name correct')
t.is(service.profile, 'git-daemon', 'Profile name correct')
t.is(service.port, 9418, 'Port correct')
t.ok(service.ip, 'IP assigned')
t.ok(service.ip.startsWith('10.22.'), 'IP in correct subnet')
t.ok(service.topic, 'Topic generated')
t.is(service.topic.length, 64, 'Topic is hex string (32 bytes)')
})
test('GatewayIdentity - register service with static mapping', async (t) => {
const tempDir = await mkdtemp(path.join(tmpdir(), 'hyperident-test-'))
const profilesData = {
version: '1.0.0',
profiles: {
'important-service': {
name: 'important-service',
created: new Date().toISOString()
}
}
}
await writeFile(
path.join(tempDir, 'profiles.json'),
JSON.stringify(profilesData, null, 2)
)
const identity = new GatewayIdentity({
hyperidentPath: tempDir,
subnet: '10.22.0.0/16',
staticMappings: {
'important-service': '10.22.0.100'
}
})
const service = await identity.registerService({
name: 'important-service',
profile: 'important-service',
port: 8080
})
t.is(service.ip, '10.22.0.100', 'Static IP used instead of hash')
})
test('GatewayIdentity - prevent duplicate service registration', async (t) => {
const tempDir = await mkdtemp(path.join(tmpdir(), 'hyperident-test-'))
const profilesData = {
version: '1.0.0',
profiles: {
'test-service': {
name: 'test-service',
created: new Date().toISOString()
}
}
}
await writeFile(
path.join(tempDir, 'profiles.json'),
JSON.stringify(profilesData, null, 2)
)
const identity = new GatewayIdentity({
hyperidentPath: tempDir
})
// Register once
await identity.registerService({
name: 'test-service',
profile: 'test-service',
port: 9000
})
// Try to register again (same topic → same IP → collision)
try {
await identity.registerService({
name: 'duplicate-service',
profile: 'test-service', // Same profile = same topic = same IP
port: 9001
})
t.fail('Should have thrown collision error')
} catch (error) {
t.ok(error.message.includes('IP collision'), 'Collision detected')
}
})