#!/usr/bin/env bash # Tests scripts/publish-site.sh without ever touching real AWS: fake `aws` and # `tofu` binaries go first on PATH, so every check here runs against what # publish-site.sh would have done rather than against a real bucket and a real # distribution. # # What is worth a real test rather than a read-through: # # 1. Without working credentials it refuses, before uploading anything. # 2. A first publish, before the stack exists, says how to create it rather # than failing inside the aws CLI. # 3. A build tree with no 404.html is refused before anything is uploaded: # the distribution answers every miss with that page (see # infra/site/main.tf), so a tree without one is not publishable. # 4. A dry run plans and writes nothing — no `s3 cp`, no `tofu apply`, no # invalidation. # 5. --publish uploads the tree under `releases//`, hands that same sha # to `tofu apply`, waits for the distribution, and only then invalidates. # The order is the point: an invalidation issued before the config lands # re-caches the tree it was meant to clear. # 6. The upload is two passes, and the content-addressed one is the pass # carrying the immutable Cache-Control. # # Usage: scripts/test-publish-site.sh set -euo pipefail cd "$(dirname "$0")/.." workdir="$(mktemp -d)" # A previous real build's site/dist/ is not this test's to destroy: it is # moved aside for the duration and always restored, pass or fail, so running # this alongside `scripts/build-site.sh` (as the site-build and # publish-site-tests prek hooks both do, in the same `prek run --all-files`) # never leaves a real build silently replaced by this test's stub tree. dist_backup="$workdir/dist-backup" if [ -d site/dist ]; then mv site/dist "$dist_backup" fi restore_dist() { rm -rf site/dist if [ -d "$dist_backup" ]; then mv "$dist_backup" site/dist fi rm -rf "$workdir" } trap restore_dist EXIT log="$workdir/calls.log" : >"$log" # Minimal stand-ins for the two CLIs. Every invocation is appended to $log # verbatim, so the assertions below read as "was this command issued, and in # what order" checks. `creds` toggles whether the credential probe succeeds. cat >"$workdir/aws" <>"$log" case "\$1 \$2" in "sts get-caller-identity") if [ -n "\${FAKE_AWS_NO_CREDS:-}" ]; then echo "Unable to locate credentials" >&2 exit 255 fi echo "arn:aws:sts::000000000000:assumed-role/test/test" ;; "s3api head-bucket") if [ -n "\${FAKE_AWS_NO_BUCKET:-}" ]; then exit 254 fi ;; "cloudfront create-invalidation") echo "ITESTINVALIDATION" ;; esac exit 0 EOF cat >"$workdir/tofu" <>"$log" for arg in "\$@"; do if [ "\$arg" = "output" ]; then echo "ETESTDISTRIBUTION" fi done exit 0 EOF chmod +x "$workdir/aws" "$workdir/tofu" export PATH="$workdir:$PATH" # Fake a build output rather than running the real (slow) build. seed_dist() { rm -rf site/dist mkdir -p site/dist/_astro site/dist/features echo "" >site/dist/index.html echo "404" >site/dist/404.html echo "" >site/dist/features/index.html echo "body{}" >site/dist/_astro/fake.hash.css } seed_dist status=0 fail() { echo "FAIL: $1" >&2 status=1 } echo "=== a failing credential check is refused, before any upload ===" >&2 : >"$log" if FAKE_AWS_NO_CREDS=1 scripts/publish-site.sh --skip-build >"$workdir/out" 2>&1; then fail "publish should have failed with no credentials" elif ! grep -q "credential check failed" "$workdir/out"; then fail "the refusal did not say the credential check failed" cat "$workdir/out" >&2 fi if grep -q '^aws s3 sync' "$log"; then fail "the tree was uploaded despite the credential check failing" fi echo "=== a first publish, with no bucket yet, says how to create it ===" >&2 : >"$log" if FAKE_AWS_NO_BUCKET=1 scripts/publish-site.sh --skip-build >"$workdir/out" 2>&1; then fail "publish should have failed with no bucket to upload to" elif ! grep -q "tofu -chdir=infra/site apply" "$workdir/out"; then fail "the refusal did not say how to create the stack" cat "$workdir/out" >&2 fi if grep -q '^aws s3 sync' "$log"; then fail "the tree was uploaded despite the bucket not existing" fi echo "=== a tree with no 404.html is refused, before any upload ===" >&2 : >"$log" rm site/dist/404.html if scripts/publish-site.sh --skip-build >"$workdir/out" 2>&1; then fail "publish should have failed with no 404.html in the tree" elif ! grep -q "404.html" "$workdir/out"; then fail "the refusal did not name 404.html" cat "$workdir/out" >&2 fi if grep -q '^aws s3 sync' "$log"; then fail "the tree was uploaded despite it carrying no 404.html" fi seed_dist echo "=== --dry-run plans and writes nothing ===" >&2 : >"$log" if ! scripts/publish-site.sh --skip-build --dry-run >"$workdir/out" 2>&1; then fail "the dry run should succeed against the stubs" cat "$workdir/out" >&2 fi if ! grep -q "dry run" "$workdir/out"; then fail "the dry run did not say it was a dry run" fi if ! grep -q '^tofu -chdir=infra/site plan' "$log"; then fail "the dry run did not plan the stack" cat "$log" >&2 fi if [ "$(grep -c -- '^aws s3 sync .*--dryrun' "$log" || true)" -ne 2 ]; then fail "the dry run did not rehearse both upload passes" cat "$log" >&2 fi if grep -qE '^tofu -chdir=infra/site apply|create-invalidation' "$log"; then fail "the dry run applied or invalidated" cat "$log" >&2 fi echo "=== a plain run uploads the tree, flips the origin, then invalidates ===" >&2 : >"$log" if ! scripts/publish-site.sh --skip-build >"$workdir/out" 2>&1; then fail "a plain run should succeed against the stubs" cat "$workdir/out" >&2 fi uploads="$(grep -c '^aws s3 sync' "$log" || true)" if [ "$uploads" -ne 2 ]; then fail "expected two upload passes (default caching, then immutable), saw $uploads" cat "$log" >&2 fi if grep -q -- '--dryrun' "$log"; then fail "a plain run rehearsed the upload instead of making it" fi # The release is the commit, and the whole tree lands under that one prefix: # this is what makes the apply below a pointer move onto a finished tree # rather than onto a bucket half-rewritten. release="$(git rev-parse --short=12 HEAD)" if [ -n "$(git status --porcelain --untracked-files=no)" ]; then release="${release}-dirty" fi if [ "$(grep -c "s3://did-bot-site/releases/${release}" "$log" || true)" -ne "$uploads" ]; then fail "not every upload pass went to releases/${release}" cat "$log" >&2 fi if ! grep -q "tofu -chdir=infra/site apply -var release_sha=${release}" "$log"; then fail "the apply did not carry the sha the tree was uploaded under" cat "$log" >&2 fi if ! grep -q '^aws cloudfront wait distribution-deployed' "$log"; then fail "the publish did not wait for the distribution to deploy" fi apply_line="$(grep -n 'tofu -chdir=infra/site apply' "$log" | head -1 | cut -d: -f1)" wait_line="$(grep -n 'cloudfront wait distribution-deployed' "$log" | head -1 | cut -d: -f1)" invalidate_line="$(grep -n 'cloudfront create-invalidation' "$log" | head -1 | cut -d: -f1)" if [ -z "$invalidate_line" ]; then fail "the publish did not invalidate the distribution" elif [ "$apply_line" -gt "$wait_line" ] || [ "$wait_line" -gt "$invalidate_line" ]; then fail "expected apply, then wait, then invalidate; saw $apply_line, $wait_line, $invalidate_line" cat "$log" >&2 fi # The content-addressed pass may be kept forever; the pass beside it may not, # or a reader holds a stale page long after the invalidation that cleared it. immutable="$(grep '^aws s3 sync' "$log" | grep -c -- '--cache-control public, max-age=31536000, immutable' || true)" if [ "$immutable" -ne 1 ]; then fail "expected exactly one immutable upload pass, saw $immutable" cat "$log" >&2 fi if ! grep '^aws s3 sync' "$log" | grep -- '--cache-control' | grep -q -- '--include _astro/\*'; then fail "the immutable pass does not name the content-addressed paths" cat "$log" >&2 fi if [ "$status" -eq 0 ]; then echo "test-publish-site: ok" >&2 fi exit "$status"