#!/usr/bin/env bash set -euo pipefail BASE_URL="${BASE_URL:-http://127.0.0.1:3000}" DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-org-settings}" ADMIN_IDENTIFIER="${ADMIN_IDENTIFIER:-admin@hyinda.com}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-AdminPassword1234}" EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}" EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}" ADMINISTRATION_IDENTIFIER="${ADMINISTRATION_IDENTIFIER:-administration@hyinda.com}" ADMINISTRATION_PASSWORD="${ADMINISTRATION_PASSWORD:-AdministrationPassword1234}" 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)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT extract_json_field() { local file_path="$1" local jq_expr="$2" jq -er "$jq_expr" "$file_path" } assert_status() { local actual="$1" local expected="$2" local name="$3" if [[ "$actual" != "$expected" ]]; then echo "[ERROR] $name expected HTTP $expected but got $actual" >&2 exit 1 fi } assert_error_code() { local file_path="$1" local expected_code="$2" local name="$3" local actual_code actual_code="$(jq -r '.error.code // empty' "$file_path")" if [[ "$actual_code" != "$expected_code" ]]; then echo "[ERROR] $name expected error code $expected_code but got $actual_code" >&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 jq assertion failed: $jq_expr" >&2 cat "$file_path" >&2 exit 1 fi } login() { local identifier="$1" local password="$2" local fingerprint="$3" local out_body="$4" local status status="$(curl -sS -o "$out_body" -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" extract_json_field "$out_body" '.data.accessToken' } echo "[1/12] Login admin/employee/administration" admin_login_body="$TMP_DIR/admin-login.json" ADMIN_TOKEN="$(login "$ADMIN_IDENTIFIER" "$ADMIN_PASSWORD" "${DEVICE_FINGERPRINT}-admin" "$admin_login_body")" admin_me_body="$TMP_DIR/admin-me.json" admin_me_status="$(curl -sS -o "$admin_me_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/me" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$admin_me_status" "200" "admin me" ADMIN_ID="$(extract_json_field "$admin_me_body" '.data.id')" employee_login_body="$TMP_DIR/employee-login.json" EMPLOYEE_TOKEN="$(login "$EMPLOYEE_IDENTIFIER" "$EMPLOYEE_PASSWORD" "${DEVICE_FINGERPRINT}-employee" "$employee_login_body")" employee_me_body="$TMP_DIR/employee-me.json" employee_me_status="$(curl -sS -o "$employee_me_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/me" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$employee_me_status" "200" "employee me" EMPLOYEE_ID="$(extract_json_field "$employee_me_body" '.data.id')" administration_login_body="$TMP_DIR/administration-login.json" ADMINISTRATION_TOKEN="$(login "$ADMINISTRATION_IDENTIFIER" "$ADMINISTRATION_PASSWORD" "${DEVICE_FINGERPRINT}-administration" "$administration_login_body")" administration_me_body="$TMP_DIR/administration-me.json" administration_me_status="$(curl -sS -o "$administration_me_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/me" \ -H "authorization: Bearer $ADMINISTRATION_TOKEN")" assert_status "$administration_me_status" "200" "administration me" ADMINISTRATION_ID="$(extract_json_field "$administration_me_body" '.data.id')" echo "[2/12] Expense types (read-only)" expense_list_body="$TMP_DIR/expense-types.json" expense_list_status="$(curl -sS -o "$expense_list_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/expense-types?enabled=true" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$expense_list_status" "200" "expense types list" assert_jq "$expense_list_body" '.data.items | length > 0' "expense types non-empty" assert_jq "$expense_list_body" '([.data.items[].sortOrder] as $a | ($a|length) as $n | if $n <= 1 then true else [range(1; $n) | $a[.-1] <= $a[.]] | all end)' "expense types sorted" echo "[3/12] Expense types (management + audit)" expense_code="TEST_$(date +%s)_$RANDOM" expense_create_body="$TMP_DIR/expense-create.json" expense_create_status="$(curl -sS -o "$expense_create_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/expense-types" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"code\":\"$expense_code\",\"name\":\"$expense_code\",\"category\":\"other\",\"sortOrder\":9999,\"enabled\":true}")" assert_status "$expense_create_status" "200" "expense type create" EXPENSE_TYPE_ID="$(extract_json_field "$expense_create_body" '.data.id')" expense_patch_body="$TMP_DIR/expense-patch.json" expense_patch_status="$(curl -sS -o "$expense_patch_body" -w "%{http_code}" \ -X PATCH "$BASE_URL/api/expense-types/$EXPENSE_TYPE_ID" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d '{"enabled":false}')" assert_status "$expense_patch_status" "200" "expense type disable" expense_enabled_body="$TMP_DIR/expense-enabled.json" expense_enabled_status="$(curl -sS -o "$expense_enabled_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/expense-types?enabled=true" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$expense_enabled_status" "200" "expense types enabled list" assert_jq "$expense_enabled_body" "([.data.items[].id] | index(\"$EXPENSE_TYPE_ID\") == null)" "disabled expense type filtered" audit_expense_body="$TMP_DIR/audit-expense.json" audit_expense_status="$(curl -sS -o "$audit_expense_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/audit-logs?action=EXPENSE_TYPE_CREATED&targetType=expense_type&pageSize=50" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$audit_expense_status" "200" "audit expense type created" assert_jq "$audit_expense_body" "([.data.items[].targetId] | index(\"$EXPENSE_TYPE_ID\") != null)" "audit contains created expense type" echo "[4/12] Config read: approval rules (login required)" approval_rules_body="$TMP_DIR/approval-rules.json" approval_rules_status="$(curl -sS -o "$approval_rules_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/approval-rules?enabled=true" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$approval_rules_status" "200" "approval rules list" assert_jq "$approval_rules_body" '.data.items | length > 0' "approval rules non-empty" echo "[5/12] Config read: document sequence (system.settings)" sequence_body="$TMP_DIR/document-sequence.json" sequence_status="$(curl -sS -o "$sequence_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/document-sequences/reimbursement" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$sequence_status" "200" "document sequence read" assert_jq "$sequence_body" '.data.documentType == "reimbursement"' "document sequence type" echo "[6/12] Departments (list)" departments_list_body="$TMP_DIR/departments-list.json" departments_list_status="$(curl -sS -o "$departments_list_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/departments" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$departments_list_status" "200" "departments list" assert_jq "$departments_list_body" '(.data.items | type) == "array"' "departments items array" assert_jq "$departments_list_body" '(.data.tree | type) == "array"' "departments tree array" echo "[7/12] Departments (archive constraints)" dept_parent_name="TEST_DEPT_PARENT_$(date +%s)_$RANDOM" dept_child_name="TEST_DEPT_CHILD_$(date +%s)_$RANDOM" dept_leaf_name="TEST_DEPT_LEAF_$(date +%s)_$RANDOM" dept_parent_body="$TMP_DIR/dept-parent.json" dept_parent_status="$(curl -sS -o "$dept_parent_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"name\":\"$dept_parent_name\"}")" assert_status "$dept_parent_status" "200" "department create parent" DEPT_PARENT_ID="$(extract_json_field "$dept_parent_body" '.data.department.id')" dept_child_body="$TMP_DIR/dept-child.json" dept_child_status="$(curl -sS -o "$dept_child_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"name\":\"$dept_child_name\",\"parentId\":\"$DEPT_PARENT_ID\"}")" assert_status "$dept_child_status" "200" "department create child" DEPT_CHILD_ID="$(extract_json_field "$dept_child_body" '.data.department.id')" dept_archive_parent_body="$TMP_DIR/dept-archive-parent.json" dept_archive_parent_status="$(curl -sS -o "$dept_archive_parent_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments/$DEPT_PARENT_ID/archive" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$dept_archive_parent_status" "409" "department archive parent blocked" assert_error_code "$dept_archive_parent_body" "VALID_001" "department archive parent blocked" assert_jq "$dept_archive_parent_body" '.error.details.department == "has_children"' "department archive parent has_children" dept_archive_child_body="$TMP_DIR/dept-archive-child.json" dept_archive_child_status="$(curl -sS -o "$dept_archive_child_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments/$DEPT_CHILD_ID/archive" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$dept_archive_child_status" "200" "department archive child" dept_leaf_body="$TMP_DIR/dept-leaf.json" dept_leaf_status="$(curl -sS -o "$dept_leaf_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"name\":\"$dept_leaf_name\"}")" assert_status "$dept_leaf_status" "200" "department create leaf" DEPT_LEAF_ID="$(extract_json_field "$dept_leaf_body" '.data.department.id')" dept_archive_leaf_body="$TMP_DIR/dept-archive-leaf.json" dept_archive_leaf_status="$(curl -sS -o "$dept_archive_leaf_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments/$DEPT_LEAF_ID/archive" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$dept_archive_leaf_status" "200" "department archive leaf" echo "[8/12] Employees alias (/users vs /employees)" users_list_body="$TMP_DIR/users-list.json" users_list_status="$(curl -sS -o "$users_list_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/users?page=1&pageSize=5" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$users_list_status" "200" "users list" employees_list_body="$TMP_DIR/employees-list.json" employees_list_status="$(curl -sS -o "$employees_list_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/employees?page=1&pageSize=5" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$employees_list_status" "200" "employees list" assert_jq "$employees_list_body" '(.data.total | type) == "number"' "employees list total" assert_jq "$employees_list_body" "(.data.total == $(jq -r '.data.total' "$users_list_body"))" "employees list total matches users" echo "[9/12] Employee can read self but not others" employee_self_body="$TMP_DIR/employee-self.json" employee_self_status="$(curl -sS -o "$employee_self_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/employees/$EMPLOYEE_ID" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$employee_self_status" "200" "employee get self" employee_other_body="$TMP_DIR/employee-other.json" employee_other_status="$(curl -sS -o "$employee_other_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/employees/$ADMIN_ID" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$employee_other_status" "403" "employee get other forbidden" assert_error_code "$employee_other_body" "PERM_001" "employee get other forbidden" echo "[10/12] Department archive blocks active employees" dept_emp_name="TEST_DEPT_EMP_$(date +%s)_$RANDOM" dept_emp_body="$TMP_DIR/dept-emp.json" dept_emp_status="$(curl -sS -o "$dept_emp_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"name\":\"$dept_emp_name\"}")" assert_status "$dept_emp_status" "200" "department create for employee" DEPT_EMP_ID="$(extract_json_field "$dept_emp_body" '.data.department.id')" new_employee_no="T$(date +%s)$RANDOM" new_employee_email="test_${new_employee_no}@hyinda.com" new_employee_phone="139${RANDOM}${RANDOM}" employee_create_body="$TMP_DIR/employee-create.json" employee_create_status="$(curl -sS -o "$employee_create_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/users" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"employeeNo\":\"$new_employee_no\",\"name\":\"Test User\",\"phone\":\"$new_employee_phone\",\"email\":\"$new_employee_email\",\"departmentId\":\"$DEPT_EMP_ID\",\"title\":\"测试\",\"directManagerId\":null,\"hireDate\":\"2020-01-01\",\"roles\":[\"employee\"],\"password\":\"TestPassword1234\"}")" assert_status "$employee_create_status" "200" "employee create" NEW_EMPLOYEE_ID="$(extract_json_field "$employee_create_body" '.data.id')" dept_archive_blocked_body="$TMP_DIR/dept-archive-blocked.json" dept_archive_blocked_status="$(curl -sS -o "$dept_archive_blocked_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments/$DEPT_EMP_ID/archive" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$dept_archive_blocked_status" "409" "department archive blocked by active employees" assert_error_code "$dept_archive_blocked_body" "VALID_001" "department archive blocked by active employees" assert_jq "$dept_archive_blocked_body" '.error.details.department == "has_active_employees"' "department archive has_active_employees" user_disable_body="$TMP_DIR/user-disable.json" user_disable_status="$(curl -sS -o "$user_disable_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/users/$NEW_EMPLOYEE_ID/disable" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$user_disable_status" "200" "user disable" dept_archive_after_disable_body="$TMP_DIR/dept-archive-after-disable.json" dept_archive_after_disable_status="$(curl -sS -o "$dept_archive_after_disable_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/departments/$DEPT_EMP_ID/archive" \ -H "authorization: Bearer $ADMIN_TOKEN")" assert_status "$dept_archive_after_disable_status" "200" "department archive after disable" echo "[11/12] Delegation grants: admin maintain principal + alias consistency" delegation_create_body="$TMP_DIR/delegation-create.json" delegation_create_status="$(curl -sS -o "$delegation_create_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/delegation-grants" \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d "{\"principalEmployeeId\":\"$EMPLOYEE_ID\",\"delegateEmployeeId\":\"$ADMINISTRATION_ID\",\"scope\":\"reimbursement_submit\"}")" assert_status "$delegation_create_status" "200" "delegation grant create" DELEGATION_ID="$(extract_json_field "$delegation_create_body" '.data.delegation.id')" assert_jq "$delegation_create_body" ".data.delegation.principal.id == \"$EMPLOYEE_ID\"" "delegation principal embedded" assert_jq "$delegation_create_body" ".data.delegation.delegate.id == \"$ADMINISTRATION_ID\"" "delegation delegate embedded" employee_delegations_body="$TMP_DIR/delegations-mine.json" employee_delegations_status="$(curl -sS -o "$employee_delegations_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/delegations/mine?status=active" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$employee_delegations_status" "200" "delegations mine" assert_jq "$employee_delegations_body" "([.data.initiated[].id] | index(\"$DELEGATION_ID\") != null)" "delegations mine contains created" delegation_list_body="$TMP_DIR/delegation-grants-list.json" delegation_list_status="$(curl -sS -o "$delegation_list_body" -w "%{http_code}" \ -X GET "$BASE_URL/api/delegation-grants?includeInactive=1" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$delegation_list_status" "200" "delegation grants list (employee view)" assert_jq "$delegation_list_body" "([.data.items[].id] | index(\"$DELEGATION_ID\") != null)" "delegation grants list contains created" delegation_forbidden_body="$TMP_DIR/delegation-forbidden.json" delegation_forbidden_status="$(curl -sS -o "$delegation_forbidden_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/delegation-grants" \ -H "authorization: Bearer $EMPLOYEE_TOKEN" \ -H "content-type: application/json" \ -d "{\"principalEmployeeId\":\"$ADMIN_ID\",\"delegateEmployeeId\":\"$ADMINISTRATION_ID\",\"scope\":\"reimbursement_submit\"}")" assert_status "$delegation_forbidden_status" "403" "delegation grant forbidden" assert_error_code "$delegation_forbidden_body" "PERM_001" "delegation grant forbidden" echo "[12/12] Revoke delegation (cleanup)" delegation_revoke_body="$TMP_DIR/delegation-revoke.json" delegation_revoke_status="$(curl -sS -o "$delegation_revoke_body" -w "%{http_code}" \ -X POST "$BASE_URL/api/delegation-grants/$DELEGATION_ID/revoke" \ -H "authorization: Bearer $EMPLOYEE_TOKEN")" assert_status "$delegation_revoke_status" "200" "delegation revoke" echo "[OK] Org/Settings regression passed"