- 组织/人员/委派对齐:费用类型、部门、员工别名、配置只读与回归脚本(F2/F3/F26/F27/F29/F31/F40) - 报销单 PATCH/DELETE 与 items CRUD(事务内重算 total_amount)(F4/F5) - 行政代提交/代上传后端校验与审计(F7/F8) - 认证安全基线:首次登录改密、失败锁定、弱密码拦截与回归脚本(F36/F37/F39/F41)
347 行
17 KiB
Bash
可执行文件
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"
|