Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # firstleg/src/main/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceService.java # firstleg/src/test/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceServiceTest.java # scripts/admin-test-env-modules/ocean-bookings.mjs # scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs # scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs # scripts/admin-test-env-modules/phase3-container-loading.mjs # scripts/admin-test-env-modules/phase3-customs.mjs # scripts/admin-test-env-modules/phase3-loading-finalize.mjs # scripts/admin-test-env-modules/phase3-loading.mjs # scripts/admin-test-env-modules/phase3-port-schedules.mjs # scripts/admin-test-env-modules/service-center-flow.mjs
This commit is contained in:
@@ -42,6 +42,7 @@ if (!Number.isFinite(timeoutMs) || timeoutMs < 1_000) {
|
||||
const now = new Date()
|
||||
const marker = `${compactTimestamp(now)}-${randomUUID().slice(0, 8)}`
|
||||
const username = `client_${compactTimestamp(now)}_${randomUUID().slice(0, 6)}`.slice(0, 64)
|
||||
const temporaryUsername = `renamed_${compactTimestamp(now)}_${randomUUID().slice(0, 6)}`.slice(0, 64)
|
||||
const clientPassword = `Client${randomUUID().replaceAll('-', '').slice(0, 14)}9!`
|
||||
const phoneSuffix = `${Date.now()}`.slice(-8)
|
||||
const legalPhone = `139${phoneSuffix}`
|
||||
@@ -91,6 +92,7 @@ try {
|
||||
await qualificationAndReviewModule()
|
||||
await merchantAfterApprovalModule()
|
||||
await profileManagementModule()
|
||||
await loginIdentifierModule()
|
||||
await rbacAndMemberModule()
|
||||
await orderLifecycleModule()
|
||||
await isolationModule()
|
||||
@@ -117,7 +119,7 @@ async function registrationModule() {
|
||||
displayName: `测试管理员-${marker}`.slice(0, 64),
|
||||
companyName,
|
||||
legalPerson: '测试法人',
|
||||
legalPhone,
|
||||
phone: legalPhone,
|
||||
contactEmail: `${username}@example.com`,
|
||||
countryCode: 'CN',
|
||||
address: '广东省深圳市南山区测试路 1 号',
|
||||
@@ -213,7 +215,6 @@ async function qualificationAndReviewModule() {
|
||||
const png = testPng()
|
||||
const form = new FormData()
|
||||
form.append('businessLicense', new Blob([png], { type: 'image/png' }), `business-license-${marker}.png`)
|
||||
form.append('legalPersonId', new Blob([png], { type: 'image/png' }), `legal-person-id-${marker}.png`)
|
||||
|
||||
await apiStep('qualification-review', 'upload required qualification package', {
|
||||
method: 'POST',
|
||||
@@ -223,7 +224,7 @@ async function qualificationAndReviewModule() {
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(json.data?.businessLicenseQualificationId, 'business license qualification id')
|
||||
assertPresent(json.data?.legalPersonIdQualificationId, 'legal person qualification id')
|
||||
assertTrue(json.data?.legalPersonIdQualificationId == null, 'legal person qualification id must stay optional')
|
||||
assertEqual(json.data?.tenantStatus, 'PROFILE_REQUIRED', 'tenant status after upload')
|
||||
}, ({ json }) => ({
|
||||
businessLicenseQualificationId: json.data.businessLicenseQualificationId,
|
||||
@@ -274,7 +275,9 @@ async function qualificationAndReviewModule() {
|
||||
method: 'GET', path: '/api/merchant/v1/iam/tenant/qualifications', token: merchantToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertQualificationStatuses(json.data, ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'], 'PENDING')
|
||||
assertQualificationStatuses(json.data, ['BUSINESS_LICENSE'], 'PENDING')
|
||||
assertTrue(!arrayData(json.data).some((item) => item.qualificationType === 'LEGAL_PERSON_ID'),
|
||||
'optional legal person ID must remain absent')
|
||||
assertQualificationStatuses(json.data, ['FREIGHT_FORWARDER_CERTIFICATE', 'BANK_ACCOUNT_LICENSE'], 'PENDING')
|
||||
}, ({ json }) => ({ qualifications: qualificationEvidence(json.data) }))
|
||||
|
||||
@@ -293,6 +296,19 @@ async function qualificationAndReviewModule() {
|
||||
}, () => ({ username: platformUsername }))
|
||||
platformToken = platformLogin.json.data.accessToken
|
||||
|
||||
const dimensionPath = `/api/platform/v1/iam/tenants/${report.resources.tenantId}/dimension-initialization`
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const statusResponse = await httpRequest({ method: 'GET', path: dimensionPath, token: platformToken })
|
||||
if (String(statusResponse.json?.data?.status) === 'READY') break
|
||||
if (attempt < 29) await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||
}
|
||||
await apiStep('qualification-review', 'customer default dimensions are ready before approval', {
|
||||
method: 'GET', path: dimensionPath, token: platformToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertEqual(json.data?.status, 'READY', 'dimension initialization status')
|
||||
}, ({ json }) => ({ status: json.data.status, attemptCount: json.data.attemptCount }))
|
||||
|
||||
const reviews = await apiStep('qualification-review', 'platform finds pending review', {
|
||||
method: 'GET', path: '/api/platform/v1/iam/reviews', token: platformToken,
|
||||
}, ({ json }) => {
|
||||
@@ -310,7 +326,9 @@ async function qualificationAndReviewModule() {
|
||||
method: 'GET', path: `/api/platform/v1/iam/tenants/${report.resources.tenantId}/qualifications`, token: platformToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertQualificationStatuses(json.data, ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'], 'PENDING')
|
||||
assertQualificationStatuses(json.data, ['BUSINESS_LICENSE'], 'PENDING')
|
||||
assertTrue(!arrayData(json.data).some((item) => item.qualificationType === 'LEGAL_PERSON_ID'),
|
||||
'platform must not require a legal person ID record')
|
||||
}, ({ json }) => ({ qualifications: qualificationEvidence(json.data) }))
|
||||
|
||||
const approval = await apiStep('qualification-review', 'platform approves enterprise', {
|
||||
@@ -343,6 +361,51 @@ async function qualificationAndReviewModule() {
|
||||
})
|
||||
}
|
||||
|
||||
async function loginIdentifierModule() {
|
||||
const staleToken = merchantToken
|
||||
await apiStep('login-identifiers', 'change username with current-password verification', {
|
||||
method: 'PUT', path: '/api/merchant/v1/iam/profile/login-identifiers', token: merchantToken,
|
||||
body: { username: temporaryUsername, phone: legalPhone, currentPassword: clientPassword },
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertEqual(json.data?.username, temporaryUsername, 'updated username')
|
||||
assertEqual(json.data?.phone, legalPhone, 'updated phone')
|
||||
}, ({ json }) => ({ username: json.data.username, phone: json.data.phone }))
|
||||
|
||||
await apiStep('login-identifiers', 'identifier change revokes the previous access token', {
|
||||
method: 'GET', path: '/api/merchant/v1/iam/profile', token: staleToken,
|
||||
}, ({ status, json }) => {
|
||||
assertEqual(status, 401, 'HTTP status')
|
||||
assertTrue(String(json?.code || '').length > 0, 'authentication error code must be present')
|
||||
}, ({ status, json }) => ({ status, rejectionCode: json?.code }))
|
||||
|
||||
const renamedLogin = await apiStep('login-identifiers', 'login with the changed username', {
|
||||
method: 'POST', path: '/api/auth/login', body: { username: temporaryUsername, password: clientPassword },
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(json.data?.accessToken, 'accessToken')
|
||||
}, () => ({ username: temporaryUsername }))
|
||||
merchantToken = renamedLogin.json.data.accessToken
|
||||
merchantRefreshToken = renamedLogin.json.data.refreshToken
|
||||
|
||||
await apiStep('login-identifiers', 'restore the original username for the remaining acceptance flow', {
|
||||
method: 'PUT', path: '/api/merchant/v1/iam/profile/login-identifiers', token: merchantToken,
|
||||
body: { username, phone: legalPhone, currentPassword: clientPassword },
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertEqual(json.data?.username, username, 'restored username')
|
||||
}, ({ json }) => ({ username: json.data.username }))
|
||||
|
||||
const restoredLogin = await apiStep('login-identifiers', 'login after restoring the original username', {
|
||||
method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword },
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(json.data?.accessToken, 'accessToken')
|
||||
}, () => ({ username }))
|
||||
merchantToken = restoredLogin.json.data.accessToken
|
||||
merchantRefreshToken = restoredLogin.json.data.refreshToken
|
||||
}
|
||||
|
||||
async function merchantAfterApprovalModule() {
|
||||
const login = await apiStep('merchant-session', 'client re-login after approval', {
|
||||
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: clientPassword },
|
||||
@@ -365,7 +428,9 @@ async function merchantAfterApprovalModule() {
|
||||
method: 'GET', path: '/api/merchant/v1/iam/tenant/qualifications', token: merchantToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertQualificationStatuses(json.data, ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'], 'APPROVED')
|
||||
assertQualificationStatuses(json.data, ['BUSINESS_LICENSE'], 'APPROVED')
|
||||
assertTrue(!arrayData(json.data).some((item) => item.qualificationType === 'LEGAL_PERSON_ID'),
|
||||
'approved enterprise must not gain a synthetic legal person ID record')
|
||||
assertQualificationStatuses(json.data, ['FREIGHT_FORWARDER_CERTIFICATE', 'BANK_ACCOUNT_LICENSE'], 'APPROVED')
|
||||
}, ({ json }) => ({ qualifications: qualificationEvidence(json.data) }))
|
||||
|
||||
@@ -1004,9 +1069,9 @@ async function orderLifecycleModule() {
|
||||
}, ({ bytes, headers }) => ({ sizeBytes: bytes.length, contentType: headers.get('content-type') }))
|
||||
|
||||
const payload = buildOrderPayload(masterData, productImageFileId, 'ACTIVE', 'fresh draft after approval')
|
||||
const invalidOrderCases = [
|
||||
const incompleteSubmitCases = [
|
||||
{
|
||||
name: 'FBA order missing Amazon identifiers is rejected',
|
||||
name: 'FBA order missing Amazon identifiers',
|
||||
mutate(value) {
|
||||
value.amazonWarehouseCode = null
|
||||
value.shipmentId = null
|
||||
@@ -1014,7 +1079,7 @@ async function orderLifecycleModule() {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'FBA cargo missing SKU UPC shipment and reference is rejected',
|
||||
name: 'FBA cargo missing SKU UPC shipment and reference',
|
||||
mutate(value) {
|
||||
value.cargoes[0].skuNo = null
|
||||
value.cargoes[0].upcNo = null
|
||||
@@ -1022,9 +1087,35 @@ async function orderLifecycleModule() {
|
||||
value.cargoes[0].referenceId = null
|
||||
},
|
||||
},
|
||||
]
|
||||
for (const testCase of incompleteSubmitCases) {
|
||||
const incompletePayload = structuredClone(payload)
|
||||
testCase.mutate(incompletePayload)
|
||||
const draft = await apiStep('order-validation', `${testCase.name} can be saved as a draft`, {
|
||||
method: 'POST', path: '/api/merchant/v1/orders/first-leg', token: merchantToken,
|
||||
headers: { 'Idempotency-Key': newIdempotencyKey('incomplete-draft') }, body: incompletePayload,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(json.data, 'incomplete draft order id')
|
||||
}, ({ json }) => ({ orderId: json.data, status: 'DRAFT' }))
|
||||
report.resources.orderIds.push(draft.json.data)
|
||||
await softApiStep('order-validation', `${testCase.name} is rejected on submit`, {
|
||||
method: 'POST', path: `/api/merchant/v1/orders/first-leg/${draft.json.data}/submit`, token: merchantToken,
|
||||
headers: { 'Idempotency-Key': newIdempotencyKey('incomplete-submit') },
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0422', 'business code')
|
||||
}, ({ json }) => ({ orderId: draft.json.data, rejectionCode: json?.code, message: json?.message }))
|
||||
}
|
||||
const invalidOrderCases = [
|
||||
{
|
||||
name: 'PICKUP order without sender is rejected',
|
||||
mutate(value) { value.sender = null },
|
||||
name: 'PICKUP order without a selected pickup address is rejected',
|
||||
mutate(value) {
|
||||
value.pickupMethod = 'PICKUP'
|
||||
value.requestedPickupAt = new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 19)
|
||||
value.sender = null
|
||||
value.senderAddressId = null
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'PRIVATE destination without receiver is rejected',
|
||||
@@ -1191,11 +1282,11 @@ async function orderLifecycleModule() {
|
||||
requireSuccess(json)
|
||||
assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'combined-filtered order')
|
||||
}, ({ json }) => ({ total: json.data?.total, orderId }))
|
||||
await apiStep('order-filters', 'sender phone filter contains submitted order', {
|
||||
method: 'GET', path: `/api/merchant/v1/orders/first-leg?page=1&size=50&customerContact=${encodeURIComponent(legalPhone)}`, token: merchantToken,
|
||||
await apiStep('order-filters', 'customer contact filter contains submitted order by customer name', {
|
||||
method: 'GET', path: `/api/merchant/v1/orders/first-leg?page=1&size=50&customerContact=${encodeURIComponent(payload.customerName)}`, token: merchantToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'sender-phone-filtered order')
|
||||
assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'customer-contact-filtered order')
|
||||
}, ({ json }) => ({ total: json.data?.total, orderId }))
|
||||
await apiStep('order-filters', 'unmatched keyword returns an empty order page', {
|
||||
method: 'GET', path: `/api/merchant/v1/orders/first-leg?page=1&size=50&keyword=${randomUUID()}`, token: merchantToken,
|
||||
@@ -1815,7 +1906,7 @@ function buildOrderPayload(masterData, productImageFileId, suffix, remark) {
|
||||
destinationWarehouseName: masterData.destination.warehouseName,
|
||||
expectedShipDate: addDays(new Date(), 30),
|
||||
customerRemark: `${remark} ${marker}`,
|
||||
pickupMethod: 'PICKUP',
|
||||
pickupMethod: 'SELF_EXPRESS',
|
||||
receivingWarehouseId: masterData.receiving.id,
|
||||
receivingWarehouseName: masterData.receiving.warehouseName,
|
||||
channelCode: masterData.channel.channelCode,
|
||||
@@ -1867,6 +1958,7 @@ function buildOrderPayload(masterData, productImageFileId, suffix, remark) {
|
||||
upcNo: `123456${`${Date.now()}`.slice(-6)}`.slice(0, 12),
|
||||
productImageFileId,
|
||||
dangerousGoods: false,
|
||||
woodenProduct: false,
|
||||
cargoRemark: `cargo-${remark} ${marker}`,
|
||||
}],
|
||||
}
|
||||
@@ -1911,10 +2003,13 @@ async function softApiStep(module, name, request, validate, evidenceBuilder = de
|
||||
async function httpRequest({ method = 'GET', path, token, headers = {}, body, responseType = 'json' }) {
|
||||
const requestHeaders = { Accept: 'application/json', 'X-Request-Id': `client-e2e-${randomUUID()}`, ...headers }
|
||||
if (token) requestHeaders.Authorization = `Bearer ${token}`
|
||||
let requestBody = body
|
||||
if (body != null && !(body instanceof FormData)) {
|
||||
const normalizedBody = path === '/api/auth/login' && body?.username && !body?.loginId
|
||||
? { ...body, loginId: body.username, username: undefined }
|
||||
: body
|
||||
let requestBody = normalizedBody
|
||||
if (normalizedBody != null && !(normalizedBody instanceof FormData)) {
|
||||
requestHeaders['Content-Type'] = 'application/json'
|
||||
requestBody = JSON.stringify(body)
|
||||
requestBody = JSON.stringify(normalizedBody)
|
||||
}
|
||||
const response = await fetch(`${gateway}${path}`, {
|
||||
method,
|
||||
|
||||
Reference in New Issue
Block a user