338 lines
12 KiB
Nim
338 lines
12 KiB
Nim
## NEXTER Container Installation Tests
|
|
##
|
|
## Tests for the NEXTER container installation workflow.
|
|
## Verifies atomic installation, rollback, and verification.
|
|
|
|
import std/[unittest, os, tempfiles, options, strutils, times, tables]
|
|
import nip/nexter_installer
|
|
import nip/nexter
|
|
import nip/nexter_manifest
|
|
import nip/manifest_parser
|
|
|
|
# Helper to create a test container
|
|
proc createTestContainer(name: string, version: string): NEXTERContainer =
|
|
let buildDate = parse("2025-11-28T12:00:00Z", "yyyy-MM-dd'T'HH:mm:ss'Z'")
|
|
return NEXTERContainer(
|
|
manifest: NEXTERManifest(
|
|
name: name,
|
|
version: parseSemanticVersion(version),
|
|
buildDate: buildDate,
|
|
metadata: ContainerInfo(
|
|
description: "Test container",
|
|
license: "MIT"
|
|
),
|
|
provenance: ProvenanceInfo(
|
|
source: "https://example.com/source.tar.gz",
|
|
sourceHash: "xxh3-source-hash",
|
|
buildTimestamp: buildDate
|
|
),
|
|
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"
|
|
)
|
|
),
|
|
environment: "PATH=/usr/bin:/bin",
|
|
chunks: @[
|
|
ChunkData(
|
|
hash: "xxh3-chunk1",
|
|
data: "chunk1 data",
|
|
size: 11,
|
|
chunkType: Binary
|
|
)
|
|
],
|
|
signature: "ed25519-signature",
|
|
archivePath: ""
|
|
)
|
|
|
|
suite "NEXTER Container Installation Tests":
|
|
|
|
setup:
|
|
let tempDir = createTempDir("nexter_install_test_", "")
|
|
let storageRoot = tempDir / "storage"
|
|
createDir(storageRoot)
|
|
|
|
teardown:
|
|
removeDir(tempDir)
|
|
|
|
test "Install NEXTER container successfully":
|
|
## Verify container can be installed atomically
|
|
|
|
let container = createTestContainer("test-container", "1.0.0")
|
|
let archivePath = storageRoot / "test-container.nexter"
|
|
|
|
# Create archive
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
|
|
# Install container
|
|
let result = installNEXTER(archivePath, storageRoot)
|
|
|
|
check result.success
|
|
check result.containerName == "test-container"
|
|
check result.version == "1.0.0"
|
|
check result.chunksInstalled >= 0
|
|
check result.error == ""
|
|
|
|
test "Installation creates correct directory structure":
|
|
## Verify installation creates all required directories and files
|
|
|
|
let container = createTestContainer("struct-test", "2.0.0")
|
|
let archivePath = storageRoot / "struct-test.nexter"
|
|
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
|
|
let result = installNEXTER(archivePath, storageRoot)
|
|
|
|
check result.success
|
|
check dirExists(storageRoot / "nexters" / "struct-test")
|
|
check fileExists(storageRoot / "nexters" / "struct-test" / "manifest.kdl")
|
|
check fileExists(storageRoot / "nexters" / "struct-test" / "environment.kdl")
|
|
check fileExists(storageRoot / "nexters" / "struct-test" / "signature.sig")
|
|
|
|
test "Installation fails for already installed container":
|
|
## Verify error when container already installed
|
|
|
|
let container = createTestContainer("dup-test", "1.0.0")
|
|
let archivePath = storageRoot / "dup-test.nexter"
|
|
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
|
|
# First installation should succeed
|
|
let result1 = installNEXTER(archivePath, storageRoot)
|
|
check result1.success
|
|
|
|
# Second installation should fail
|
|
let result2 = installNEXTER(archivePath, storageRoot)
|
|
check not result2.success
|
|
check "already installed" in result2.error.toLowerAscii()
|
|
|
|
test "Check if container is installed":
|
|
## Verify isContainerInstalled() works correctly
|
|
|
|
let container = createTestContainer("check-test", "1.0.0")
|
|
let archivePath = storageRoot / "check-test.nexter"
|
|
|
|
# Before installation
|
|
check not isContainerInstalled("check-test", storageRoot)
|
|
|
|
# Install
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
discard installNEXTER(archivePath, storageRoot)
|
|
|
|
# After installation
|
|
check isContainerInstalled("check-test", storageRoot)
|
|
|
|
test "Get installed container version":
|
|
## Verify getInstalledContainerVersion() returns correct version
|
|
|
|
let container = createTestContainer("version-test", "3.2.1")
|
|
let archivePath = storageRoot / "version-test.nexter"
|
|
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
discard installNEXTER(archivePath, storageRoot)
|
|
|
|
let version = getInstalledContainerVersion("version-test", storageRoot)
|
|
check version.isSome
|
|
check version.get() == "3.2.1"
|
|
|
|
test "List installed containers":
|
|
## Verify listInstalledContainers() returns all installed containers
|
|
|
|
# Install multiple containers
|
|
for i in 1..3:
|
|
let container = createTestContainer("list-test-" & $i, "1.0.0")
|
|
let archivePath = storageRoot / ("list-test-" & $i & ".nexter")
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
discard installNEXTER(archivePath, storageRoot)
|
|
|
|
let containers = listInstalledContainers(storageRoot)
|
|
check containers.len == 3
|
|
check "list-test-1" in containers
|
|
check "list-test-2" in containers
|
|
check "list-test-3" in containers
|
|
|
|
test "Verify valid container installation":
|
|
## Verify verifyContainerInstallation() detects valid installations
|
|
|
|
let container = createTestContainer("verify-test", "1.0.0")
|
|
let archivePath = storageRoot / "verify-test.nexter"
|
|
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
discard installNEXTER(archivePath, storageRoot)
|
|
|
|
check verifyContainerInstallation("verify-test", storageRoot)
|
|
|
|
test "Verify fails for non-existent container":
|
|
## Verify verifyContainerInstallation() fails for missing containers
|
|
|
|
check not verifyContainerInstallation("nonexistent", storageRoot)
|
|
|
|
test "Format installation result - success":
|
|
## Verify formatting of successful installation result
|
|
|
|
let result = ContainerInstallResult(
|
|
success: true,
|
|
containerName: "test",
|
|
version: "1.0.0",
|
|
installPath: "/path/to/test",
|
|
chunksInstalled: 5,
|
|
error: ""
|
|
)
|
|
|
|
let formatted = $result
|
|
check "✅" in formatted
|
|
check "test" in formatted
|
|
check "1.0.0" in formatted
|
|
|
|
test "Format installation result - failure":
|
|
## Verify formatting of failed installation result
|
|
|
|
let result = ContainerInstallResult(
|
|
success: false,
|
|
containerName: "",
|
|
version: "",
|
|
installPath: "",
|
|
chunksInstalled: 0,
|
|
error: "Test error"
|
|
)
|
|
|
|
let formatted = $result
|
|
check "❌" in formatted
|
|
check "Test error" in formatted
|
|
|
|
suite "NEXTER Container Installation Property Tests":
|
|
|
|
test "Property: Successful installation has ALL artifacts":
|
|
## Verify successful installation creates all required artifacts
|
|
|
|
let tempDir = createTempDir("nexter_prop_", "")
|
|
defer: removeDir(tempDir)
|
|
|
|
let container = createTestContainer("prop-test-1", "1.0.0")
|
|
let archivePath = tempDir / "prop-test-1.nexter"
|
|
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
|
|
let result = installNEXTER(archivePath, tempDir / "storage")
|
|
|
|
if result.success:
|
|
check dirExists(tempDir / "storage" / "nexters" / "prop-test-1")
|
|
check fileExists(tempDir / "storage" / "nexters" / "prop-test-1" / "manifest.kdl")
|
|
check fileExists(tempDir / "storage" / "nexters" / "prop-test-1" / "environment.kdl")
|
|
check fileExists(tempDir / "storage" / "nexters" / "prop-test-1" / "signature.sig")
|
|
|
|
test "Property: Failed installation has NO artifacts (complete rollback)":
|
|
## Verify failed installation leaves no partial state
|
|
|
|
let tempDir = createTempDir("nexter_prop_", "")
|
|
defer: removeDir(tempDir)
|
|
|
|
# Try to install to non-existent parent directory
|
|
let result = installNEXTER("/nonexistent/path/container.nexter", tempDir / "storage")
|
|
|
|
check not result.success
|
|
# Verify no partial state was created
|
|
check not dirExists(tempDir / "storage" / "nexters")
|
|
|
|
test "Property: Multiple containers share CAS chunks":
|
|
## Verify CAS deduplication works across multiple containers
|
|
|
|
let tempDir = createTempDir("nexter_prop_", "")
|
|
defer: removeDir(tempDir)
|
|
let storageRoot = tempDir / "storage"
|
|
createDir(storageRoot)
|
|
|
|
# Create two containers with same chunk
|
|
let sharedChunk = ChunkData(
|
|
hash: "xxh3-shared",
|
|
data: "shared chunk data",
|
|
size: 16,
|
|
chunkType: Binary
|
|
)
|
|
|
|
var container1 = createTestContainer("shared-1", "1.0.0")
|
|
container1.chunks = @[sharedChunk]
|
|
|
|
var container2 = createTestContainer("shared-2", "1.0.0")
|
|
container2.chunks = @[sharedChunk]
|
|
|
|
let archive1 = storageRoot / "shared-1.nexter"
|
|
let archive2 = storageRoot / "shared-2.nexter"
|
|
|
|
createNEXTER(container1.manifest, container1.environment, container1.chunks,
|
|
container1.signature, archive1)
|
|
createNEXTER(container2.manifest, container2.environment, container2.chunks,
|
|
container2.signature, archive2)
|
|
|
|
let result1 = installNEXTER(archive1, storageRoot)
|
|
let result2 = installNEXTER(archive2, storageRoot)
|
|
|
|
check result1.success
|
|
check result2.success
|
|
|
|
# Verify both containers reference the same chunk
|
|
let refsPath1 = storageRoot / "cas" / "refs" / "nexters" / "shared-1.refs"
|
|
let refsPath2 = storageRoot / "cas" / "refs" / "nexters" / "shared-2.refs"
|
|
|
|
if fileExists(refsPath1) and fileExists(refsPath2):
|
|
let refs1 = readFile(refsPath1).split('\n')
|
|
let refs2 = readFile(refsPath2).split('\n')
|
|
check refs1[0] == refs2[0] # Same chunk hash
|
|
|
|
test "Property: Installation preserves manifest integrity":
|
|
## Verify manifest is preserved exactly through installation
|
|
|
|
let tempDir = createTempDir("nexter_prop_", "")
|
|
defer: removeDir(tempDir)
|
|
|
|
let container = createTestContainer("integrity-test", "2.5.3")
|
|
let archivePath = tempDir / "integrity-test.nexter"
|
|
|
|
createNEXTER(container.manifest, container.environment, container.chunks,
|
|
container.signature, archivePath)
|
|
|
|
let result = installNEXTER(archivePath, tempDir / "storage")
|
|
|
|
if result.success:
|
|
let manifestPath = tempDir / "storage" / "nexters" / "integrity-test" / "manifest.kdl"
|
|
check fileExists(manifestPath)
|
|
let manifestContent = readFile(manifestPath)
|
|
check "integrity-test" in manifestContent
|
|
check "2.5.3" in manifestContent
|