209 lines
8.7 KiB
Bash
Executable File
209 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "${repo_root}"
|
|
project_name="${MINEROPS_REPAIR_PARTS_MIGRATION_PROJECT:-minerops-repair-parts-migration-$$}"
|
|
compose=(docker compose --project-name "${project_name}" --env-file .env.dev -f compose.dev.yaml)
|
|
|
|
cleanup() {
|
|
status=$?
|
|
if (( status != 0 )); then
|
|
"${compose[@]}" logs --no-color --tail 120 postgres || true
|
|
fi
|
|
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
|
|
exit "${status}"
|
|
}
|
|
trap cleanup EXIT
|
|
run_alembic() { "${compose[@]}" run --rm --no-deps backend alembic "$@"; }
|
|
psql_db() {
|
|
"${compose[@]}" exec -T postgres sh -ec \
|
|
'exec psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" "$@"' sh "$@"
|
|
}
|
|
|
|
[[ -f .env.dev ]] || { echo "Missing .env.dev" >&2; exit 1; }
|
|
"${compose[@]}" up --detach postgres
|
|
for attempt in $(seq 1 60); do
|
|
"${compose[@]}" exec -T postgres sh -ec \
|
|
'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' >/dev/null 2>&1 && break
|
|
(( attempt < 60 )) || { echo "PostgreSQL did not become ready" >&2; exit 1; }
|
|
sleep 1
|
|
done
|
|
"${compose[@]}" build backend
|
|
|
|
echo "Upgrading from Task 004 parent..."
|
|
run_alembic upgrade 0013_control_board_replacement
|
|
[[ "$(run_alembic current 2>/dev/null)" == *"0013_control_board_replacement"* ]]
|
|
run_alembic upgrade 0014_repair_parts_tracking
|
|
[[ "$(run_alembic current 2>/dev/null)" == *"0014_repair_parts_tracking"* ]]
|
|
|
|
psql_db <<'SQL'
|
|
DO $$
|
|
DECLARE component_roles text[];
|
|
DECLARE create_roles text[];
|
|
DECLARE manage_roles text[];
|
|
BEGIN
|
|
IF to_regclass('public.components') IS NULL OR to_regclass('public.repair_parts') IS NULL THEN
|
|
RAISE EXCEPTION 'Task 005 tables are missing';
|
|
END IF;
|
|
SELECT array_agg(r.name ORDER BY r.name) INTO component_roles
|
|
FROM role_permissions rp JOIN roles r ON r.id = rp.role_id
|
|
JOIN permissions p ON p.id = rp.permission_id WHERE p.key = 'components.read';
|
|
SELECT array_agg(r.name ORDER BY r.name) INTO create_roles
|
|
FROM role_permissions rp JOIN roles r ON r.id = rp.role_id
|
|
JOIN permissions p ON p.id = rp.permission_id WHERE p.key = 'components.create';
|
|
SELECT array_agg(r.name ORDER BY r.name) INTO manage_roles
|
|
FROM role_permissions rp JOIN roles r ON r.id = rp.role_id
|
|
JOIN permissions p ON p.id = rp.permission_id WHERE p.key = 'repairs.parts.manage';
|
|
IF component_roles <> ARRAY['Admin', 'Manager', 'Read Only', 'Technician'] THEN
|
|
RAISE EXCEPTION 'Unexpected components.read roles: %', component_roles;
|
|
END IF;
|
|
IF create_roles <> ARRAY['Admin', 'Manager', 'Technician']
|
|
OR manage_roles <> ARRAY['Admin', 'Manager', 'Technician'] THEN
|
|
RAISE EXCEPTION 'Unexpected Task 005 write roles: %, %', create_roles, manage_roles;
|
|
END IF;
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'ck_repair_parts_entry_xor'
|
|
) OR NOT EXISTS (
|
|
SELECT 1 FROM pg_trigger WHERE tgname = 'trg_validate_repair_part_history'
|
|
) THEN
|
|
RAISE EXCEPTION 'Task 005 constraints or triggers are missing';
|
|
END IF;
|
|
END $$;
|
|
SQL
|
|
|
|
echo "Testing safe downgrade and re-upgrade..."
|
|
run_alembic downgrade 0013_control_board_replacement
|
|
[[ "$(run_alembic current 2>/dev/null)" == *"0013_control_board_replacement"* ]]
|
|
absent="$(psql_db -Atc "SELECT to_regclass('public.components') IS NULL AND to_regclass('public.repair_parts') IS NULL")"
|
|
[[ "${absent}" == "t" ]]
|
|
run_alembic upgrade 0014_repair_parts_tracking
|
|
|
|
echo "Testing registry, XOR, snapshots, voids, and immutability..."
|
|
psql_db <<'SQL'
|
|
INSERT INTO users (id, username, display_name, password_hash, role_id)
|
|
SELECT '00000000-0000-0000-0000-000000001501', 'repair-parts-migration',
|
|
'Repair Parts Migration', 'unused', id FROM roles WHERE name = 'Admin';
|
|
INSERT INTO miners (
|
|
id, asset_tag, lifecycle_status, status, first_seen_at, last_seen_at
|
|
) VALUES (
|
|
'00000000-0000-0000-0000-000000001502', 'PARTS-MINER',
|
|
'in_repair', 'unknown', NULL, NULL
|
|
);
|
|
INSERT INTO repairs (id, miner_id, status, reason, opened_by)
|
|
VALUES (
|
|
'00000000-0000-0000-0000-000000001503',
|
|
'00000000-0000-0000-0000-000000001502', 'OPEN', 'OTHER',
|
|
'00000000-0000-0000-0000-000000001501'
|
|
);
|
|
INSERT INTO repair_events (id, repair_id, sequence, event_type, created_by, payload)
|
|
VALUES (
|
|
'00000000-0000-0000-0000-000000001504',
|
|
'00000000-0000-0000-0000-000000001503', 1, 'REPAIR_STARTED',
|
|
'00000000-0000-0000-0000-000000001501',
|
|
'{"reason":"OTHER","from_lifecycle":"active","to_lifecycle":"in_repair"}'
|
|
);
|
|
INSERT INTO components (
|
|
id, component_type, asset_id, manufacturer, manufacturer_serial, created_by
|
|
) VALUES (
|
|
'00000000-0000-4000-8000-000000001505', 'hashboard', 'HB-005',
|
|
'MicroBT', 'SER-005', '00000000-0000-0000-0000-000000001501'
|
|
);
|
|
INSERT INTO repair_events (id, repair_id, sequence, event_type, created_by, payload)
|
|
VALUES (
|
|
'00000000-0000-0000-0000-000000001506',
|
|
'00000000-0000-0000-0000-000000001503', 2, 'REPAIR_PART_RECORDED',
|
|
'00000000-0000-0000-0000-000000001501',
|
|
'{"repair_part_id":"00000000-0000-4000-8000-000000001507","entry_kind":"tracked_component","component_id":"00000000-0000-4000-8000-000000001505","component_type":"hashboard","component_asset_id":"HB-005","component_manufacturer":"MicroBT","component_model":null,"component_manufacturer_serial":"SER-005","notes":null}'
|
|
);
|
|
INSERT INTO repair_parts (
|
|
id, repair_id, component_id, component_type_snapshot,
|
|
component_asset_id_snapshot, component_manufacturer_snapshot,
|
|
component_model_snapshot, component_manufacturer_serial_snapshot,
|
|
recorded_by, recorded_at, recorded_event_id
|
|
) SELECT
|
|
'00000000-0000-4000-8000-000000001507',
|
|
'00000000-0000-0000-0000-000000001503',
|
|
'00000000-0000-4000-8000-000000001505', 'hashboard', 'HB-005',
|
|
'MicroBT', NULL, 'SER-005', '00000000-0000-0000-0000-000000001501',
|
|
created_at, id FROM repair_events
|
|
WHERE id = '00000000-0000-0000-0000-000000001506';
|
|
|
|
UPDATE components SET manufacturer = 'Updated Manufacturer'
|
|
WHERE id = '00000000-0000-4000-8000-000000001505';
|
|
DO $$
|
|
BEGIN
|
|
IF (SELECT component_manufacturer_snapshot FROM repair_parts
|
|
WHERE id = '00000000-0000-4000-8000-000000001507') <> 'MicroBT' THEN
|
|
RAISE EXCEPTION 'Tracked snapshot changed with live metadata';
|
|
END IF;
|
|
BEGIN
|
|
INSERT INTO repair_parts (
|
|
repair_id, consumable_description, quantity, unit, component_id,
|
|
recorded_by, recorded_event_id
|
|
) VALUES (
|
|
'00000000-0000-0000-0000-000000001503', 'invalid', 1, 'each',
|
|
'00000000-0000-4000-8000-000000001505',
|
|
'00000000-0000-0000-0000-000000001501',
|
|
'00000000-0000-0000-0000-000000001506'
|
|
);
|
|
RAISE EXCEPTION 'XOR violation was accepted';
|
|
EXCEPTION WHEN check_violation OR raise_exception THEN NULL;
|
|
END;
|
|
END $$;
|
|
|
|
INSERT INTO repair_events (id, repair_id, sequence, event_type, created_by, payload)
|
|
VALUES (
|
|
'00000000-0000-0000-0000-000000001508',
|
|
'00000000-0000-0000-0000-000000001503', 3, 'REPAIR_PART_VOIDED',
|
|
'00000000-0000-0000-0000-000000001501',
|
|
'{"repair_part_id":"00000000-0000-4000-8000-000000001507","reason":"Wrong repair"}'
|
|
);
|
|
UPDATE repair_parts SET
|
|
voided_by = '00000000-0000-0000-0000-000000001501',
|
|
voided_at = (SELECT created_at FROM repair_events WHERE id = '00000000-0000-0000-0000-000000001508'),
|
|
void_reason = 'Wrong repair',
|
|
void_event_id = '00000000-0000-0000-0000-000000001508'
|
|
WHERE id = '00000000-0000-4000-8000-000000001507';
|
|
|
|
DO $$
|
|
BEGIN
|
|
BEGIN
|
|
UPDATE repair_parts SET void_reason = 'Changed'
|
|
WHERE id = '00000000-0000-4000-8000-000000001507';
|
|
RAISE EXCEPTION 'Second void mutation was accepted';
|
|
EXCEPTION WHEN raise_exception THEN NULL;
|
|
END;
|
|
BEGIN
|
|
DELETE FROM repair_parts WHERE id = '00000000-0000-4000-8000-000000001507';
|
|
RAISE EXCEPTION 'Repair-part deletion was accepted';
|
|
EXCEPTION WHEN raise_exception THEN NULL;
|
|
END;
|
|
BEGIN
|
|
DELETE FROM repair_events WHERE id = '00000000-0000-0000-0000-000000001508';
|
|
RAISE EXCEPTION 'Repair-event deletion was accepted';
|
|
EXCEPTION WHEN raise_exception THEN NULL;
|
|
END;
|
|
END $$;
|
|
SQL
|
|
|
|
if downgrade_output="$(run_alembic downgrade 0013_control_board_replacement 2>&1)"; then
|
|
echo "Task 005 downgrade unexpectedly destroyed history" >&2
|
|
exit 1
|
|
fi
|
|
[[ "${downgrade_output}" == *"Cannot downgrade Repair Parts Tracking"* ]]
|
|
[[ "$(run_alembic current 2>/dev/null)" == *"0014_repair_parts_tracking"* ]]
|
|
|
|
echo "Testing empty database through head..."
|
|
"${compose[@]}" exec -T postgres sh -ec \
|
|
'createdb -U "$POSTGRES_USER" minerops_task005_empty'
|
|
"${compose[@]}" run --rm --no-deps -e POSTGRES_DB=minerops_task005_empty \
|
|
backend alembic upgrade head
|
|
empty_revision="$(
|
|
"${compose[@]}" exec -T postgres sh -ec \
|
|
'psql -U "$POSTGRES_USER" -d minerops_task005_empty -Atc "SELECT version_num FROM alembic_version"'
|
|
)"
|
|
[[ "${empty_revision}" == "0014_repair_parts_tracking" ]]
|
|
|
|
echo "Repair Parts Tracking migration tests passed."
|