优化
This commit is contained in:
@@ -61,6 +61,8 @@ const report = {
|
||||
tenantId: null,
|
||||
membershipId: null,
|
||||
reviewId: null,
|
||||
businessLicenseQualificationId: null,
|
||||
businessLicenseDocumentUrl: null,
|
||||
productImageFileId: null,
|
||||
enterpriseAddressId: null,
|
||||
receivingWarehouseId: null,
|
||||
@@ -367,6 +369,8 @@ async function merchantAfterApprovalModule() {
|
||||
const businessLicense = arrayData(approvedQualifications.json.data)
|
||||
.find((item) => item.qualificationType === 'BUSINESS_LICENSE')
|
||||
assertPresent(businessLicense?.documentUrl, 'business license documentUrl')
|
||||
report.resources.businessLicenseQualificationId = businessLicense.id ?? businessLicense.qualificationId
|
||||
report.resources.businessLicenseDocumentUrl = businessLicense.documentUrl
|
||||
await apiStep('merchant-session', 'preview approved qualification file', {
|
||||
method: 'GET', path: apiPath(businessLicense.documentUrl), token: merchantToken, responseType: 'bytes',
|
||||
}, ({ status, bytes, headers }) => {
|
||||
@@ -524,6 +528,9 @@ async function rbacAndMemberModule() {
|
||||
.filter((item) => item.status === 'ACTIVE' && desiredCodes.includes(item.permissionCode))
|
||||
assertEqual(grantedPermissions.length, desiredCodes.length, 'order permission count')
|
||||
const permissionIds = grantedPermissions.map((item) => item.id)
|
||||
const orderListPermissionId = grantedPermissions
|
||||
.find((item) => item.permissionCode === 'merchant:order:list')?.id
|
||||
assertPresent(orderListPermissionId, 'merchant:order:list permission id')
|
||||
|
||||
const ownerPermissions = await apiStep('menu-permissions', 'load owner effective permissions', {
|
||||
method: 'GET', path: '/api/merchant/v1/iam/authorization/permissions', token: merchantToken,
|
||||
@@ -773,6 +780,55 @@ async function rbacAndMemberModule() {
|
||||
assertSetEqual(visibleClientMenus(arrayData(json.data)), ['account:profile:view', 'dashboard:view', 'order:first-leg:list'], 'visible Client menus')
|
||||
}, ({ json }) => ({ backendPermissions: json.data, visibleClientMenus: visibleClientMenus(arrayData(json.data)) }))
|
||||
|
||||
await apiStep('menu-permissions', 'remove order view permission from assigned role', {
|
||||
method: 'PUT', path: '/api/merchant/v1/iam/roles/permissions', token: merchantToken,
|
||||
body: { roleId, permissionIds: [orderListPermissionId] },
|
||||
}, ({ json }) => requireSuccess(json), () => ({ roleId, permissionCodes: [desiredCodes[0]] }))
|
||||
await softApiStep('menu-permissions', 'permission removal revokes stale member token', {
|
||||
method: 'GET', path: `/api/merchant/v1/orders/first-leg/${report.resources.orderIds[0]}`, token: memberToken,
|
||||
}, ({ json, status }) => {
|
||||
assertTrue([200, 401, 403].includes(status), `unexpected HTTP status ${status}`)
|
||||
assertTrue(['A0401', 'A0403'].includes(String(json?.code)), `expected A0401/A0403, actual=${json?.code}`)
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
|
||||
const reducedPermissionLogin = await apiStep('menu-permissions', 'member re-login after permission removal', {
|
||||
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(json.data?.accessToken, 'member accessToken')
|
||||
}, () => ({ username: memberUsername }))
|
||||
memberToken = reducedPermissionLogin.json.data.accessToken
|
||||
await apiStep('menu-permissions', 'permission removal is reflected after re-login', {
|
||||
method: 'GET', path: '/api/merchant/v1/iam/authorization/permissions', token: memberToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertSetEqual(arrayData(json.data), [desiredCodes[0]], 'reduced member permissions')
|
||||
}, ({ json }) => ({ backendPermissions: json.data }))
|
||||
await apiStep('menu-permissions', 'member without view permission cannot read order detail', {
|
||||
method: 'GET', path: `/api/merchant/v1/orders/first-leg/${report.resources.orderIds[0]}`, token: memberToken,
|
||||
}, ({ json, status }) => {
|
||||
assertTrue([200, 403].includes(status), `unexpected HTTP status ${status}`)
|
||||
assertEqual(String(json?.code), 'A0403', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code }))
|
||||
|
||||
await apiStep('menu-permissions', 'restore assigned role permissions', {
|
||||
method: 'PUT', path: '/api/merchant/v1/iam/roles/permissions', token: merchantToken,
|
||||
body: { roleId, permissionIds },
|
||||
}, ({ json }) => requireSuccess(json), () => ({ roleId, permissionCodes: desiredCodes }))
|
||||
const restoredPermissionLogin = await apiStep('menu-permissions', 'member re-login after permission restoration', {
|
||||
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(json.data?.accessToken, 'member accessToken')
|
||||
}, () => ({ username: memberUsername }))
|
||||
memberToken = restoredPermissionLogin.json.data.accessToken
|
||||
await apiStep('menu-permissions', 'restored permission allows order detail again', {
|
||||
method: 'GET', path: `/api/merchant/v1/orders/first-leg/${report.resources.orderIds[0]}`, token: memberToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertEqual(String(json.data?.order?.id), String(report.resources.orderIds[0]), 'order id')
|
||||
}, ({ json }) => ({ orderId: json.data.order.id }))
|
||||
|
||||
const editedMemberBody = {
|
||||
displayName: `已编辑订单成员-${marker}`.slice(0, 64),
|
||||
phone: memberBody.phone,
|
||||
@@ -811,6 +867,12 @@ async function rbacAndMemberModule() {
|
||||
requireSuccess(json)
|
||||
assertEqual(json.data?.status, 'DISABLED', 'member status')
|
||||
}, ({ json }) => ({ membershipId: json.data.membershipId, status: json.data.status }))
|
||||
await softApiStep('rbac-member', 'disabled member stale token is rejected', {
|
||||
method: 'GET', path: '/api/merchant/v1/orders/first-leg?page=1&size=20', token: memberToken,
|
||||
}, ({ json, status }) => {
|
||||
assertTrue([200, 401, 403].includes(status), `unexpected HTTP status ${status}`)
|
||||
assertTrue(['A0401', 'A0403'].includes(String(json?.code)), `expected A0401/A0403, actual=${json?.code}`)
|
||||
}, ({ json }) => ({ membershipId, rejectionCode: json?.code }))
|
||||
await apiStep('rbac-member', 'disabled member login is rejected', {
|
||||
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: resetMemberPassword },
|
||||
}, ({ json, status }) => {
|
||||
@@ -824,6 +886,17 @@ async function rbacAndMemberModule() {
|
||||
assertEqual(json.data?.status, 'ACTIVE', 'member status')
|
||||
}, ({ json }) => ({ membershipId: json.data.membershipId, status: json.data.status }))
|
||||
|
||||
const enabledMemberLogin = await apiStep('rbac-member', 'member re-login after account is enabled', {
|
||||
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: resetMemberPassword },
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(json.data?.accessToken, 'member accessToken')
|
||||
}, () => ({ username: memberUsername }))
|
||||
memberToken = enabledMemberLogin.json.data.accessToken
|
||||
await apiStep('rbac-member', 're-enabled member can list orders before roles are cleared', {
|
||||
method: 'GET', path: '/api/merchant/v1/orders/first-leg?page=1&size=20', token: memberToken,
|
||||
}, ({ json }) => requireSuccess(json), ({ json }) => ({ total: json.data?.total }))
|
||||
|
||||
await apiStep('rbac-member', 'clear enterprise member roles', {
|
||||
method: 'PUT', path: '/api/merchant/v1/iam/members/roles', token: merchantToken,
|
||||
body: { membershipId, roleIds: [] },
|
||||
@@ -882,6 +955,33 @@ async function orderLifecycleModule() {
|
||||
assertEqual(String(json?.code), 'A0400', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
|
||||
const emptyImageForm = new FormData()
|
||||
emptyImageForm.append('file', new Blob([], { type: 'image/png' }), `empty-product-${marker}.png`)
|
||||
await softApiStep('order-product-image', 'empty product image is rejected', {
|
||||
method: 'POST', path: '/api/merchant/v1/files/product-images', token: merchantToken, body: emptyImageForm,
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0400', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
|
||||
const forgedImageForm = new FormData()
|
||||
forgedImageForm.append('file', new Blob(['not-a-real-png'], { type: 'image/png' }), `forged-product-${marker}.png`)
|
||||
await softApiStep('order-product-image', 'forged product image signature is rejected', {
|
||||
method: 'POST', path: '/api/merchant/v1/files/product-images', token: merchantToken, body: forgedImageForm,
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0400', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
|
||||
const oversizedImageForm = new FormData()
|
||||
oversizedImageForm.append('file', new Blob([Buffer.alloc(5 * 1024 * 1024 + 1)], { type: 'image/png' }), `oversized-product-${marker}.png`)
|
||||
await softApiStep('order-product-image', 'product image over 5MB is rejected', {
|
||||
method: 'POST', path: '/api/merchant/v1/files/product-images', token: merchantToken, body: oversizedImageForm,
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0400', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
|
||||
await apiStep('order-lifecycle', 'download own product image', {
|
||||
method: 'GET', path: `/api/merchant/v1/files/product-images/${productImageFileId}`, token: merchantToken,
|
||||
responseType: 'bytes',
|
||||
@@ -925,6 +1025,18 @@ async function orderLifecycleModule() {
|
||||
name: 'invalid product image file ID is rejected',
|
||||
mutate(value) { value.cargoes[0].productImageFileId = -1 },
|
||||
},
|
||||
{
|
||||
name: 'zero package count weight and volume are rejected',
|
||||
mutate(value) {
|
||||
value.cargoes[0].packageCount = 0
|
||||
value.cargoes[0].grossWeightKg = 0
|
||||
value.cargoes[0].volumeCbm = 0
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'customer remark over 1000 characters is rejected',
|
||||
mutate(value) { value.customerRemark = 'x'.repeat(1001) },
|
||||
},
|
||||
]
|
||||
for (const testCase of invalidOrderCases) {
|
||||
const invalidPayload = structuredClone(payload)
|
||||
@@ -937,6 +1049,19 @@ async function orderLifecycleModule() {
|
||||
assertEqual(String(json?.code), 'A0422', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
}
|
||||
await softApiStep('order-validation', 'missing Idempotency-Key is rejected', {
|
||||
method: 'POST', path: '/api/merchant/v1/orders/first-leg', token: merchantToken, body: payload,
|
||||
}, ({ json, status }) => {
|
||||
assertTrue([200, 400].includes(status), `unexpected HTTP status ${status}`)
|
||||
assertEqual(String(json?.code), 'A0400', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
await softApiStep('order-validation', 'Idempotency-Key over 128 characters is rejected', {
|
||||
method: 'POST', path: '/api/merchant/v1/orders/first-leg', token: merchantToken,
|
||||
headers: { 'Idempotency-Key': 'x'.repeat(129) }, body: payload,
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0422', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
const createKey = newIdempotencyKey('approved-create')
|
||||
const created = await apiStep('order-lifecycle', 'create valid draft after approval', {
|
||||
method: 'POST', path: '/api/merchant/v1/orders/first-leg', token: merchantToken,
|
||||
@@ -1044,6 +1169,41 @@ async function orderLifecycleModule() {
|
||||
assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'submitted status-filtered order')
|
||||
}, ({ json }) => ({ total: json.data?.total, orderId, status: 'SUBMITTED' }))
|
||||
|
||||
await apiStep('order-filters', 'warehouse channel and destination filters contain submitted order', {
|
||||
method: 'GET',
|
||||
path: `/api/merchant/v1/orders/first-leg?page=1&size=50&receivingWarehouseId=${payload.receivingWarehouseId}`
|
||||
+ `&channelName=${encodeURIComponent(payload.channelName)}&destinationType=${payload.destinationType}`
|
||||
+ `&destinationWarehouseId=${payload.destinationWarehouseId}`,
|
||||
token: merchantToken,
|
||||
}, ({ json }) => {
|
||||
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,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'sender-phone-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,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertEqual(pageRecords(json.data).length, 0, 'unmatched order count')
|
||||
}, ({ json }) => ({ total: json.data?.total }))
|
||||
await softApiStep('order-validation', 'invalid order status filter is rejected', {
|
||||
method: 'GET', path: '/api/merchant/v1/orders/first-leg?page=1&size=20&status=UNKNOWN', token: merchantToken,
|
||||
}, ({ json, status }) => {
|
||||
assertTrue([200, 400].includes(status), `unexpected HTTP status ${status}`)
|
||||
assertEqual(String(json?.code), 'A0400', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
|
||||
await apiStep('order-boundary', 'unknown order detail is rejected', {
|
||||
method: 'GET', path: '/api/merchant/v1/orders/first-leg/1', token: merchantToken,
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0404', 'business code')
|
||||
}, ({ json }) => ({ orderId: 1, rejectionCode: json?.code }))
|
||||
|
||||
await softApiStep('order-boundary', 'submitted order cannot be edited by client', {
|
||||
method: 'PUT', path: `/api/merchant/v1/orders/first-leg/${orderId}`, token: merchantToken, body: editedPayload,
|
||||
}, ({ json, status }) => {
|
||||
@@ -1142,9 +1302,37 @@ async function isolationModule() {
|
||||
assertTrue([200, 403, 404].includes(status), `unexpected HTTP status ${status}`)
|
||||
assertTrue(['A0403', 'A0404'].includes(String(json?.code)), `expected A0403/A0404, actual=${json?.code}`)
|
||||
}, ({ json }) => ({ fileId: report.resources.productImageFileId, rejectionCode: json?.code }))
|
||||
await softApiStep('isolation', 'cross-client member detail access is rejected', {
|
||||
method: 'GET', path: `/api/merchant/v1/iam/members/${report.resources.testMembershipId}`, token: existingClientToken,
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertTrue(['A0403', 'A0404'].includes(String(json?.code)), `expected A0403/A0404, actual=${json?.code}`)
|
||||
}, ({ json }) => ({ membershipId: report.resources.testMembershipId, rejectionCode: json?.code }))
|
||||
await apiStep('isolation', 'existing client address list excludes new enterprise address', {
|
||||
method: 'GET', path: '/api/merchant/v1/iam/tenant/addresses', token: existingClientToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertTrue(!arrayData(json.data).some((item) => String(item.id) === String(report.resources.enterpriseAddressId)),
|
||||
'cross-client address must not be visible')
|
||||
}, ({ json }) => ({ addressCount: arrayData(json.data).length, excludedAddressId: report.resources.enterpriseAddressId }))
|
||||
await apiStep('isolation', 'existing client qualification list excludes new enterprise qualification', {
|
||||
method: 'GET', path: '/api/merchant/v1/iam/tenant/qualifications', token: existingClientToken,
|
||||
}, ({ json }) => {
|
||||
requireSuccess(json)
|
||||
assertTrue(!arrayData(json.data).some((item) => String(item.id ?? item.qualificationId)
|
||||
=== String(report.resources.businessLicenseQualificationId)), 'cross-client qualification must not be visible')
|
||||
}, ({ json }) => ({ qualificationCount: arrayData(json.data).length,
|
||||
excludedQualificationId: report.resources.businessLicenseQualificationId }))
|
||||
await softApiStep('isolation', 'cross-client qualification file access is rejected', {
|
||||
method: 'GET', path: apiPath(report.resources.businessLicenseDocumentUrl), token: existingClientToken,
|
||||
}, ({ json, status }) => {
|
||||
assertTrue([200, 403, 404].includes(status), `unexpected HTTP status ${status}`)
|
||||
assertTrue(['A0403', 'A0404'].includes(String(json?.code)), `expected A0403/A0404, actual=${json?.code}`)
|
||||
}, ({ json }) => ({ qualificationId: report.resources.businessLicenseQualificationId, rejectionCode: json?.code }))
|
||||
}
|
||||
|
||||
async function accountSecurityModule() {
|
||||
const consumedRefreshToken = merchantRefreshToken
|
||||
const rotated = await apiStep('account-security', 'rotate refresh token', {
|
||||
method: 'POST', path: '/api/auth/refresh', body: { refreshToken: merchantRefreshToken },
|
||||
}, ({ json }) => {
|
||||
@@ -1154,6 +1342,12 @@ async function accountSecurityModule() {
|
||||
}, () => ({ rotated: true }))
|
||||
merchantToken = rotated.json.data.accessToken
|
||||
merchantRefreshToken = rotated.json.data.refreshToken
|
||||
await apiStep('account-security', 'consumed refresh token cannot be replayed', {
|
||||
method: 'POST', path: '/api/auth/refresh', body: { refreshToken: consumedRefreshToken },
|
||||
}, ({ json, status }) => {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0401', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json?.code }))
|
||||
|
||||
temporaryOwnerPassword = `Temporary${randomUUID().replaceAll('-', '').slice(0, 14)}6!`
|
||||
await apiStep('account-security', 'change owner password to temporary value', {
|
||||
@@ -1204,6 +1398,9 @@ async function accountSecurityModule() {
|
||||
assertEqual(status, 200, 'HTTP status')
|
||||
assertEqual(String(json?.code), 'A0401', 'business code')
|
||||
}, ({ json }) => ({ rejectionCode: json.code }))
|
||||
await apiStep('account-security', 'repeated logout remains idempotent', {
|
||||
method: 'POST', path: '/api/auth/logout', body: { refreshToken: loggedOutRefreshToken },
|
||||
}, ({ json }) => requireSuccess(json), () => ({ repeated: true }))
|
||||
}
|
||||
|
||||
async function recoverOwnerPassword() {
|
||||
|
||||
Reference in New Issue
Block a user