docs/test(P18A-5,P18A-6): 收口资产矩阵文档与回归

这个提交包含在:
2026-03-07 22:33:17 +08:00
父节点 447ffa66d2
当前提交 865acb63aa
共修改 7 个文件,包含 348 行新增和 21 行删除
+11 -10
查看文件
@@ -57,7 +57,7 @@ if [[ -n "$JSON_REPORT_PATH" ]]; then
trap rr_cleanup EXIT
fi
TOTAL_STEPS=13
TOTAL_STEPS=14
PASSED_STEPS=0
FAILED_STEPS=0
SKIPPED_STEPS=0
@@ -125,15 +125,16 @@ run_step 1 "smoke" "Smoke Test" "1" '"$SCRIPT_DIR"/smoke-test.sh'
run_step 2 "dedup" "Dedup Regression" "1" '"$SCRIPT_DIR"/regression-dedup.sh'
run_step 3 "workflow_actions" "Workflow Actions Regression" "1" '"$SCRIPT_DIR"/regression-workflow-actions.sh'
run_step 4 "procurement" "Procurement Regression" "1" '"$SCRIPT_DIR"/regression-procurement.sh'
run_step 5 "audit" "Audit Regression" "$RUN_AUDIT_REGRESSION" '"$SCRIPT_DIR"/regression-audit.sh' "RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION"
run_step 6 "ocr" "OCR Regression" "$RUN_OCR_REGRESSION" '"$SCRIPT_DIR"/regression-ocr.sh' "RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION"
run_step 7 "org_settings" "Org + Settings Regression" "$RUN_ORG_SETTINGS_REGRESSION" '"$SCRIPT_DIR"/regression-org-settings.sh' "RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION"
run_step 8 "auth_security" "Auth Security Regression" "$RUN_AUTH_SECURITY_REGRESSION" '"$SCRIPT_DIR"/regression-auth-security.sh' "RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION"
run_step 9 "verification" "Verification Regression" "$RUN_VERIFICATION_REGRESSION" '"$SCRIPT_DIR"/regression-verification.sh' "RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION"
run_step 10 "dashboard" "Dashboard Regression" "$RUN_DASHBOARD_REGRESSION" '"$SCRIPT_DIR"/regression-dashboard.sh' "RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION"
run_step 11 "budget" "Budget Regression" "$RUN_BUDGET_REGRESSION" '"$SCRIPT_DIR"/regression-budget.sh' "RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION"
run_step 12 "integrations" "Integrations Regression" "$RUN_INTEGRATIONS_REGRESSION" '"$SCRIPT_DIR"/regression-integrations.sh' "RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION"
run_step 13 "feature_flags" "Feature Flags Regression" "$RUN_FEATURE_FLAGS_REGRESSION" '"$SCRIPT_DIR"/regression-feature-flags.sh' "RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION"
run_step 5 "asset" "Asset Regression" "1" '"$SCRIPT_DIR"/regression-assets.sh'
run_step 6 "audit" "Audit Regression" "$RUN_AUDIT_REGRESSION" '"$SCRIPT_DIR"/regression-audit.sh' "RUN_AUDIT_REGRESSION=$RUN_AUDIT_REGRESSION"
run_step 7 "ocr" "OCR Regression" "$RUN_OCR_REGRESSION" '"$SCRIPT_DIR"/regression-ocr.sh' "RUN_OCR_REGRESSION=$RUN_OCR_REGRESSION"
run_step 8 "org_settings" "Org + Settings Regression" "$RUN_ORG_SETTINGS_REGRESSION" '"$SCRIPT_DIR"/regression-org-settings.sh' "RUN_ORG_SETTINGS_REGRESSION=$RUN_ORG_SETTINGS_REGRESSION"
run_step 9 "auth_security" "Auth Security Regression" "$RUN_AUTH_SECURITY_REGRESSION" '"$SCRIPT_DIR"/regression-auth-security.sh' "RUN_AUTH_SECURITY_REGRESSION=$RUN_AUTH_SECURITY_REGRESSION"
run_step 10 "verification" "Verification Regression" "$RUN_VERIFICATION_REGRESSION" '"$SCRIPT_DIR"/regression-verification.sh' "RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION"
run_step 11 "dashboard" "Dashboard Regression" "$RUN_DASHBOARD_REGRESSION" '"$SCRIPT_DIR"/regression-dashboard.sh' "RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION"
run_step 12 "budget" "Budget Regression" "$RUN_BUDGET_REGRESSION" '"$SCRIPT_DIR"/regression-budget.sh' "RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION"
run_step 13 "integrations" "Integrations Regression" "$RUN_INTEGRATIONS_REGRESSION" '"$SCRIPT_DIR"/regression-integrations.sh' "RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION"
run_step 14 "feature_flags" "Feature Flags Regression" "$RUN_FEATURE_FLAGS_REGRESSION" '"$SCRIPT_DIR"/regression-feature-flags.sh' "RUN_FEATURE_FLAGS_REGRESSION=$RUN_FEATURE_FLAGS_REGRESSION"
RUN_ENDED_MS="$(rr_now_ms)"
OVERALL_DURATION_MS="$((RUN_ENDED_MS - RUN_STARTED_MS))"
+283
查看文件
@@ -0,0 +1,283 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-assets}"
EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}"
EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}"
FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}"
FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}"
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
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)"
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
[[ -n "$body_file" && -f "$body_file" ]] && cat "$body_file" >&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
}
login_user() {
local identifier="$1"
local password="$2"
local out_file="$3"
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: $DEVICE_FINGERPRINT" \
-d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")"
assert_status "$status" "200" "login $identifier" "$out_file"
jq -er '.data.accessToken' "$out_file"
}
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
}
resolve_fixture_ids() {
DATABASE_URL="$DATABASE_URL" node - <<'NODE'
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
const employee = await prisma.employee.findUnique({
where: { email: process.env.SEED_EMPLOYEE_EMAIL ?? '[email protected]' },
select: { id: true, legal_entity_id: true },
});
const costCenter = await prisma.costCenter.findFirst({
where: {
legal_entity_id: employee.legal_entity_id,
enabled: true,
archived_at: null,
},
orderBy: { created_at: 'asc' },
select: { id: true },
});
const documentCategory = await prisma.documentCategory.findFirst({
where: {
entity_type: 'asset_card',
enabled: true,
archived_at: null,
code: 'ASSET_CARD_DEFAULT',
},
orderBy: { created_at: 'asc' },
select: { id: true },
}) ?? await prisma.documentCategory.findFirst({
where: {
entity_type: 'asset_card',
enabled: true,
archived_at: null,
},
orderBy: { created_at: 'asc' },
select: { id: true },
});
const approvedPurchase = await prisma.purchaseRequest.findFirst({
where: { status: 'approved' },
orderBy: { updated_at: 'desc' },
select: { id: true },
});
process.stdout.write([
employee.id,
employee.legal_entity_id ?? '',
costCenter?.id ?? '',
documentCategory?.id ?? '',
approvedPurchase?.id ?? '',
].join('\n'));
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
assert_asset_state() {
local asset_id="$1"
local category_id="$2"
local employee_id="$3"
local purchase_request_id="$4"
DATABASE_URL="$DATABASE_URL" node - "$asset_id" "$category_id" "$employee_id" "$purchase_request_id" <<'NODE'
const [assetId, categoryId, employeeId, purchaseRequestId] = process.argv.slice(2);
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
const category = await prisma.assetCategory.findUnique({
where: { id: categoryId },
select: { name: true, document_category: { select: { code: true } } },
});
const asset = await prisma.assetCard.findUnique({
where: { id: assetId },
select: {
asset_no: true,
status: true,
assignee_employee_id: true,
purchase_request_id: true,
location: true,
},
});
if (!category || category.name !== '回归资产分类-已更新' || category.document_category?.code !== 'ASSET_CARD_DEFAULT') {
console.error('[ERROR] asset category state mismatch', category);
process.exit(1);
}
if (!asset || !asset.asset_no.startsWith('ZC-') || asset.status !== 'in_use' || asset.assignee_employee_id !== employeeId) {
console.error('[ERROR] asset card state mismatch', asset);
process.exit(1);
}
if (purchaseRequestId && asset.purchase_request_id !== purchaseRequestId) {
console.error('[ERROR] asset purchase source mismatch', asset);
process.exit(1);
}
if (asset.location !== '深圳办公室 A-01') {
console.error('[ERROR] asset location mismatch', asset);
process.exit(1);
}
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
echo "[1/8] login finance manager and employee"
FINANCE_MGR_TOKEN="$(login_user "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$TMP_DIR/finance-mgr-login.json")"
EMPLOYEE_TOKEN="$(login_user "$EMPLOYEE_IDENTIFIER" "$EMPLOYEE_PASSWORD" "$TMP_DIR/employee-login.json")"
mapfile -t FIXTURE_IDS < <(resolve_fixture_ids)
EMPLOYEE_ID="${FIXTURE_IDS[0]}"
LEGAL_ENTITY_ID="${FIXTURE_IDS[1]}"
COST_CENTER_ID="${FIXTURE_IDS[2]}"
DOCUMENT_CATEGORY_ID="${FIXTURE_IDS[3]}"
APPROVED_PURCHASE_ID="${FIXTURE_IDS[4]}"
if [[ -z "$LEGAL_ENTITY_ID" || -z "$COST_CENTER_ID" || -z "$DOCUMENT_CATEGORY_ID" ]]; then
echo "[ERROR] missing asset fixtures" >&2
exit 1
fi
echo "[2/8] create asset category"
CATEGORY_CODE="ASSET_REG_$(date +%s)"
CREATE_CATEGORY_BODY="$(jq -nc --arg code "$CATEGORY_CODE" --arg dc "$DOCUMENT_CATEGORY_ID" '{code:$code,name:"回归资产分类",documentCategoryId:$dc,enabled:true}')"
create_category_status="$(api_json_auth POST "$BASE_URL/api/asset-categories" "$FINANCE_MGR_TOKEN" "$TMP_DIR/create-category.json" "$CREATE_CATEGORY_BODY")"
assert_status "$create_category_status" "200" "create asset category" "$TMP_DIR/create-category.json"
CATEGORY_ID="$(jq -er '.data.assetCategory.id' "$TMP_DIR/create-category.json")"
assert_jq "$TMP_DIR/create-category.json" '.data.assetCategory.documentCategory.code == "ASSET_CARD_DEFAULT"' "asset category document category"
echo "[3/8] update asset category"
PATCH_CATEGORY_BODY='{"name":"回归资产分类-已更新","enabled":true}'
patch_category_status="$(api_json_auth PATCH "$BASE_URL/api/asset-categories/$CATEGORY_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/patch-category.json" "$PATCH_CATEGORY_BODY")"
assert_status "$patch_category_status" "200" "patch asset category" "$TMP_DIR/patch-category.json"
assert_jq "$TMP_DIR/patch-category.json" '.data.assetCategory.name == "回归资产分类-已更新"' "patched asset category name"
echo "[4/8] create asset card with approved purchase source"
CREATE_ASSET_BODY="$(jq -nc --arg name "回归资产卡片" --arg categoryId "$CATEGORY_ID" --arg legalEntityId "$LEGAL_ENTITY_ID" --arg costCenterId "$COST_CENTER_ID" --arg purchaseRequestId "$APPROVED_PURCHASE_ID" --arg assigneeEmployeeId "$EMPLOYEE_ID" '{name:$name,categoryId:$categoryId,legalEntityId:$legalEntityId,costCenterId:$costCenterId,purchaseRequestId:($purchaseRequestId|select(length>0)),status:"in_stock",assigneeEmployeeId:$assigneeEmployeeId}')"
create_asset_status="$(api_json_auth POST "$BASE_URL/api/assets" "$FINANCE_MGR_TOKEN" "$TMP_DIR/create-asset.json" "$CREATE_ASSET_BODY")"
assert_status "$create_asset_status" "200" "create asset" "$TMP_DIR/create-asset.json"
ASSET_ID="$(jq -er '.data.asset.id' "$TMP_DIR/create-asset.json")"
assert_jq "$TMP_DIR/create-asset.json" '.data.asset.assetNo | test("^ZC-")' "asset no generated"
assert_jq "$TMP_DIR/create-asset.json" '.data.asset.assignee.employeeNo == "EMP001" or .data.asset.assignee.name == "Employee"' "asset assignee set"
echo "[5/8] employee can only see own assigned asset"
list_own_status="$(api_json_auth GET "$BASE_URL/api/assets?mine=1" "$EMPLOYEE_TOKEN" "$TMP_DIR/list-own.json")"
assert_status "$list_own_status" "200" "employee list own assets" "$TMP_DIR/list-own.json"
assert_jq "$TMP_DIR/list-own.json" ".data.mine == true and (.data.items | map(.id) | index(\"$ASSET_ID\") != null)" "employee owns asset"
echo "[6/8] finance manager updates asset status and location"
PATCH_ASSET_BODY='{"status":"in_use","location":"深圳办公室 A-01"}'
patch_asset_status="$(api_json_auth PATCH "$BASE_URL/api/assets/$ASSET_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/patch-asset.json" "$PATCH_ASSET_BODY")"
assert_status "$patch_asset_status" "200" "patch asset" "$TMP_DIR/patch-asset.json"
assert_jq "$TMP_DIR/patch-asset.json" '.data.asset.status == "in_use" and .data.asset.location == "深圳办公室 A-01"' "asset status/location updated"
echo "[7/8] purchase source is immutable after create"
INVALID_PATCH_BODY="$(jq -nc --arg purchaseRequestId "$APPROVED_PURCHASE_ID" '{purchaseRequestId:$purchaseRequestId}')"
invalid_patch_status="$(api_json_auth PATCH "$BASE_URL/api/assets/$ASSET_ID" "$FINANCE_MGR_TOKEN" "$TMP_DIR/invalid-patch.json" "$INVALID_PATCH_BODY")"
assert_status "$invalid_patch_status" "400" "patch asset purchase source forbidden" "$TMP_DIR/invalid-patch.json"
assert_jq "$TMP_DIR/invalid-patch.json" '.error.code == "VALID_001"' "invalid purchase source patch code"
echo "[8/8] verify category and asset db state"
assert_asset_state "$ASSET_ID" "$CATEGORY_ID" "$EMPLOYEE_ID" "$APPROVED_PURCHASE_ID"
echo "[OK] Asset regression passed"
echo "- assetCategoryId: $CATEGORY_ID"
echo "- assetId: $ASSET_ID"
@@ -44,6 +44,17 @@ describe("Permission Constants", () => {
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "procurement.manage")).toBe(true);
});
it("grants asset permissions to configured roles", () => {
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "asset.view_own")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["manager"]), "asset.view_all")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["finance"]), "asset.view_all")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "asset.manage")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["administration"]), "asset.transfer")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["finance_mgr"]), "asset.inventory")).toBe(true);
expect(hasPermission(resolvePermissionsForRoles(["employee"]), "asset.manage")).toBe(false);
});
it("maps workflow entities to correct permission codes", () => {
expect(resolveEntityPermission("reimbursement", "approve")).toBe("expense.approve");
expect(resolveEntityPermission("reimbursement", "transfer")).toBe("expense.transfer");
@@ -130,4 +130,27 @@ describe("documentSequence service", () => {
expect(issued.config.source).toBe("document_category");
expect(issued.documentNumber).toMatch(/^CG-\d{8}-0001$/);
});
it("falls back to default asset category code for asset cards", async () => {
const prisma = buildPrisma({
categoryMap: {
ASSET_CARD_DEFAULT: {
code: "ASSET_CARD_DEFAULT",
entity_type: "asset_card",
sequence_prefix: "ZC",
number_length: 4,
reset_period: "yearly",
enabled: true,
archived_at: null,
},
},
});
const config = await resolveDocumentSequenceConfig(prisma, { documentType: "asset_card" });
expect(config.source).toBe("document_category");
expect(config.documentCategoryCode).toBe("ASSET_CARD_DEFAULT");
expect(config.prefix).toBe("ZC");
});
});