#!/usr/bin/env bash set -euo pipefail project_name="${MINEROPS_SMOKE_PROJECT:-minerops-smoke-$$}" dev_port="${MINEROPS_SMOKE_PORT:-18001}" keep_environment="${MINEROPS_SMOKE_KEEP:-false}" base_url="http://127.0.0.1:${dev_port}" compose=( docker compose --project-name "${project_name}" --env-file .env.dev -f compose.dev.yaml ) tmp_prefix="/tmp/${project_name}" admin_cookie="${tmp_prefix}-admin.cookies" manager_cookie="${tmp_prefix}-manager.cookies" technician_cookie="${tmp_prefix}-technician.cookies" readonly_cookie="${tmp_prefix}-readonly.cookies" cleanup() { status=$? if (( status != 0 )); then echo echo "Smoke test failed. Recent service logs:" "${compose[@]}" logs --no-color --tail 160 || true fi rm -f "${tmp_prefix}"-*.json "${tmp_prefix}"-*.cookies "${tmp_prefix}"-*.headers if [[ "${keep_environment}" != "true" ]]; then "${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true else echo "Keeping Compose project ${project_name} for inspection." fi exit "${status}" } trap cleanup EXIT status_for() { local cookie_jar="$1" local method="$2" local path="$3" local body="${4:-}" local args=(--silent --output /dev/null --write-out '%{http_code}' -X "${method}") if [[ -n "${cookie_jar}" ]]; then args+=(-b "${cookie_jar}") fi if [[ -n "${body}" ]]; then args+=(-H 'Content-Type: application/json' --data "${body}") fi curl "${args[@]}" "${base_url}${path}" } if [[ ! -f .env.dev ]]; then echo "Missing .env.dev. Run ./scripts/dev-env-init.sh first." >&2 exit 1 fi echo "Validating Compose configuration..." MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" config --quiet echo "Building and starting disposable development stack..." MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" up --detach --build echo "Waiting for the gateway health endpoint..." health_file="${tmp_prefix}-health.json" for attempt in $(seq 1 90); do if curl --fail --silent "${base_url}/api/health" >"${health_file}"; then break fi if (( attempt == 90 )); then echo "Gateway did not become healthy within 90 seconds." >&2 exit 1 fi sleep 1 done health_json="$(cat "${health_file}")" [[ "${health_json}" == *'"status":"ok"'* ]] [[ "${health_json}" == *'"environment":"development"'* ]] [[ "${health_json}" == *'"database":"ok"'* ]] [[ "${health_json}" == *'"redis":"ok"'* ]] echo "Checking migrations and required schema..." current_revision="$("${compose[@]}" exec -T backend alembic current 2>/dev/null)" [[ "${current_revision}" == *"0007_identity_and_roles"* ]] missing_tables="$( "${compose[@]}" exec -T postgres sh -c \ 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At' \ < sql/verify-development-schema.sql )" if [[ -n "${missing_tables}" ]]; then echo "Missing required tables:" >&2 echo "${missing_tables}" >&2 exit 1 fi echo "Checking the unauthenticated login gate..." setup_status="$(curl --fail --silent "${base_url}/api/auth/setup-status")" [[ "${setup_status}" == *'"setup_required":true'* ]] [[ "$(status_for "" GET /login)" == "200" ]] for path in dashboard repairs locations system-health accounts; do [[ "$(status_for "" GET "/${path}")" == "303" ]] done [[ "$(status_for "" GET /api/scanner/status)" == "401" ]] echo "Creating the first administrator and role test accounts..." setup_response="$(curl --fail --silent -c "${admin_cookie}" \ -H 'Content-Type: application/json' \ --data '{"username":"admin","display_name":"Test Administrator","email":"admin@example.com","password":"MinerOpsTest!123"}' \ "${base_url}/api/auth/setup-admin")" [[ "${setup_response}" == *'"role":"Admin"'* ]] [[ "$(status_for "${admin_cookie}" GET /dashboard)" == "200" ]] [[ "$(status_for "${admin_cookie}" GET /accounts)" == "200" ]] for account in \ 'manager|Operations Manager|Manager|manager@example.com' \ 'technician|Repair Technician|Technician|technician@example.com' \ 'readonly|Read Only User|Read Only|readonly@example.com'; do IFS='|' read -r username display_name role email <<<"${account}" response="$(curl --fail --silent -b "${admin_cookie}" \ -H 'Content-Type: application/json' \ --data "{\"username\":\"${username}\",\"display_name\":\"${display_name}\",\"email\":\"${email}\",\"role\":\"${role}\",\"password\":\"MinerOpsTest!123\"}" \ "${base_url}/api/admin/users")" [[ "${response}" == *"\"role\":\"${role}\""* ]] done roles_response="$(curl --fail --silent -b "${admin_cookie}" "${base_url}/api/admin/roles")" for role in Admin Manager Technician 'Read Only'; do [[ "${roles_response}" == *"\"name\":\"${role}\""* ]] done users_response="$(curl --fail --silent -b "${admin_cookie}" "${base_url}/api/admin/users")" for username in admin manager technician readonly; do [[ "${users_response}" == *"\"username\":\"${username}\""* ]] done echo "Logging in each role and checking authorization boundaries..." for login in \ "manager|${manager_cookie}|Manager" \ "technician|${technician_cookie}|Technician" \ "readonly|${readonly_cookie}|Read Only"; do IFS='|' read -r username cookie_jar role <<<"${login}" login_response="$(curl --fail --silent -c "${cookie_jar}" \ -H 'Content-Type: application/json' \ --data "{\"username\":\"${username}\",\"password\":\"MinerOpsTest!123\"}" \ "${base_url}/api/auth/login")" [[ "${login_response}" == *"\"role\":\"${role}\""* ]] [[ "$(status_for "${cookie_jar}" GET /dashboard)" == "200" ]] done [[ "$(status_for "${manager_cookie}" GET /system-health)" == "200" ]] [[ "$(status_for "${manager_cookie}" GET /accounts)" == "403" ]] [[ "$(status_for "${manager_cookie}" GET /api/admin/users)" == "403" ]] [[ "$(status_for "${technician_cookie}" GET /repairs)" == "200" ]] [[ "$(status_for "${technician_cookie}" GET /system-health)" == "403" ]] [[ "$(status_for "${readonly_cookie}" GET /locations)" == "200" ]] [[ "$(status_for "${readonly_cookie}" POST /api/rooms '{"name":"Must Not Be Created"}')" == "403" ]] room_status="$(status_for "${technician_cookie}" POST /api/rooms '{"name":"Role Matrix Test Room"}')" [[ "${room_status}" == "200" || "${room_status}" == "201" ]] me_response="$(curl --fail --silent -b "${readonly_cookie}" "${base_url}/api/auth/me")" [[ "${me_response}" == *'"role":"Read Only"'* ]] [[ "$(status_for "${readonly_cookie}" POST /api/auth/logout)" == "200" ]] [[ "$(status_for "${readonly_cookie}" GET /dashboard)" == "303" ]] echo "Checking development safety interlocks..." "${compose[@]}" exec -T backend sh -ec ' test "$SCHEDULED_SCAN_ENABLED" = "false" test "$MINEROPS_HUB_MODE" = "true" test "$MINEROPS_DISABLE_LOCAL_SCANNER" = "true" test "$MINEROPS_ENABLE_REAL_REBOOT" = "false" test "$MINEROPS_ENABLE_REAL_POWER_CONTROL" = "false" ' echo "Running retention rollups against the disposable baseline..." "${compose[@]}" exec -T postgres sh -c \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ < sql/minerops-retention-hourly.sql >/dev/null "${compose[@]}" exec -T postgres sh -c \ 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ < sql/minerops-retention-daily.sql >/dev/null echo "Development identity baseline passed."