318 lines
9.9 KiB
Nim
318 lines
9.9 KiB
Nim
## 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
|