## NEXTER Archive Handler Tests ## ## Tests for the NEXTER archive handler that creates and parses .nexter containers. ## This verifies that containers can be packaged and extracted correctly. import std/[unittest, os, tempfiles, options, strutils, times, tables] import nip/nexter import nip/nexter_manifest import nip/manifest_parser # Helper to create a fully initialized NEXTER manifest proc createTestManifest(name: string, version: string, description: string = "Test container"): NEXTERManifest = let buildDate = parse("2025-11-28T12:00:00Z", "yyyy-MM-dd'T'HH:mm:ss'Z'") return NEXTERManifest( name: name, version: parseSemanticVersion(version), buildDate: buildDate, metadata: ContainerInfo( description: description, license: "MIT" ), provenance: ProvenanceInfo( source: "https://example.com/source.tar.gz", sourceHash: "xxh3-source-hash", buildTimestamp: buildDate, builder: some("test-builder") ), buildConfig: BuildConfiguration( configureFlags: @[], compilerFlags: @[], compilerVersion: "gcc-13", targetArchitecture: "x86_64", libc: "musl", allocator: "jemalloc", buildSystem: "custom" ), base: BaseConfig( baseImage: some("alpine"), baseVersion: some("3.18") ), environment: initTable[string, string](), casChunks: @[], namespace: ContainerNamespace( isolationType: "full", capabilities: @[], mounts: @[], devices: @[] ), startup: StartupConfig( command: @["/bin/sh"], workingDir: "/", user: none(string), entrypoint: none(string) ), buildHash: "xxh3-build-hash", signature: SignatureInfo( algorithm: "ed25519", keyId: "test-key", signature: "test-sig" ) ) suite "NEXTER Archive Handler Tests": setup: let tempDir = createTempDir("nexter_test_archive_", "") let archivePath = tempDir / "test-container.nexter" teardown: removeDir(tempDir) test "Create NEXTER archive with all components": ## Verify NEXTER archive can be created with manifest, environment, chunks, and signature # Create manifest var manifest = createTestManifest("test-container", "1.0.0") # Create environment let environment = "PATH=/usr/bin:/bin\nHOME=/home/user" # Create chunks var chunks: seq[ChunkData] = @[ ChunkData( hash: "xxh3-chunk1", data: "chunk1 data", size: 11, chunkType: Binary ), ChunkData( hash: "xxh3-chunk2", data: "chunk2 data", size: 11, chunkType: Config ) ] # Create signature let signature = "ed25519-signature-placeholder" # Create archive createNEXTER(manifest, environment, chunks, signature, archivePath) # Verify archive exists check fileExists(archivePath) check getFileSize(archivePath) > 0 test "Parse NEXTER archive and extract components": ## Verify NEXTER archive can be parsed and all components extracted # Create and write archive first var manifest = createTestManifest("parse-test", "2.0.0") let environment = "TEST=value" var chunks: seq[ChunkData] = @[ ChunkData( hash: "xxh3-parse-chunk", data: "parse test data", size: 15, chunkType: Binary ) ] let signature = "ed25519-parse-sig" createNEXTER(manifest, environment, chunks, signature, archivePath) # Parse archive let container = parseNEXTER(archivePath) # Verify components check container.manifest.name == "parse-test" check container.manifest.version.major == 2 check container.environment.contains("TEST=value") check container.signature == "ed25519-parse-sig" check container.chunks.len == 1 test "Archive creation fails for non-existent output path": ## Verify error handling for invalid output paths let invalidPath = "/nonexistent/path/container.nexter" var manifest = createTestManifest("fail-test", "1.0.0") # This should fail gracefully expect OSError: createNEXTER(manifest, "", @[], "", invalidPath) test "Parse fails for non-existent archive": ## Verify error handling for missing archives let nonExistentPath = tempDir / "nonexistent.nexter" expect NEXTERArchiveError: discard parseNEXTER(nonExistentPath) test "Verify NEXTER archive integrity": ## Verify archive integrity checking works # Create valid archive var manifest = createTestManifest("verify-test", "1.0.0") createNEXTER(manifest, "TEST=1", @[], "sig", archivePath) # Verify archive check verifyNEXTER(archivePath) == true test "List chunks in archive": ## Verify chunk listing works var manifest = createTestManifest("list-test", "1.0.0") var chunks: seq[ChunkData] = @[ ChunkData(hash: "xxh3-chunk-a", data: "a", size: 1, chunkType: Binary), ChunkData(hash: "xxh3-chunk-b", data: "b", size: 1, chunkType: Config), ChunkData(hash: "xxh3-chunk-c", data: "c", size: 1, chunkType: Data) ] createNEXTER(manifest, "", chunks, "", archivePath) # List chunks let chunkList = listChunksInArchive(archivePath) check chunkList.len == 3 check "xxh3-chunk-a" in chunkList check "xxh3-chunk-b" in chunkList check "xxh3-chunk-c" in chunkList test "Get archive size": ## Verify archive size calculation var manifest = createTestManifest("size-test", "1.0.0") createNEXTER(manifest, "ENV=test", @[], "sig", archivePath) let size = getArchiveSize(archivePath) check size > 0 test "Get container info from archive": ## Verify container info extraction var manifest = createTestManifest("info-test", "3.2.1", "Info test container") createNEXTER(manifest, "", @[], "", archivePath) let info = getContainerInfo(archivePath) check info.isSome check info.get().name == "info-test" check info.get().version.major == 3 check info.get().version.minor == 2 check info.get().version.patch == 1 check info.get().metadata.description == "Info test container" test "Archive with multiple chunks": ## Verify archives with many chunks work correctly var manifest = createTestManifest("multi-chunk", "1.0.0") # Create 10 chunks var chunks: seq[ChunkData] = @[] for i in 0..<10: chunks.add(ChunkData( hash: "xxh3-chunk-" & $i, data: "chunk data " & $i, size: (11 + i).int64, chunkType: Binary )) createNEXTER(manifest, "", chunks, "", archivePath) # Verify all chunks present let chunkList = listChunksInArchive(archivePath) check chunkList.len == 10 test "Archive roundtrip: create and parse": ## Verify create → parse roundtrip preserves data var manifest = createTestManifest("roundtrip-test", "1.5.2", "Roundtrip test") manifest.metadata.author = some("Roundtrip Author") let environment = "VAR1=value1\nVAR2=value2\nVAR3=value3" var chunks: seq[ChunkData] = @[ ChunkData(hash: "xxh3-rt-1", data: "roundtrip data 1", size: 16, chunkType: Binary), ChunkData(hash: "xxh3-rt-2", data: "roundtrip data 2", size: 16, chunkType: Config) ] let signature = "ed25519-roundtrip-signature" # Create archive createNEXTER(manifest, environment, chunks, signature, archivePath) # Parse archive let container = parseNEXTER(archivePath) # Verify roundtrip check container.manifest.name == "roundtrip-test" check container.manifest.version.major == 1 check container.manifest.version.minor == 5 check container.manifest.version.patch == 2 check container.manifest.metadata.description == "Roundtrip test" check container.manifest.metadata.author.get() == "Roundtrip Author" check container.environment == environment check container.signature == signature check container.chunks.len == 2 ## Property-Based Tests suite "NEXTER Archive Property Tests": test "Property: Archive creation is deterministic": ## Verify creating the same archive twice produces identical results let tempDir = createTempDir("nexter_prop_", "") defer: removeDir(tempDir) let path1 = tempDir / "archive1.nexter" let path2 = tempDir / "archive2.nexter" var manifest = createTestManifest("deterministic", "1.0.0") let environment = "FIXED=value" var chunks: seq[ChunkData] = @[ ChunkData(hash: "xxh3-det-1", data: "fixed data", size: 10, chunkType: Binary) ] let signature = "fixed-signature" # Create two archives with identical data createNEXTER(manifest, environment, chunks, signature, path1) createNEXTER(manifest, environment, chunks, signature, path2) # Verify both archives are identical let size1 = getFileSize(path1) let size2 = getFileSize(path2) check size1 == size2 test "Property: Archive parsing preserves all data": ## Verify parsing doesn't lose or corrupt data let tempDir = createTempDir("nexter_preserve_", "") defer: removeDir(tempDir) let archivePath = tempDir / "preserve.nexter" var manifest = createTestManifest("preserve-test", "2.3.4") let environment = "PRESERVE=yes\nDATA=intact" var chunks: seq[ChunkData] = @[ ChunkData(hash: "xxh3-p-1", data: "preserved", size: 9, chunkType: Binary), ChunkData(hash: "xxh3-p-2", data: "data", size: 4, chunkType: Config) ] let signature = "preserved-sig" # Create and parse createNEXTER(manifest, environment, chunks, signature, archivePath) let container = parseNEXTER(archivePath) # Verify all data preserved check container.manifest.name == manifest.name check container.manifest.version == manifest.version check container.environment == environment check container.signature == signature check container.chunks.len == chunks.len