226 lines
8.4 KiB
Bash
Executable File
226 lines
8.4 KiB
Bash
Executable File
#!/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
|
|
# Read and update the jar so logout and refreshed cookies behave like a browser.
|
|
args+=(-b "${cookie_jar}" -c "${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}" == *"0010_begin_repair"* ]]
|
|
|
|
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" ]]
|
|
|
|
echo "Checking Begin Repair permission and workflow..."
|
|
"${compose[@]}" exec -T postgres sh -ec \
|
|
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
|
|
INSERT INTO miners (
|
|
id,
|
|
asset_tag,
|
|
status,
|
|
lifecycle_status,
|
|
first_seen_at,
|
|
last_seen_at
|
|
)
|
|
VALUES (
|
|
'00000000-0000-0000-0000-000000000981',
|
|
'SMOKE-REPAIR-001',
|
|
'unknown',
|
|
'active',
|
|
NULL,
|
|
NULL
|
|
);
|
|
SQL
|
|
[[ "$(
|
|
status_for "${readonly_cookie}" POST /api/repairs \
|
|
'{"miner_id":"00000000-0000-0000-0000-000000000981","reason":"CONTROL_BOARD"}'
|
|
)" == "403" ]]
|
|
begin_repair_response="$(curl --fail --silent -b "${technician_cookie}" \
|
|
-H 'Content-Type: application/json' \
|
|
--data '{"miner_id":"00000000-0000-0000-0000-000000000981","reason":"CONTROL_BOARD","notes":"Smoke test repair."}' \
|
|
"${base_url}/api/repairs")"
|
|
[[ "${begin_repair_response}" == *'"status":"OPEN"'* ]]
|
|
[[ "${begin_repair_response}" == *'"reason":"CONTROL_BOARD"'* ]]
|
|
[[ "$(
|
|
status_for "${technician_cookie}" POST /api/repairs \
|
|
'{"miner_id":"00000000-0000-0000-0000-000000000981","reason":"CONTROL_BOARD"}'
|
|
)" == "409" ]]
|
|
|
|
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."
|