397 lines
12 KiB
Nim
397 lines
12 KiB
Nim
## tests/test_decentralized.nim
|
|
## Tests for Decentralized Architecture
|
|
|
|
import std/[unittest, tables, times, asyncdispatch, options, strutils]
|
|
import ../src/nimpak/[decentralized, merkle_tree, nippel_types]
|
|
import ../src/nimpak/utils/resultutils
|
|
|
|
suite "Decentralized Architecture":
|
|
|
|
# ==========================================================================
|
|
# Test: Peer-to-Peer Discovery
|
|
# ==========================================================================
|
|
|
|
test "Discovery manager creation":
|
|
let manager = newDiscoveryManager(mDNS)
|
|
check manager.protocol == mDNS
|
|
check manager.localServices.len == 0
|
|
check manager.discoveredServices.len == 0
|
|
check manager.announceInterval == 30
|
|
check not manager.isRunning
|
|
|
|
test "Service announcement":
|
|
let manager = newDiscoveryManager(mDNS)
|
|
|
|
let announcement = ServiceAnnouncement(
|
|
name: "test-nippel",
|
|
kind: NippelService,
|
|
port: 8080,
|
|
utcpAddress: "utcp://localhost/nippel/test-nippel",
|
|
metadata: initTable[string, string](),
|
|
ttl: 300
|
|
)
|
|
|
|
proc testAnnounce() {.async.} =
|
|
let result = await manager.announceService(announcement)
|
|
check result.isOk
|
|
check manager.localServices.hasKey("test-nippel")
|
|
check manager.localServices["test-nippel"].kind == NippelService
|
|
|
|
waitFor testAnnounce()
|
|
|
|
test "Service discovery":
|
|
let manager = newDiscoveryManager(mDNS)
|
|
|
|
# Add a discovered service manually for testing
|
|
let service = DiscoveredService(
|
|
name: "remote-nippel",
|
|
kind: NippelService,
|
|
host: "192.168.1.100",
|
|
port: 8080,
|
|
protocol: mDNS,
|
|
utcpAddress: "utcp://192.168.1.100/nippel/remote-nippel",
|
|
metadata: initTable[string, string](),
|
|
discoveredAt: now(),
|
|
lastSeen: now()
|
|
)
|
|
|
|
manager.discoveredServices["remote-nippel"] = service
|
|
|
|
proc testDiscover() {.async.} =
|
|
let result = await manager.discoverServices(NippelService)
|
|
check result.isOk
|
|
let services = result.value
|
|
check services.len == 1
|
|
check services[0].name == "remote-nippel"
|
|
|
|
waitFor testDiscover()
|
|
|
|
test "Find service by name":
|
|
let manager = newDiscoveryManager(mDNS)
|
|
|
|
let service = DiscoveredService(
|
|
name: "test-service",
|
|
kind: CASService,
|
|
host: "localhost",
|
|
port: 9000,
|
|
protocol: mDNS,
|
|
utcpAddress: "utcp://localhost/cas/test-service",
|
|
metadata: initTable[string, string](),
|
|
discoveredAt: now(),
|
|
lastSeen: now()
|
|
)
|
|
|
|
manager.discoveredServices["test-service"] = service
|
|
|
|
let found = manager.findService("test-service")
|
|
check found.isSome
|
|
check found.get().kind == CASService
|
|
|
|
let notFound = manager.findService("nonexistent")
|
|
check notFound.isNone
|
|
|
|
test "Update service last seen":
|
|
let manager = newDiscoveryManager(mDNS)
|
|
|
|
let service = DiscoveredService(
|
|
name: "test-service",
|
|
kind: NippelService,
|
|
host: "localhost",
|
|
port: 8080,
|
|
protocol: mDNS,
|
|
utcpAddress: "utcp://localhost/nippel/test-service",
|
|
metadata: initTable[string, string](),
|
|
discoveredAt: now() - 100.seconds,
|
|
lastSeen: now() - 100.seconds
|
|
)
|
|
|
|
manager.discoveredServices["test-service"] = service
|
|
|
|
let oldLastSeen = manager.discoveredServices["test-service"].lastSeen
|
|
manager.updateServiceLastSeen("test-service")
|
|
let newLastSeen = manager.discoveredServices["test-service"].lastSeen
|
|
|
|
check newLastSeen > oldLastSeen
|
|
|
|
test "Remove stale services":
|
|
let manager = newDiscoveryManager(mDNS)
|
|
|
|
# Add a fresh service
|
|
let freshService = DiscoveredService(
|
|
name: "fresh-service",
|
|
kind: NippelService,
|
|
host: "localhost",
|
|
port: 8080,
|
|
protocol: mDNS,
|
|
utcpAddress: "utcp://localhost/nippel/fresh-service",
|
|
metadata: initTable[string, string](),
|
|
discoveredAt: now(),
|
|
lastSeen: now()
|
|
)
|
|
|
|
# Add a stale service
|
|
let staleService = DiscoveredService(
|
|
name: "stale-service",
|
|
kind: NippelService,
|
|
host: "localhost",
|
|
port: 8081,
|
|
protocol: mDNS,
|
|
utcpAddress: "utcp://localhost/nippel/stale-service",
|
|
metadata: initTable[string, string](),
|
|
discoveredAt: now() - 400.seconds,
|
|
lastSeen: now() - 400.seconds
|
|
)
|
|
|
|
manager.discoveredServices["fresh-service"] = freshService
|
|
manager.discoveredServices["stale-service"] = staleService
|
|
|
|
check manager.discoveredServices.len == 2
|
|
|
|
manager.removeStaleServices(300) # Remove services older than 5 minutes
|
|
|
|
check manager.discoveredServices.len == 1
|
|
check manager.discoveredServices.hasKey("fresh-service")
|
|
check not manager.discoveredServices.hasKey("stale-service")
|
|
|
|
# ==========================================================================
|
|
# Test: Distributed UTCP Addressing
|
|
# ==========================================================================
|
|
|
|
test "Parse distributed UTCP address":
|
|
let result = parseDistributedUTCPAddress("utcp://example.com:8080/nippel/test")
|
|
check result.isOk
|
|
|
|
let address = result.value
|
|
check address.scheme == "utcp"
|
|
check address.host == "example.com"
|
|
check address.port == 8080
|
|
check address.resource == "/nippel/test"
|
|
|
|
test "Parse UTCP address without port":
|
|
let result = parseDistributedUTCPAddress("utcp://localhost/nexter/container1")
|
|
check result.isOk
|
|
|
|
let address = result.value
|
|
check address.scheme == "utcp"
|
|
check address.host == "localhost"
|
|
check address.port == 8080 # Default port
|
|
check address.resource == "/nexter/container1"
|
|
|
|
test "Parse invalid UTCP address":
|
|
let result = parseDistributedUTCPAddress("http://example.com/test")
|
|
check result.isErr
|
|
check result.error.contains("must start with utcp://")
|
|
|
|
test "Format distributed UTCP address":
|
|
let address = DistributedUTCPAddress(
|
|
scheme: "utcp",
|
|
host: "example.com",
|
|
port: 9000,
|
|
resource: "/nippel/test",
|
|
query: initTable[string, string]()
|
|
)
|
|
|
|
let formatted = formatDistributedUTCPAddress(address)
|
|
check formatted == "utcp://example.com:9000/nippel/test"
|
|
|
|
test "Format UTCP address with default port":
|
|
let address = DistributedUTCPAddress(
|
|
scheme: "utcp",
|
|
host: "localhost",
|
|
port: 8080,
|
|
resource: "/cas/store",
|
|
query: initTable[string, string]()
|
|
)
|
|
|
|
let formatted = formatDistributedUTCPAddress(address)
|
|
check formatted == "utcp://localhost/cas/store"
|
|
|
|
test "UTCP router creation":
|
|
let router = newUTCPRouter("localhost", 8080)
|
|
check router.localAddress.host == "localhost"
|
|
check router.localAddress.port == 8080
|
|
check router.routes.len == 0
|
|
check router.neighbors.len == 0
|
|
|
|
test "Add route to router":
|
|
let router = newUTCPRouter("localhost", 8080)
|
|
|
|
let result = router.addRoute(
|
|
"utcp://remote.host:9000/nippel/test",
|
|
"192.168.1.100",
|
|
metric = 5
|
|
)
|
|
|
|
check result.isOk
|
|
check router.routes.len == 1
|
|
|
|
test "Find route in router":
|
|
let router = newUTCPRouter("localhost", 8080)
|
|
|
|
discard router.addRoute(
|
|
"utcp://remote.host:9000/nippel/test",
|
|
"192.168.1.100",
|
|
metric = 5
|
|
)
|
|
|
|
let found = router.findRoute("utcp://remote.host:9000/nippel/test")
|
|
check found.isSome
|
|
check found.get().nextHop == "192.168.1.100"
|
|
check found.get().metric == 5
|
|
|
|
let notFound = router.findRoute("utcp://nonexistent/test")
|
|
check notFound.isNone
|
|
|
|
test "Add neighbor to router":
|
|
let router = newUTCPRouter("localhost", 8080)
|
|
|
|
router.addNeighbor("192.168.1.100")
|
|
router.addNeighbor("192.168.1.101")
|
|
|
|
check router.neighbors.len == 2
|
|
check "192.168.1.100" in router.neighbors
|
|
check "192.168.1.101" in router.neighbors
|
|
|
|
# Adding same neighbor again shouldn't duplicate
|
|
router.addNeighbor("192.168.1.100")
|
|
check router.neighbors.len == 2
|
|
|
|
# ==========================================================================
|
|
# Test: Merkle Tree Synchronization
|
|
# ==========================================================================
|
|
|
|
test "Sync manager creation":
|
|
let manager = newSyncManager()
|
|
check manager.activeSessions.len == 0
|
|
check manager.syncInterval == 60
|
|
|
|
test "Start sync session":
|
|
let manager = newSyncManager()
|
|
|
|
# Create a simple merkle tree for testing
|
|
let tree = MerkleTree(
|
|
root: MerkleNode(
|
|
hash: "test-root-hash",
|
|
path: "/",
|
|
isLeaf: false,
|
|
children: @[],
|
|
size: 0
|
|
),
|
|
hashAlgorithm: "xxh3",
|
|
nodeCount: 1,
|
|
leafCount: 0
|
|
)
|
|
|
|
proc testSync() {.async.} =
|
|
let result = await manager.startSync(tree, "remote.host:8080")
|
|
check result.isOk
|
|
|
|
let sessionId = result.value
|
|
check sessionId.len > 0
|
|
check manager.activeSessions.hasKey(sessionId)
|
|
|
|
waitFor testSync()
|
|
|
|
test "Get sync status":
|
|
let manager = newSyncManager()
|
|
|
|
let tree = MerkleTree(
|
|
root: MerkleNode(
|
|
hash: "test-root-hash",
|
|
path: "/",
|
|
isLeaf: false,
|
|
children: @[], size: 0
|
|
),
|
|
hashAlgorithm: "xxh3", nodeCount: 1, leafCount: 0
|
|
)
|
|
|
|
proc testStatus() {.async.} =
|
|
let result = await manager.startSync(tree, "remote.host:8080")
|
|
check result.isOk
|
|
|
|
let sessionId = result.value
|
|
let status = manager.getSyncStatus(sessionId)
|
|
|
|
check status.isSome
|
|
check status.get().status == Syncing
|
|
check status.get().remoteAddress == "remote.host:8080"
|
|
|
|
waitFor testStatus()
|
|
|
|
test "Cancel sync session":
|
|
let manager = newSyncManager()
|
|
|
|
let tree = MerkleTree(
|
|
root: MerkleNode(
|
|
hash: "test-root-hash",
|
|
path: "/",
|
|
isLeaf: false,
|
|
children: @[], size: 0
|
|
),
|
|
hashAlgorithm: "xxh3", nodeCount: 1, leafCount: 0
|
|
)
|
|
|
|
proc testCancel() {.async.} =
|
|
let result = await manager.startSync(tree, "remote.host:8080")
|
|
check result.isOk
|
|
|
|
let sessionId = result.value
|
|
check manager.activeSessions.hasKey(sessionId)
|
|
|
|
let cancelResult = manager.cancelSync(sessionId)
|
|
check cancelResult.isOk
|
|
check not manager.activeSessions.hasKey(sessionId)
|
|
|
|
waitFor testCancel()
|
|
|
|
# ==========================================================================
|
|
# Test: High-Level Operations
|
|
# ==========================================================================
|
|
|
|
test "Build decentralized cluster":
|
|
proc testCluster() {.async.} =
|
|
let result = await buildDecentralizedCluster(
|
|
@["nippel1", "nippel2"],
|
|
@["nexter1"]
|
|
)
|
|
|
|
check result.isOk
|
|
let manager = result.value
|
|
|
|
check manager.localServices.len == 3
|
|
check manager.localServices.hasKey("nippel1")
|
|
check manager.localServices.hasKey("nippel2")
|
|
check manager.localServices.hasKey("nexter1")
|
|
|
|
waitFor testCluster()
|
|
|
|
# ==========================================================================
|
|
# Test: String Conversions
|
|
# ==========================================================================
|
|
|
|
test "DiscoveryProtocol string conversion":
|
|
check $mDNS == "mDNS"
|
|
check $DNSSD == "DNS-SD"
|
|
check $DHT == "DHT"
|
|
check $Gossip == "Gossip"
|
|
|
|
test "ServiceKind string conversion":
|
|
check $NippelService == "Nippel"
|
|
check $NexterService == "Nexter"
|
|
check $CASService == "CAS"
|
|
check $MerkleService == "Merkle"
|
|
|
|
test "SyncStatus string conversion":
|
|
check $InSync == "InSync"
|
|
check $OutOfSync == "OutOfSync"
|
|
check $Syncing == "Syncing"
|
|
check $SyncFailed == "SyncFailed"
|
|
|
|
test "DiffAction string conversion":
|
|
check $Add == "Add"
|
|
check $Remove == "Remove"
|
|
check $Update == "Update"
|
|
check $Conflict == "Conflict"
|
|
|
|
echo "✓ All Decentralized Architecture tests completed"
|