#!/usr/bin/env bats # ----------------------------------------------------------------------------- # Unit tests for # packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh # # render_cozystack_version_manifest emits the cozystack-version # ConfigMap manifest on stdout, always carrying the # platform.cozystack.io/no-delete=true label that the # cozystack-no-delete-guardrail ValidatingAdmissionPolicy keys on. Migrations # apply it under the default kubectl field manager; a label-less apply by that # same manager would strip the label, so the manifest is centralized here and # every migration sources it instead of copy-pasting a heredoc. # # CI runs this file under Bats through `make bats-unit-tests`. It also remains # compatible with the legacy `hack/cozytest.sh` translator, whose awk parser # recognizes only @test blocks and a bare `}` on its own line. The tests avoid # `run` and `$status` for that compatibility path and use direct shell # assertions. Each test runs in its own process, so NAMESPACE does not leak. # # Run with: bats hack/cozystack-version-stamp.bats # ----------------------------------------------------------------------------- load test_helper @test "renders parseable YAML for a version" { . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh render_cozystack_version_manifest 45 | yq . >/dev/null } @test "carries the no-delete label set to true" { . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh val=$(render_cozystack_version_manifest 45 \ | yq -r '.metadata.labels."platform.cozystack.io/no-delete"') [ "$val" = "true" ] } @test "names the ConfigMap cozystack-version" { . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh name=$(render_cozystack_version_manifest 45 | yq -r '.metadata.name') [ "$name" = "cozystack-version" ] } @test "stamps the requested version as a quoted string" { # The version MUST render as a quoted YAML string; an unquoted numeric # scalar (version: 45) would change the ConfigMap data type the rest of # the platform reads as a string. . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh out=$(render_cozystack_version_manifest 45) printf '%s\n' "$out" | grep -qx ' version: "45"' v=$(printf '%s\n' "$out" | yq -r '.data.version') [ "$v" = "45" ] } @test "defaults namespace to cozy-system when NAMESPACE is unset" { unset NAMESPACE . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh ns=$(render_cozystack_version_manifest 45 | yq -r '.metadata.namespace') [ "$ns" = "cozy-system" ] } @test "honors a NAMESPACE override" { . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh out=$(export NAMESPACE=cozy-other; render_cozystack_version_manifest 45) ns=$(printf '%s\n' "$out" | yq -r '.metadata.namespace') [ "$ns" = "cozy-other" ] } @test "requires a version argument" { . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh if ( render_cozystack_version_manifest ) 2>/dev/null; then echo "expected non-zero exit when version arg is missing" >&2 exit 1 fi } @test "stamp_cozystack_version requires a version argument" { # The guard fires before the kubectl pipe, so this needs no cluster: a # missing version must abort rather than apply empty input to kubectl. . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh if ( stamp_cozystack_version ) 2>/dev/null; then echo "expected non-zero exit when version arg is missing" >&2 exit 1 fi } @test "render output matches the canonical labeled manifest" { # Golden test: pins the exact bytes migrations 42/43/44 apply today, so the # refactor that routes them through this helper is provably manifest-stable. unset NAMESPACE . packages/core/platform/images/migrations/migrations/lib/cozystack-version.sh expected=$(cat <<'EOF' apiVersion: v1 kind: ConfigMap metadata: name: cozystack-version namespace: cozy-system labels: platform.cozystack.io/no-delete: "true" data: version: "45" EOF ) actual=$(render_cozystack_version_manifest 45) if [ "$actual" != "$expected" ]; then printf 'expected:\n%s\n---\nactual:\n%s\n' "$expected" "$actual" >&2 exit 1 fi } @test "no go-forward stamp bypasses the helper (migrations >= 42 and run-migrations.sh)" { # The helper exists so go-forward stamps cannot drift back to a label-less # apply that drops the no-delete label. Migrations 1-41 are frozen and # backfilled by 42, so they are exempt; migration 42 onward and the # bootstrap path must each source the helper, call its API, and never stamp # cozystack-version directly with kubectl. Blocking only the historical # 'kubectl create configmap' shape is too weak — an inline 'kubectl apply' # of a hand-rolled label-less manifest would slip past it. mig_dir=packages/core/platform/images/migrations/migrations files="" for f in "$mig_dir"/*; do n=$(basename "$f") case "$n" in *[!0-9]*) continue ;; esac # skip lib/ and other non-numeric [ "$n" -ge 42 ] || continue files="$files $f" done files="$files packages/core/platform/images/migrations/run-migrations.sh" for f in $files; do if ! grep -Eq 'cozystack-version\.sh' "$f"; then echo "$f does not source the cozystack-version helper" >&2 exit 1 fi if ! grep -Eq '(render_cozystack_version_manifest|stamp_cozystack_version)' "$f"; then echo "$f does not call the cozystack-version helper API" >&2 exit 1 fi # Strip full-comment lines first: a comment that explains why a stamp # routes through the helper must not read as a direct stamp itself. A # real bypass command never starts with '#', so the guard keeps its # strength against actual direct stamping. if grep -v '^[[:space:]]*#' "$f" | grep -Eq 'kubectl[[:space:]].*(create[[:space:]]+configmap|apply).*cozystack-version'; then echo "$f stamps cozystack-version directly instead of via the helper" >&2 exit 1 fi done }