284 行
10 KiB
Bash
可执行文件
284 行
10 KiB
Bash
可执行文件
#!/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"
|