文件
hyb-oa/server/scripts/regression-org-settings.sh
wangsli f755ff0dda feat(server): P5 基础能力补齐(组织/报销/代办/认证安全)
- 组织/人员/委派对齐:费用类型、部门、员工别名、配置只读与回归脚本(F2/F3/F26/F27/F29/F31/F40)

- 报销单 PATCH/DELETE 与 items CRUD(事务内重算 total_amount)(F4/F5)

- 行政代提交/代上传后端校验与审计(F7/F8)

- 认证安全基线:首次登录改密、失败锁定、弱密码拦截与回归脚本(F36/F37/F39/F41)
2026-02-09 16:20:31 +08:00

347 行
17 KiB
Bash
可执行文件

#!/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"