#!/usr/bin/env bash set -euo pipefail BASE_URL="${BASE_URL:-http://127.0.0.1:3000}" DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-audit}" EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}" EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}" MANAGER_IDENTIFIER="${MANAGER_IDENTIFIER:-manager@hyinda.com}" MANAGER_PASSWORD="${MANAGER_PASSWORD:-ManagerPassword1234}" FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}" FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}" CEO_IDENTIFIER="${CEO_IDENTIFIER:-ceo@hyinda.com}" if ! command -v curl >/dev/null 2>&1; then echo "[ERROR] curl not found" >&2 exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "[ERROR] jq not found" >&2 exit 1 fi TMP_DIR="$(mktemp -d)" RUN_SUFFIX="$(date +%s)-$RANDOM" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT assert_status() { local actual="$1" local expected="$2" local name="$3" local body_file="${4:-}" if [[ "$actual" != "$expected" ]]; then echo "[ERROR] $name expected HTTP $expected but got $actual" >&2 if [[ -n "$body_file" && -f "$body_file" ]]; then cat "$body_file" >&2 fi exit 1 fi } assert_error_code() { local file_path="$1" local expected="$2" local name="$3" local actual actual="$(jq -r '.error.code // empty' "$file_path")" if [[ "$actual" != "$expected" ]]; then echo "[ERROR] $name expected error code $expected but got $actual" >&2 cat "$file_path" >&2 exit 1 fi } assert_jq() { local file_path="$1" local jq_expr="$2" local name="$3" if ! jq -e "$jq_expr" "$file_path" >/dev/null; then echo "[ERROR] $name assertion failed: $jq_expr" >&2 cat "$file_path" >&2 exit 1 fi } api_json_auth() { local method="$1" local url="$2" local token="$3" local out_file="$4" local body="${5:-}" if [[ -n "$body" ]]; then curl -sS -o "$out_file" -w "%{http_code}" \ -X "$method" "$url" \ -H "authorization: Bearer $token" \ -H "content-type: application/json" \ -d "$body" else curl -sS -o "$out_file" -w "%{http_code}" \ -X "$method" "$url" \ -H "authorization: Bearer $token" fi } login_user() { local identifier="$1" local password="$2" local fingerprint="$3" local out_file="$4" local status status="$(curl -sS -o "$out_file" -w "%{http_code}" \ -X POST "$BASE_URL/api/auth/login" \ -H "content-type: application/json" \ -H "x-device-fingerprint: $fingerprint" \ -d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")" assert_status "$status" "200" "login $identifier" "$out_file" jq -er '.data.accessToken' "$out_file" } auth_get() { local url="$1" local token="$2" local out_file="$3" curl -sS -o "$out_file" -w "%{http_code}" \ -X GET "$url" \ -H "authorization: Bearer $token" } get_me_id() { local token="$1" local out_file="$2" local status status="$(auth_get "$BASE_URL/api/me" "$token" "$out_file")" assert_status "$status" "200" "get me" "$out_file" jq -er '.data.id' "$out_file" } create_delegation() { local token="$1" local delegate_id="$2" local valid_from="$3" local valid_to="$4" local out_file="$5" local status status="$(api_json_auth POST "$BASE_URL/api/delegations" "$token" "$out_file" "{\"delegateEmployeeId\":\"$delegate_id\",\"scope\":\"all\",\"validFrom\":\"$valid_from\",\"validTo\":\"$valid_to\"}")" assert_status "$status" "200" "create delegation" "$out_file" jq -er '.data.delegation.id' "$out_file" } revoke_delegation() { local token="$1" local delegation_id="$2" local out_file="$3" local status status="$(api_json_auth POST "$BASE_URL/api/delegations/$delegation_id/revoke" "$token" "$out_file" '{}')" assert_status "$status" "200" "revoke delegation" "$out_file" } echo "[1/6] login users" EMPLOYEE_TOKEN="$(login_user "$EMPLOYEE_IDENTIFIER" "$EMPLOYEE_PASSWORD" "$DEVICE_FINGERPRINT-employee-$RUN_SUFFIX" "$TMP_DIR/login-employee.json")" MANAGER_TOKEN="$(login_user "$MANAGER_IDENTIFIER" "$MANAGER_PASSWORD" "$DEVICE_FINGERPRINT-manager-$RUN_SUFFIX" "$TMP_DIR/login-manager.json")" FINANCE_MGR_TOKEN="$(login_user "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$DEVICE_FINGERPRINT-finance-mgr-$RUN_SUFFIX" "$TMP_DIR/login-finance-mgr.json")" CEO_TOKEN="$(login_user "$CEO_IDENTIFIER" "${CEO_PASSWORD:-CeoPassword1234}" "$DEVICE_FINGERPRINT-ceo-$RUN_SUFFIX" "$TMP_DIR/login-ceo.json")" echo "[2/6] permission check: non-audit role forbidden" perm_status="$(auth_get "$BASE_URL/api/audit-logs?page=1&pageSize=5" "$EMPLOYEE_TOKEN" "$TMP_DIR/audit-perm.json")" assert_status "$perm_status" "403" "audit permission" "$TMP_DIR/audit-perm.json" assert_error_code "$TMP_DIR/audit-perm.json" "PERM_001" "audit permission" echo "[3/6] create deterministic audit records" CEO_ID="$(get_me_id "$CEO_TOKEN" "$TMP_DIR/me-ceo.json")" VALID_FROM="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" VALID_TO="$(date -u -d "+1 day" +"%Y-%m-%dT%H:%M:%S.000Z")" DELEGATION_ID="$(create_delegation "$MANAGER_TOKEN" "$CEO_ID" "$VALID_FROM" "$VALID_TO" "$TMP_DIR/delegation-create.json")" revoke_delegation "$MANAGER_TOKEN" "$DELEGATION_ID" "$TMP_DIR/delegation-revoke.json" echo "[4/6] pagination and base response" page_status="$(auth_get "$BASE_URL/api/audit-logs?page=1&pageSize=3" "$FINANCE_MGR_TOKEN" "$TMP_DIR/audit-page.json")" assert_status "$page_status" "200" "audit list page" "$TMP_DIR/audit-page.json" assert_jq "$TMP_DIR/audit-page.json" '.data.page == 1 and .data.pageSize == 3 and (.data.total|type=="number") and (.data.items|type=="array")' "audit list page" echo "[5/6] action/targetType/date filters" filter_status="$(auth_get "$BASE_URL/api/audit-logs?action=DELEGATION_CREATE&targetType=delegation_grant&page=1&pageSize=20" "$FINANCE_MGR_TOKEN" "$TMP_DIR/audit-filter-action.json")" assert_status "$filter_status" "200" "audit filter action" "$TMP_DIR/audit-filter-action.json" assert_jq "$TMP_DIR/audit-filter-action.json" '.data.items | any(.action == "DELEGATION_CREATE" and .targetType == "delegation_grant")' "audit filter action" range_status="$(auth_get "$BASE_URL/api/audit-logs?dateFrom=2026-01-01T00:00:00.000Z&dateTo=2026-12-31T23:59:59.000Z&page=1&pageSize=10" "$FINANCE_MGR_TOKEN" "$TMP_DIR/audit-filter-date.json")" assert_status "$range_status" "200" "audit filter date" "$TMP_DIR/audit-filter-date.json" assert_jq "$TMP_DIR/audit-filter-date.json" '.data.items|type=="array"' "audit filter date" echo "[6/6] invalid query check" invalid_status="$(auth_get "$BASE_URL/api/audit-logs?page=0&pageSize=20" "$FINANCE_MGR_TOKEN" "$TMP_DIR/audit-invalid-page.json")" assert_status "$invalid_status" "400" "audit invalid page" "$TMP_DIR/audit-invalid-page.json" assert_error_code "$TMP_DIR/audit-invalid-page.json" "VALID_001" "audit invalid page" date_invalid_status="$(auth_get "$BASE_URL/api/audit-logs?dateFrom=2026-02-10T00:00:00.000Z&dateTo=2026-02-01T00:00:00.000Z" "$FINANCE_MGR_TOKEN" "$TMP_DIR/audit-invalid-date.json")" assert_status "$date_invalid_status" "400" "audit invalid date" "$TMP_DIR/audit-invalid-date.json" assert_error_code "$TMP_DIR/audit-invalid-date.json" "VALID_001" "audit invalid date" assert_jq "$TMP_DIR/audit-invalid-date.json" '.error.details.dateTo == "must_be_after_dateFrom"' "audit invalid date" echo "[OK] Audit regression passed"