feat(server): 完成 P0+P1 后端能力(工作流、RBAC、付款归档)
P0 MVP: - 认证:login/refresh/logout/forgot/reset + tokenVersion - RBAC:角色到权限映射 + requirePermission - 发票池:upload/bind/signed-url + 判重(VALID_003/VALID_005) - 报销:create/view/submit + 单号生成 - 工作流:approve + department_manager 解析 P1 扩展: - 动作:reject/return/withdraw(含 comment) - transfer:任务转审 + 链路上限 3 - pay:approved -> paid + 发票归档 used_complete - 动态审批链:approval_rules 驱动 回归脚本: - smoke-test.sh / regression-dedup.sh / regression-workflow-actions.sh
这个提交包含在:
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "=== [1/3] Smoke Test ==="
|
||||
"$SCRIPT_DIR/smoke-test.sh"
|
||||
echo ""
|
||||
|
||||
echo "=== [2/3] Dedup Regression ==="
|
||||
"$SCRIPT_DIR/regression-dedup.sh"
|
||||
echo ""
|
||||
|
||||
echo "=== [3/3] Workflow Actions Regression ==="
|
||||
"$SCRIPT_DIR/regression-workflow-actions.sh"
|
||||
echo ""
|
||||
|
||||
echo "[OK] All regressions passed"
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
|
||||
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-dedup}"
|
||||
EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}"
|
||||
EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}"
|
||||
|
||||
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
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "[ERROR] node not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [[ -z "${DATABASE_URL:-}" && -f ".env" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [[ -z "${DATABASE_URL:-}" ]]; then
|
||||
echo "[ERROR] DATABASE_URL is required (export it or provide server/.env)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
TMP_INDEX_CREATED=0
|
||||
RUN_SUFFIX="$(date +%s)-$RANDOM"
|
||||
DEDUP_CODE="RCODE-$RUN_SUFFIX"
|
||||
DEDUP_NUMBER="RNO-$RUN_SUFFIX"
|
||||
|
||||
cleanup() {
|
||||
if [[ "$TMP_INDEX_CREATED" == "1" ]]; then
|
||||
drop_tmp_non_dedup_index || true
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
api_json() {
|
||||
local method="$1"
|
||||
local url="$2"
|
||||
local out_file="$3"
|
||||
local body="${4:-}"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
upload_file() {
|
||||
local file_path="$1"
|
||||
local out_file="$2"
|
||||
|
||||
curl -sS -o "$out_file" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/invoice-pool/upload" \
|
||||
-H "authorization: Bearer $TOKEN" \
|
||||
-F "file=@$file_path"
|
||||
}
|
||||
|
||||
create_reimbursement() {
|
||||
local out_file="$1"
|
||||
local status
|
||||
status="$(api_json POST "$BASE_URL/api/reimbursements" "$out_file" '{}')"
|
||||
assert_status "$status" "200" "create reimbursement" "$out_file"
|
||||
jq -er '.data.id' "$out_file"
|
||||
}
|
||||
|
||||
set_invoice_code_number() {
|
||||
local invoice_id="$1"
|
||||
local invoice_code="$2"
|
||||
local invoice_number="$3"
|
||||
|
||||
DATABASE_URL="$DATABASE_URL" node - "$invoice_id" "$invoice_code" "$invoice_number" <<'NODE'
|
||||
const invoiceId = process.argv[2];
|
||||
const invoiceCode = process.argv[3];
|
||||
const invoiceNumber = process.argv[4];
|
||||
const { createPrismaClient } = require('./src/prismaClient');
|
||||
|
||||
(async () => {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
try {
|
||||
await prisma.invoicePool.update({
|
||||
where: { id: invoiceId },
|
||||
data: {
|
||||
invoice_code: invoiceCode,
|
||||
invoice_number: invoiceNumber,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
create_tmp_non_dedup_index() {
|
||||
DATABASE_URL="$DATABASE_URL" node <<'NODE'
|
||||
const { createPrismaClient } = require('./src/prismaClient');
|
||||
|
||||
(async () => {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS tmp_unique_linked_reim ON "invoice_pool" ("linked_reimbursement_id") WHERE "linked_reimbursement_id" IS NOT NULL',
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
drop_tmp_non_dedup_index() {
|
||||
DATABASE_URL="$DATABASE_URL" node <<'NODE'
|
||||
const { createPrismaClient } = require('./src/prismaClient');
|
||||
|
||||
(async () => {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
try {
|
||||
await prisma.$executeRawUnsafe('DROP INDEX IF EXISTS "tmp_unique_linked_reim"');
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_error_code() {
|
||||
local file_path="$1"
|
||||
local expected="$2"
|
||||
local actual
|
||||
actual="$(jq -r '.error.code // empty' "$file_path")"
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "[ERROR] expected error code $expected but got $actual" >&2
|
||||
cat "$file_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[1/7] Login employee"
|
||||
login_code="$(curl -sS -o "$TMP_DIR/login.json" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/auth/login" \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
|
||||
-d "{\"identifier\":\"$EMPLOYEE_IDENTIFIER\",\"password\":\"$EMPLOYEE_PASSWORD\"}")"
|
||||
assert_status "$login_code" "200" "login" "$TMP_DIR/login.json"
|
||||
TOKEN="$(jq -er '.data.accessToken' "$TMP_DIR/login.json")"
|
||||
|
||||
echo "[2/7] Dedup by file_sha256 => VALID_003"
|
||||
cat > "$TMP_DIR/same-file.pdf" <<PDF
|
||||
%PDF-1.4
|
||||
same-content-$RUN_SUFFIX
|
||||
PDF
|
||||
|
||||
RID_A="$(create_reimbursement "$TMP_DIR/rA.json")"
|
||||
status="$(upload_file "$TMP_DIR/same-file.pdf" "$TMP_DIR/uA.json")"
|
||||
assert_status "$status" "200" "upload first same-file" "$TMP_DIR/uA.json"
|
||||
IID_A="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uA.json")"
|
||||
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_A/bind" "$TMP_DIR/bA.json" "{\"reimbursementId\":\"$RID_A\"}")"
|
||||
assert_status "$status" "200" "bind first same-file" "$TMP_DIR/bA.json"
|
||||
|
||||
RID_B="$(create_reimbursement "$TMP_DIR/rB.json")"
|
||||
status="$(upload_file "$TMP_DIR/same-file.pdf" "$TMP_DIR/uB.json")"
|
||||
assert_status "$status" "200" "upload second same-file" "$TMP_DIR/uB.json"
|
||||
IID_B="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uB.json")"
|
||||
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_B/bind" "$TMP_DIR/bB.json" "{\"reimbursementId\":\"$RID_B\"}")"
|
||||
assert_status "$status" "409" "file_sha256 dedup bind" "$TMP_DIR/bB.json"
|
||||
assert_error_code "$TMP_DIR/bB.json" "VALID_003"
|
||||
|
||||
echo "[3/7] Dedup by invoice_code+invoice_number => VALID_003"
|
||||
cat > "$TMP_DIR/code-1.pdf" <<PDF
|
||||
%PDF-1.4
|
||||
code-one-$RUN_SUFFIX
|
||||
PDF
|
||||
cat > "$TMP_DIR/code-2.pdf" <<PDF
|
||||
%PDF-1.4
|
||||
code-two-$RUN_SUFFIX
|
||||
PDF
|
||||
|
||||
RID_C="$(create_reimbursement "$TMP_DIR/rC.json")"
|
||||
status="$(upload_file "$TMP_DIR/code-1.pdf" "$TMP_DIR/uC.json")"
|
||||
assert_status "$status" "200" "upload code-1" "$TMP_DIR/uC.json"
|
||||
IID_C="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uC.json")"
|
||||
set_invoice_code_number "$IID_C" "$DEDUP_CODE" "$DEDUP_NUMBER"
|
||||
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_C/bind" "$TMP_DIR/bC.json" "{\"reimbursementId\":\"$RID_C\"}")"
|
||||
assert_status "$status" "200" "bind code-1" "$TMP_DIR/bC.json"
|
||||
|
||||
RID_D="$(create_reimbursement "$TMP_DIR/rD.json")"
|
||||
status="$(upload_file "$TMP_DIR/code-2.pdf" "$TMP_DIR/uD.json")"
|
||||
assert_status "$status" "200" "upload code-2" "$TMP_DIR/uD.json"
|
||||
IID_D="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uD.json")"
|
||||
set_invoice_code_number "$IID_D" "$DEDUP_CODE" "$DEDUP_NUMBER"
|
||||
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_D/bind" "$TMP_DIR/bD.json" "{\"reimbursementId\":\"$RID_D\"}")"
|
||||
assert_status "$status" "409" "code+number dedup bind" "$TMP_DIR/bD.json"
|
||||
assert_error_code "$TMP_DIR/bD.json" "VALID_003"
|
||||
|
||||
echo "[4/7] Non-dedup P2002 should not map to VALID_003"
|
||||
create_tmp_non_dedup_index
|
||||
TMP_INDEX_CREATED=1
|
||||
|
||||
cat > "$TMP_DIR/non-dedup-1.pdf" <<PDF
|
||||
%PDF-1.4
|
||||
non-dedup-1-$RUN_SUFFIX
|
||||
PDF
|
||||
cat > "$TMP_DIR/non-dedup-2.pdf" <<PDF
|
||||
%PDF-1.4
|
||||
non-dedup-2-$RUN_SUFFIX
|
||||
PDF
|
||||
|
||||
RID_E="$(create_reimbursement "$TMP_DIR/rE.json")"
|
||||
status="$(upload_file "$TMP_DIR/non-dedup-1.pdf" "$TMP_DIR/uE1.json")"
|
||||
assert_status "$status" "200" "upload non-dedup-1" "$TMP_DIR/uE1.json"
|
||||
IID_E1="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uE1.json")"
|
||||
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_E1/bind" "$TMP_DIR/bE1.json" "{\"reimbursementId\":\"$RID_E\"}")"
|
||||
assert_status "$status" "200" "bind non-dedup-1" "$TMP_DIR/bE1.json"
|
||||
|
||||
status="$(upload_file "$TMP_DIR/non-dedup-2.pdf" "$TMP_DIR/uE2.json")"
|
||||
assert_status "$status" "200" "upload non-dedup-2" "$TMP_DIR/uE2.json"
|
||||
IID_E2="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uE2.json")"
|
||||
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_E2/bind" "$TMP_DIR/bE2.json" "{\"reimbursementId\":\"$RID_E\"}")"
|
||||
if [[ "$status" == "409" ]] && [[ "$(jq -r '.error.code // empty' "$TMP_DIR/bE2.json")" == "VALID_003" ]]; then
|
||||
echo "[ERROR] non-dedup P2002 was incorrectly mapped to VALID_003" >&2
|
||||
cat "$TMP_DIR/bE2.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[5/7] Validate non-dedup result is server-side failure path"
|
||||
if [[ "$status" != "500" ]]; then
|
||||
echo "[ERROR] expected HTTP 500 for temporary non-dedup unique index violation, got $status" >&2
|
||||
cat "$TMP_DIR/bE2.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[6/7] Ensure non-dedup response is not VALID_003"
|
||||
unexpected_code="$(jq -r '.error.code // empty' "$TMP_DIR/bE2.json")"
|
||||
if [[ "$unexpected_code" == "VALID_003" ]]; then
|
||||
echo "[ERROR] unexpected VALID_003 for non-dedup conflict" >&2
|
||||
cat "$TMP_DIR/bE2.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
drop_tmp_non_dedup_index
|
||||
TMP_INDEX_CREATED=0
|
||||
|
||||
echo "[7/7] Done"
|
||||
echo "[OK] Dedup regression passed"
|
||||
+1107
文件差异内容过多而无法显示
加载差异
+208
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
|
||||
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-smoke-script-device}"
|
||||
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_IDENTIFIER="${FINANCE_IDENTIFIER:-finance@hyinda.com}"
|
||||
FINANCE_PASSWORD="${FINANCE_PASSWORD:-FinancePassword1234}"
|
||||
SAMPLE_FILE="${SAMPLE_FILE:-/tmp/hyb-oa-sample-invoice.pdf}"
|
||||
|
||||
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
|
||||
|
||||
if [[ ! -f "$SAMPLE_FILE" ]]; then
|
||||
echo "[ERROR] Sample file not found: $SAMPLE_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
RUN_UPLOAD_FILE="$TMP_DIR/upload-$(basename "$SAMPLE_FILE")"
|
||||
cp "$SAMPLE_FILE" "$RUN_UPLOAD_FILE"
|
||||
printf '\n%%RUN-%s\n' "$(date +%s%N)" >> "$RUN_UPLOAD_FILE"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
echo "[1/13] Health check"
|
||||
health_body="$TMP_DIR/health.json"
|
||||
health_status="$(curl -sS -o "$health_body" -w "%{http_code}" "$BASE_URL/health")"
|
||||
assert_status "$health_status" "200" "health"
|
||||
|
||||
echo "[2/13] Negative: no token should be 401"
|
||||
no_token_body="$TMP_DIR/no-token.json"
|
||||
no_token_status="$(curl -sS -o "$no_token_body" -w "%{http_code}" "$BASE_URL/api/reimbursements/test-id")"
|
||||
assert_status "$no_token_status" "401" "no token"
|
||||
|
||||
echo "[3/13] Login employee"
|
||||
employee_login_body="$TMP_DIR/employee-login.json"
|
||||
employee_login_status="$(curl -sS -o "$employee_login_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/auth/login" \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
|
||||
-d "{\"identifier\":\"$EMPLOYEE_IDENTIFIER\",\"password\":\"$EMPLOYEE_PASSWORD\"}")"
|
||||
assert_status "$employee_login_status" "200" "employee login"
|
||||
EMPLOYEE_TOKEN="$(extract_json_field "$employee_login_body" '.data.accessToken')"
|
||||
|
||||
echo "[4/13] Create reimbursement draft"
|
||||
create_body="$TMP_DIR/create.json"
|
||||
create_status="$(curl -sS -o "$create_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/reimbursements" \
|
||||
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{}')"
|
||||
assert_status "$create_status" "200" "create reimbursement"
|
||||
REIMBURSEMENT_ID="$(extract_json_field "$create_body" '.data.id')"
|
||||
|
||||
echo "[5/13] Upload invoice"
|
||||
upload_body="$TMP_DIR/upload.json"
|
||||
upload_status="$(curl -sS -o "$upload_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/invoice-pool/upload" \
|
||||
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
|
||||
-F "file=@$RUN_UPLOAD_FILE")"
|
||||
assert_status "$upload_status" "200" "upload invoice"
|
||||
INVOICE_ID="$(extract_json_field "$upload_body" '.data.invoicePoolId')"
|
||||
|
||||
echo "[6/13] Bind invoice"
|
||||
bind_body="$TMP_DIR/bind.json"
|
||||
bind_status="$(curl -sS -o "$bind_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/bind" \
|
||||
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d "{\"reimbursementId\":\"$REIMBURSEMENT_ID\"}")"
|
||||
assert_status "$bind_status" "200" "bind invoice"
|
||||
|
||||
echo "[7/13] Negative: bind same invoice again should be VALID_005"
|
||||
rebind_body="$TMP_DIR/rebind.json"
|
||||
rebind_status="$(curl -sS -o "$rebind_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/bind" \
|
||||
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d "{\"reimbursementId\":\"$REIMBURSEMENT_ID\"}")"
|
||||
assert_status "$rebind_status" "409" "rebind conflict"
|
||||
assert_error_code "$rebind_body" "VALID_005" "rebind conflict"
|
||||
|
||||
echo "[8/13] Submit reimbursement"
|
||||
submit_body="$TMP_DIR/submit.json"
|
||||
submit_status="$(curl -sS -o "$submit_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/reimbursements/$REIMBURSEMENT_ID/submit" \
|
||||
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{}')"
|
||||
assert_status "$submit_status" "200" "submit reimbursement"
|
||||
|
||||
echo "[9/13] Login manager"
|
||||
manager_login_body="$TMP_DIR/manager-login.json"
|
||||
manager_login_status="$(curl -sS -o "$manager_login_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/auth/login" \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-device-fingerprint: ${DEVICE_FINGERPRINT}-manager" \
|
||||
-d "{\"identifier\":\"$MANAGER_IDENTIFIER\",\"password\":\"$MANAGER_PASSWORD\"}")"
|
||||
assert_status "$manager_login_status" "200" "manager login"
|
||||
MANAGER_TOKEN="$(extract_json_field "$manager_login_body" '.data.accessToken')"
|
||||
|
||||
echo "[10/13] Login finance (approve permission, non-assignee)"
|
||||
finance_login_body="$TMP_DIR/finance-login.json"
|
||||
finance_login_status="$(curl -sS -o "$finance_login_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/auth/login" \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-device-fingerprint: ${DEVICE_FINGERPRINT}-finance" \
|
||||
-d "{\"identifier\":\"$FINANCE_IDENTIFIER\",\"password\":\"$FINANCE_PASSWORD\"}")"
|
||||
assert_status "$finance_login_status" "200" "finance login"
|
||||
FINANCE_TOKEN="$(extract_json_field "$finance_login_body" '.data.accessToken')"
|
||||
|
||||
echo "[11/13] Query pending tasks as manager"
|
||||
tasks_body="$TMP_DIR/tasks.json"
|
||||
tasks_status="$(curl -sS -o "$tasks_body" -w "%{http_code}" \
|
||||
-X GET "$BASE_URL/api/workflow/tasks?status=pending" \
|
||||
-H "authorization: Bearer $MANAGER_TOKEN")"
|
||||
assert_status "$tasks_status" "200" "query workflow tasks"
|
||||
TASK_ID="$(jq -er --arg rid "$REIMBURSEMENT_ID" '.data.items[] | select(.workflow_instance.entity_id == $rid) | .id' "$tasks_body" | head -n 1)"
|
||||
if [[ -z "$TASK_ID" ]]; then
|
||||
echo "[ERROR] No pending task found for reimbursement $REIMBURSEMENT_ID" >&2
|
||||
cat "$tasks_body" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[12/13] Negative: finance cannot approve manager task (PERM_002)"
|
||||
finance_approve_body="$TMP_DIR/finance-approve.json"
|
||||
finance_approve_status="$(curl -sS -o "$finance_approve_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/workflow/tasks/$TASK_ID/approve" \
|
||||
-H "authorization: Bearer $FINANCE_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{}')"
|
||||
assert_status "$finance_approve_status" "403" "finance approve forbidden"
|
||||
assert_error_code "$finance_approve_body" "PERM_002" "finance approve forbidden"
|
||||
|
||||
echo "[13/13] Approve by manager and fetch signed URL"
|
||||
manager_approve_body="$TMP_DIR/manager-approve.json"
|
||||
manager_approve_status="$(curl -sS -o "$manager_approve_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/workflow/tasks/$TASK_ID/approve" \
|
||||
-H "authorization: Bearer $MANAGER_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{}')"
|
||||
assert_status "$manager_approve_status" "200" "manager approve"
|
||||
|
||||
signed_body="$TMP_DIR/signed-url.json"
|
||||
signed_status="$(curl -sS -o "$signed_body" -w "%{http_code}" \
|
||||
-X POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/signed-url" \
|
||||
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{}')"
|
||||
assert_status "$signed_status" "200" "signed url"
|
||||
SIGNED_URL="$(extract_json_field "$signed_body" '.data.signedUrl')"
|
||||
|
||||
signed_download="$TMP_DIR/signed-download.bin"
|
||||
download_status="$(curl -sS -L -o "$signed_download" -w "%{http_code}" "$SIGNED_URL")"
|
||||
assert_status "$download_status" "200" "signed url download"
|
||||
|
||||
if [[ ! -s "$signed_download" ]]; then
|
||||
echo "[ERROR] Signed URL download is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[OK] Smoke test passed"
|
||||
echo "- reimbursementId: $REIMBURSEMENT_ID"
|
||||
echo "- invoiceId: $INVOICE_ID"
|
||||
echo "- taskId: $TASK_ID"
|
||||
在新工单中引用
屏蔽一个用户