#!/usr/bin/env bash set -euo pipefail # NORA E2E Smoke Test # Starts NORA, runs real-world scenarios, verifies results. # Exit code 0 = all passed, non-zero = failures. NORA_BIN="${NORA_BIN:-./target/release/nora}" PORT="${NORA_TEST_PORT:-14000}" BASE="http://localhost:${PORT}" STORAGE_DIR=$(mktemp -d) PASSED=0 FAILED=0 NORA_PID="" cleanup() { [ -n "$NORA_PID" ] && kill "$NORA_PID" 2>/dev/null || true rm -rf "$STORAGE_DIR" } trap cleanup EXIT fail() { echo " FAIL: $1" FAILED=$((FAILED + 1)) } pass() { echo " PASS: $1" PASSED=$((PASSED + 1)) } check() { local desc="$1" shift if "$@" >/dev/null 2>&1; then pass "$desc" else fail "$desc" fi } echo "=== NORA Smoke Test ===" echo "Binary: $NORA_BIN" echo "Port: $PORT" echo "Storage: $STORAGE_DIR" echo "" # Start NORA NORA_HOST=127.0.0.1 \ NORA_PORT=$PORT \ NORA_STORAGE_PATH="$STORAGE_DIR" \ NORA_RATE_LIMIT_ENABLED=false \ NORA_PUBLIC_URL="$BASE" \ "$NORA_BIN" serve & NORA_PID=$! # Wait for startup for i in $(seq 1 20); do curl -sf "$BASE/health" >/dev/null 2>&1 && break sleep 0.5 done echo "--- Health & Monitoring ---" check "GET /health returns healthy" \ curl -sf "$BASE/health" check "GET /ready returns 200" \ curl -sf "$BASE/ready" check "GET /metrics returns prometheus" \ curl -sf "$BASE/metrics" echo "" echo "--- npm Proxy ---" # Fetch metadata — triggers proxy cache METADATA=$(curl -sf "$BASE/npm/chalk" 2>/dev/null || echo "{}") check "npm metadata returns 200" \ curl -sf "$BASE/npm/chalk" # URL rewriting check TARBALL_URL=$(echo "$METADATA" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('versions',{}).get('5.4.1',{}).get('dist',{}).get('tarball',''))" 2>/dev/null || echo "") if echo "$TARBALL_URL" | grep -q "localhost:${PORT}/npm"; then pass "npm tarball URL rewritten to NORA" else fail "npm tarball URL not rewritten: $TARBALL_URL" fi # Fetch tarball check "npm tarball download" \ curl -sf "$BASE/npm/chalk/-/chalk-5.4.1.tgz" -o /dev/null # Scoped package check "npm scoped package @babel/parser" \ curl -sf "$BASE/npm/@babel/parser" # Publish PUBLISH_RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ -H "Content-Type: application/json" \ -d '{"name":"smoke-test-pkg","versions":{"1.0.0":{"name":"smoke-test-pkg","version":"1.0.0","dist":{}}},"dist-tags":{"latest":"1.0.0"},"_attachments":{"smoke-test-pkg-1.0.0.tgz":{"data":"dGVzdA==","content_type":"application/octet-stream"}}}' \ "$BASE/npm/smoke-test-pkg") if [ "$PUBLISH_RESULT" = "201" ]; then pass "npm publish returns 201" else fail "npm publish returned $PUBLISH_RESULT" fi # Version immutability DUPE_RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ -H "Content-Type: application/json" \ -d '{"name":"smoke-test-pkg","versions":{"1.0.0":{"name":"smoke-test-pkg","version":"1.0.0","dist":{}}},"dist-tags":{"latest":"1.0.0"},"_attachments":{"smoke-test-pkg-1.0.0.tgz":{"data":"dGVzdA==","content_type":"application/octet-stream"}}}' \ "$BASE/npm/smoke-test-pkg") if [ "$DUPE_RESULT" = "409" ]; then pass "npm version immutability (409 on duplicate)" else fail "npm duplicate publish returned $DUPE_RESULT, expected 409" fi # Security: name mismatch MISMATCH_RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ -H "Content-Type: application/json" \ -d '{"name":"evil-pkg","versions":{"1.0.0":{}},"_attachments":{"a.tgz":{"data":"dGVzdA=="}}}' \ "$BASE/npm/lodash") if [ "$MISMATCH_RESULT" = "400" ]; then pass "npm name mismatch rejected (400)" else fail "npm name mismatch returned $MISMATCH_RESULT, expected 400" fi # Security: path traversal TRAVERSAL_RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ -H "Content-Type: application/json" \ -d '{"name":"test-pkg","versions":{"1.0.0":{}},"_attachments":{"../../etc/passwd":{"data":"dGVzdA=="}}}' \ "$BASE/npm/test-pkg") if [ "$TRAVERSAL_RESULT" = "400" ]; then pass "npm path traversal rejected (400)" else fail "npm path traversal returned $TRAVERSAL_RESULT, expected 400" fi echo "" echo "--- Maven ---" check "Maven proxy download" \ curl -sf "$BASE/maven2/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.pom" -o /dev/null echo "" echo "--- PyPI ---" check "PyPI simple index" \ curl -sf "$BASE/simple/" check "PyPI package page" \ curl -sf "$BASE/simple/requests/" echo "" echo "--- Docker ---" check "Docker v2 check" \ curl -sf "$BASE/v2/" echo "" echo "--- Raw ---" echo "raw-test-data" | curl -sf -X PUT --data-binary @- "$BASE/raw/smoke/test.txt" >/dev/null 2>&1 check "Raw upload" \ curl -sf "$BASE/raw/smoke/test.txt" -o /dev/null echo "" echo "--- UI & API ---" check "UI dashboard loads" \ curl -sf "$BASE/ui/" check "OpenAPI docs" \ curl -sf "$BASE/api-docs" -o /dev/null # Dashboard stats — check npm count > 0 after proxy fetches sleep 1 STATS=$(curl -sf "$BASE/ui/api/stats" 2>/dev/null || echo "{}") NPM_COUNT=$(echo "$STATS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('npm',0))" 2>/dev/null || echo "0") if [ "$NPM_COUNT" -gt 0 ] 2>/dev/null; then pass "Dashboard npm count > 0 (got $NPM_COUNT)" else fail "Dashboard npm count is $NPM_COUNT, expected > 0" fi echo "" echo "--- Docker Push/Pull + Digest Verify ---" # Create a minimal Docker image, push, pull, verify digest DOCKER_AVAILABLE=true if ! docker info >/dev/null 2>&1; then echo " SKIP: Docker daemon not available" DOCKER_AVAILABLE=false fi if [ "$DOCKER_AVAILABLE" = true ]; then DOCKER_IMG="localhost:${PORT}/smoke-test/hello:v1" # Create tiny image from scratch DOCKER_BUILD_DIR=$(mktemp -d) echo "FROM scratch" > "$DOCKER_BUILD_DIR/Dockerfile" echo "smoke-test" > "$DOCKER_BUILD_DIR/data.txt" echo "COPY data.txt /data.txt" >> "$DOCKER_BUILD_DIR/Dockerfile" if docker build -t "$DOCKER_IMG" "$DOCKER_BUILD_DIR" >/dev/null 2>&1; then pass "docker build smoke image" else fail "docker build smoke image" fi rm -rf "$DOCKER_BUILD_DIR" # Push if docker push "$DOCKER_IMG" >/dev/null 2>&1; then pass "docker push to NORA" else fail "docker push to NORA" fi # Get digest from registry MANIFEST=$(curl -sf -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ "$BASE/v2/smoke-test/hello/manifests/v1" 2>/dev/null || echo "") if [ -n "$MANIFEST" ] && echo "$MANIFEST" | python3 -c "import sys,json; json.load(sys.stdin)" >/dev/null 2>&1; then pass "docker manifest retrievable from NORA" else fail "docker manifest not retrievable" fi # Remove local image and pull back docker rmi "$DOCKER_IMG" >/dev/null 2>&1 || true if docker pull "$DOCKER_IMG" >/dev/null 2>&1; then pass "docker pull from NORA" else fail "docker pull from NORA" fi # Verify digest matches: push digest == pull digest PUSH_DIGEST=$(docker inspect "$DOCKER_IMG" --format='{{index .RepoDigests 0}}' 2>/dev/null | cut -d@ -f2) if [ -n "$PUSH_DIGEST" ] && echo "$PUSH_DIGEST" | grep -q "^sha256:"; then pass "docker digest verified (${PUSH_DIGEST:0:20}...)" else fail "docker digest verification failed" fi # Cleanup docker rmi "$DOCKER_IMG" >/dev/null 2>&1 || true fi echo "" echo "--- npm Install + Integrity Verify ---" # Test real npm client against NORA (not just curl) NPM_TEST_DIR=$(mktemp -d) cd "$NPM_TEST_DIR" # Create minimal package.json cat > package.json << 'PKGJSON' { "name": "nora-smoke-test", "version": "1.0.0", "dependencies": { "chalk": "5.4.1" } } PKGJSON # npm install using NORA as registry if npm install --registry "$BASE/npm/" --prefer-online --no-audit --no-fund >/dev/null 2>&1; then pass "npm install chalk via NORA registry" else fail "npm install chalk via NORA registry" fi # Verify package was installed if [ -f "node_modules/chalk/package.json" ]; then INSTALLED_VER=$(python3 -c "import json; print(json.load(open('node_modules/chalk/package.json'))['version'])" 2>/dev/null || echo "") if [ "$INSTALLED_VER" = "5.4.1" ]; then pass "npm installed correct version (5.4.1)" else fail "npm installed wrong version: $INSTALLED_VER" fi else fail "npm node_modules/chalk not found" fi # Verify integrity: check that package-lock.json has sha512 integrity if [ -f "package-lock.json" ]; then INTEGRITY=$(python3 -c " import json lock = json.load(open('package-lock.json')) pkgs = lock.get('packages', {}) chalk = pkgs.get('node_modules/chalk', pkgs.get('chalk', {})) print(chalk.get('integrity', '')) " 2>/dev/null || echo "") if echo "$INTEGRITY" | grep -q "^sha512-"; then pass "npm integrity hash present (sha512)" else fail "npm integrity hash missing: $INTEGRITY" fi else fail "npm package-lock.json not created" fi cd /tmp rm -rf "$NPM_TEST_DIR" echo "" echo "--- Upstream Timeout Handling ---" # Verify that requesting a non-existent package from upstream returns 404 quickly (not hang) TIMEOUT_START=$(date +%s) TIMEOUT_RESULT=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \ "$BASE/npm/@nora-smoke-test/nonexistent-package-xyz-12345") TIMEOUT_END=$(date +%s) TIMEOUT_DURATION=$((TIMEOUT_END - TIMEOUT_START)) if [ "$TIMEOUT_RESULT" = "404" ]; then pass "upstream 404 returned correctly" else fail "upstream returned $TIMEOUT_RESULT, expected 404" fi if [ "$TIMEOUT_DURATION" -lt 10 ]; then pass "upstream 404 returned in ${TIMEOUT_DURATION}s (< 10s)" else fail "upstream 404 took ${TIMEOUT_DURATION}s (too slow, retry may hang)" fi echo "" # ============================================ # Go Proxy Tests # ============================================ echo "" echo "=== Go Proxy ===" # Pre-seed a Go module for testing GO_MODULE="example.com/testmod" GO_VERSION="v1.0.0" GO_STORAGE="$STORAGE_DIR/go" mkdir -p "$GO_STORAGE/example.com/testmod/@v" # Create .info file echo '{"Version":"v1.0.0","Time":"2026-01-01T00:00:00Z"}' > "$GO_STORAGE/example.com/testmod/@v/v1.0.0.info" # Create .mod file echo 'module example.com/testmod go 1.21' > "$GO_STORAGE/example.com/testmod/@v/v1.0.0.mod" # Create list file echo "v1.0.0" > "$GO_STORAGE/example.com/testmod/@v/list" # Test: Go module list check "Go list versions" \ curl -sf "$BASE/go/example.com/testmod/@v/list" -o /dev/null # Test: Go module .info INFO_RESULT=$(curl -sf "$BASE/go/example.com/testmod/@v/v1.0.0.info" 2>/dev/null) if echo "$INFO_RESULT" | grep -q "v1.0.0"; then pass "Go .info returns version" else fail "Go .info: $INFO_RESULT" fi # Test: Go module .mod MOD_RESULT=$(curl -sf "$BASE/go/example.com/testmod/@v/v1.0.0.mod" 2>/dev/null) if echo "$MOD_RESULT" | grep -q "module example.com/testmod"; then pass "Go .mod returns module content" else fail "Go .mod: $MOD_RESULT" fi # Test: Go @latest (200 with upstream, 404 without — both valid) LATEST_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/go/example.com/testmod/@latest") if [ "$LATEST_CODE" = "200" ] || [ "$LATEST_CODE" = "404" ]; then pass "Go @latest handled ($LATEST_CODE)" else fail "Go @latest returned $LATEST_CODE" fi # Test: Go path traversal rejection TRAVERSAL_RESULT=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/go/../../etc/passwd/@v/list") if [ "$TRAVERSAL_RESULT" = "400" ] || [ "$TRAVERSAL_RESULT" = "404" ]; then pass "Go path traversal rejected ($TRAVERSAL_RESULT)" else fail "Go path traversal returned $TRAVERSAL_RESULT" fi # Test: Go nonexistent module NOTFOUND=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/go/nonexistent.com/pkg/@v/list") if [ "$NOTFOUND" = "404" ]; then pass "Go 404 on nonexistent module" else fail "Go nonexistent returned $NOTFOUND" fi # ============================================ # Raw Registry Extended Tests # ============================================ echo "" echo "=== Raw Registry (extended) ===" # Test: Raw upload and download (basic — already exists, extend) echo "integration-test-data-$(date +%s)" | curl -sf -X PUT --data-binary @- "$BASE/raw/integration/test.txt" >/dev/null 2>&1 check "Raw upload + download" \ curl -sf "$BASE/raw/integration/test.txt" -o /dev/null # Test: Raw HEAD (check exists) HEAD_RESULT=$(curl -sf -o /dev/null -w "%{http_code}" --head "$BASE/raw/integration/test.txt") if [ "$HEAD_RESULT" = "200" ]; then pass "Raw HEAD returns 200" else fail "Raw HEAD returned $HEAD_RESULT" fi # Test: Raw 404 on nonexistent NOTFOUND=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/raw/nonexistent/file.bin") if [ "$NOTFOUND" = "404" ]; then pass "Raw 404 on nonexistent file" else fail "Raw nonexistent returned $NOTFOUND" fi # Test: Raw path traversal TRAVERSAL=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/raw/../../../etc/passwd") if [ "$TRAVERSAL" = "400" ] || [ "$TRAVERSAL" = "404" ]; then pass "Raw path traversal rejected ($TRAVERSAL)" else fail "Raw path traversal returned $TRAVERSAL" fi # Test: Raw overwrite echo "version-1" | curl -sf -X PUT --data-binary @- "$BASE/raw/integration/overwrite.txt" >/dev/null 2>&1 echo "version-2" | curl -sf -X PUT --data-binary @- "$BASE/raw/integration/overwrite.txt" >/dev/null 2>&1 CONTENT=$(curl -sf "$BASE/raw/integration/overwrite.txt" 2>/dev/null) if [ "$CONTENT" = "version-2" ]; then pass "Raw overwrite works" else fail "Raw overwrite: got '$CONTENT'" fi # Test: Raw delete curl -sf -X DELETE "$BASE/raw/integration/overwrite.txt" >/dev/null 2>&1 DELETE_CHECK=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/raw/integration/overwrite.txt") if [ "$DELETE_CHECK" = "404" ]; then pass "Raw delete works" else fail "Raw delete: file still returns $DELETE_CHECK" fi # Test: Raw binary data (not just text) dd if=/dev/urandom bs=1024 count=10 2>/dev/null | curl -sf -X PUT --data-binary @- "$BASE/raw/integration/binary.bin" >/dev/null 2>&1 BIN_SIZE=$(curl -sf "$BASE/raw/integration/binary.bin" 2>/dev/null | wc -c) if [ "$BIN_SIZE" -ge 10000 ]; then pass "Raw binary upload/download (${BIN_SIZE} bytes)" else fail "Raw binary: expected ~10240, got $BIN_SIZE" fi echo "--- Mirror CLI ---" # Create a minimal lockfile LOCKFILE=$(mktemp) cat > "$LOCKFILE" << 'EOF' { "lockfileVersion": 3, "packages": { "": { "name": "test" }, "node_modules/chalk": { "version": "5.4.1" } } } EOF MIRROR_RESULT=$("$NORA_BIN" mirror --registry "$BASE" npm --lockfile "$LOCKFILE" 2>&1) if echo "$MIRROR_RESULT" | grep -q "Failed: 0"; then pass "nora mirror npm --lockfile (0 failures)" else fail "nora mirror: $MIRROR_RESULT" fi rm -f "$LOCKFILE" echo "" echo "================================" echo "Results: $PASSED passed, $FAILED failed" echo "================================" [ "$FAILED" -eq 0 ] && exit 0 || exit 1