This commit is contained in:
2026-08-14 10:09:46 +08:00
parent 0bc752a2be
commit 66089f9b99
31 changed files with 719 additions and 141 deletions
+240 -1
View File
@@ -70,6 +70,8 @@ const report = {
channelCode: null,
roleId: null,
testMembershipId: null,
notificationTemplateId: null,
notificationMessageIds: [],
orderIds: [],
},
steps: [],
@@ -92,6 +94,7 @@ try {
await rbacAndMemberModule()
await orderLifecycleModule()
await isolationModule()
await notificationInboxModule()
await accountSecurityModule()
report.success = report.steps.every((step) => step.passed)
} catch (error) {
@@ -547,7 +550,7 @@ async function rbacAndMemberModule() {
assertSetEqual(
visibleClientMenus(arrayData(ownerPermissions.json.data)),
['account:profile:view', 'company:member:list', 'company:permission:list', 'company:profile:view',
'company:role:list', 'dashboard:view', 'order:first-leg:list'],
'company:role:list', 'dashboard:view', 'notification:inbox:view', 'order:first-leg:list'],
'owner visible Client menus',
)
@@ -1340,6 +1343,240 @@ async function isolationModule() {
}, ({ json }) => ({ qualificationId: report.resources.businessLicenseQualificationId, rejectionCode: json?.code }))
}
async function notificationInboxModule() {
const module = 'notification-inbox'
const templateBase = '/api/platform/v1/notification-templates'
const inboxBase = '/api/merchant/v1/notifications'
const suffix = marker.replace(/[^0-9A-Za-z]/g, '').slice(-24).toUpperCase()
const templateCode = `CLIENT_INBOX_${suffix}`.slice(0, 64)
const category = 'CLIENT_ACCEPTANCE'
let template = null
try {
const createTemplate = await apiStep(module, 'platform creates client inbox template', {
method: 'POST', path: templateBase, token: platformToken,
body: {
templateCode,
templateName: `客户消息闭环-${marker}`.slice(0, 128),
category,
titleTemplate: '客户消息验收 {{sequence}}',
bodyTemplate: '客户 {{marker}} 的第 {{sequence}} 条真实站内消息。',
variableWhitelist: ['marker', 'sequence'],
},
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.id, 'notification template id')
assertEqual(json.data?.status, 'DRAFT', 'notification template status')
assertEqual(json.data?.channel, 'IN_APP', 'notification template channel')
}, ({ json }) => ({ templateId: json.data.id, templateCode: json.data.templateCode, status: json.data.status }))
template = createTemplate.json.data
report.resources.notificationTemplateId = template.id
const activateTemplate = await apiStep(module, 'platform activates client inbox template', {
method: 'PUT', path: `${templateBase}/${template.id}/status`, token: platformToken,
body: { targetStatus: 'ACTIVE', version: template.version },
}, ({ json }) => {
requireSuccess(json)
assertEqual(json.data?.status, 'ACTIVE', 'notification template status')
}, ({ json }) => ({ templateId: json.data.id, status: json.data.status, version: json.data.version }))
template = activateTemplate.json.data
await apiStep(module, 'client normalizes existing inbox messages to read', {
method: 'PUT', path: `${inboxBase}/read-all`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertTrue(Number(json.data?.updatedCount) >= 0, 'updatedCount must be non-negative')
}, ({ json }) => ({ updatedCount: json.data.updatedCount }))
await apiStep(module, 'client unread baseline is zero', {
method: 'GET', path: `${inboxBase}/unread-count`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(Number(json.data?.unreadCount), 0, 'unread baseline')
}, ({ json }) => ({ unreadCount: json.data.unreadCount }))
const sendMessage = async (sequence) => apiStep(module, `platform sends client message ${sequence}`, {
method: 'POST', path: '/api/platform/v1/notifications', token: platformToken,
body: {
tenantId: String(report.resources.tenantId),
templateCode,
recipientType: 'MEMBERSHIP',
recipientIds: [String(report.resources.membershipId)],
variables: { marker, sequence: String(sequence) },
scheduledAt: null,
idempotencyKey: newIdempotencyKey(`client-notification-${sequence}`),
sourceEventId: `client-e2e-notification-${sequence}-${marker}`.slice(0, 128),
},
}, ({ json }) => {
requireSuccess(json)
assertEqual(json.data?.status, 'SENT', 'notification task status')
assertEqual(Number(json.data?.recipientCount), 1, 'notification recipient count')
}, ({ json }) => ({ taskId: json.data.id, taskNo: json.data.taskNo, status: json.data.status, recipientCount: json.data.recipientCount }))
await sendMessage(1)
await apiStep(module, 'client unread count increments after first delivery', {
method: 'GET', path: `${inboxBase}/unread-count`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(Number(json.data?.unreadCount), 1, 'unread count after first delivery')
}, ({ json }) => ({ unreadCount: json.data.unreadCount }))
const firstUnreadPage = await apiStep(module, 'client filters first message as unread', {
method: 'GET', path: `${inboxBase}?page=1&size=20&status=UNREAD&category=${category}`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
const message = pageRecords(json.data).find((item) => item.title === `客户消息验收 1`)
assertPresent(message?.id, 'first unread notification id')
assertEqual(message.status, 'UNREAD', 'first notification status')
}, ({ json }) => ({ total: json.data.total, unreadIds: pageRecords(json.data).map((item) => item.id) }))
const firstMessage = pageRecords(firstUnreadPage.json.data).find((item) => item.title === '客户消息验收 1')
const firstMessageId = firstMessage.id
report.resources.notificationMessageIds.push(firstMessageId)
await apiStep(module, 'client reads first message detail while it is unread', {
method: 'GET', path: `${inboxBase}/${firstMessageId}`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(String(json.data?.id), String(firstMessageId), 'first notification id')
assertEqual(json.data?.status, 'UNREAD', 'first notification detail status')
assertTrue(String(json.data?.body).includes(marker), 'first notification body must contain marker')
}, ({ json }) => ({ messageId: json.data.id, status: json.data.status, category: json.data.category }))
if (existingClientToken) {
await apiStep(module, 'other client cannot read first message detail', {
method: 'GET', path: `${inboxBase}/${firstMessageId}`, token: existingClientToken,
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
assertTrue(['A0403', 'A0404'].includes(String(json?.code)), `expected A0403/A0404, actual=${json?.code}`)
}, ({ json }) => ({ messageId: firstMessageId, rejectionCode: json.code }))
} else {
recordSkipped(module, 'other client cannot read first message detail', 'Cross-client credentials were not supplied')
}
await apiStep(module, 'client marks first message read', {
method: 'PUT', path: `${inboxBase}/${firstMessageId}/read`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(json.data?.status, 'READ', 'first notification status after read')
assertPresent(json.data?.readAt, 'first notification readAt')
}, ({ json }) => ({ messageId: json.data.id, status: json.data.status, readAt: json.data.readAt }))
await apiStep(module, 'single-message read is idempotent', {
method: 'PUT', path: `${inboxBase}/${firstMessageId}/read`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(json.data?.status, 'READ', 'idempotent notification status')
}, ({ json }) => ({ messageId: json.data.id, status: json.data.status, readAt: json.data.readAt }))
await apiStep(module, 'client filters first message as read', {
method: 'GET', path: `${inboxBase}?page=1&size=20&status=READ&category=${category}`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertTrue(pageRecords(json.data).some((item) => String(item.id) === String(firstMessageId) && item.status === 'READ'),
'read filter must contain first notification')
}, ({ json }) => ({ total: json.data.total, readIds: pageRecords(json.data).map((item) => item.id) }))
await apiStep(module, 'client unread count returns to zero after single read', {
method: 'GET', path: `${inboxBase}/unread-count`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(Number(json.data?.unreadCount), 0, 'unread count after single read')
}, ({ json }) => ({ unreadCount: json.data.unreadCount }))
await sendMessage(2)
const secondUnreadPage = await apiStep(module, 'client receives second unread message', {
method: 'GET', path: `${inboxBase}?page=1&size=20&status=UNREAD&category=${category}`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
const message = pageRecords(json.data).find((item) => item.title === '客户消息验收 2')
assertPresent(message?.id, 'second unread notification id')
assertEqual(message.status, 'UNREAD', 'second notification status')
}, ({ json }) => ({ total: json.data.total, unreadIds: pageRecords(json.data).map((item) => item.id) }))
const secondMessage = pageRecords(secondUnreadPage.json.data).find((item) => item.title === '客户消息验收 2')
const secondMessageId = secondMessage.id
report.resources.notificationMessageIds.push(secondMessageId)
if (existingClientToken) {
await apiStep(module, 'other client cannot see acceptance messages in inbox', {
method: 'GET', path: `${inboxBase}?page=1&size=200&category=${category}`, token: existingClientToken,
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
if (['A0403', 'A0404'].includes(String(json?.code))) return
requireSuccess(json)
const foreignIds = new Set(pageRecords(json.data).map((item) => String(item.id)))
assertTrue(!foreignIds.has(String(firstMessageId)) && !foreignIds.has(String(secondMessageId)),
'other client inbox must exclude acceptance messages')
}, ({ json }) => ({ rejectionCode: json?.code === '200' ? null : json?.code,
visibleCount: json?.code === '200' ? pageRecords(json.data).length : null,
excludedIds: [firstMessageId, secondMessageId] }))
} else {
recordSkipped(module, 'other client cannot see acceptance messages in inbox', 'Cross-client credentials were not supplied')
}
await apiStep(module, 'client unread count increments after second delivery', {
method: 'GET', path: `${inboxBase}/unread-count`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(Number(json.data?.unreadCount), 1, 'unread count after second delivery')
}, ({ json }) => ({ unreadCount: json.data.unreadCount }))
await apiStep(module, 'client marks all messages read', {
method: 'PUT', path: `${inboxBase}/read-all`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(Number(json.data?.updatedCount), 1, 'read-all updatedCount')
}, ({ json }) => ({ updatedCount: json.data.updatedCount }))
await apiStep(module, 'second message detail is read after read-all', {
method: 'GET', path: `${inboxBase}/${secondMessageId}`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(json.data?.status, 'READ', 'second notification status after read-all')
assertPresent(json.data?.readAt, 'second notification readAt')
}, ({ json }) => ({ messageId: json.data.id, status: json.data.status, readAt: json.data.readAt }))
await apiStep(module, 'client unread count is zero after read-all', {
method: 'GET', path: `${inboxBase}/unread-count`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(Number(json.data?.unreadCount), 0, 'unread count after read-all')
}, ({ json }) => ({ unreadCount: json.data.unreadCount }))
} finally {
if (merchantToken && report.resources.notificationMessageIds.length > 0) {
await softApiStep(module, 'cleanup keeps acceptance messages read', {
method: 'PUT', path: `${inboxBase}/read-all`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertTrue(Number(json.data?.updatedCount) >= 0, 'cleanup updatedCount must be non-negative')
}, ({ json }) => ({ updatedCount: json?.data?.updatedCount }))
await softApiStep(module, 'cleanup leaves no unread acceptance messages', {
method: 'GET', path: `${inboxBase}/unread-count`, token: merchantToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(Number(json.data?.unreadCount), 0, 'cleanup unread count')
}, ({ json }) => ({ unreadCount: json?.data?.unreadCount }))
}
if (template?.status === 'ACTIVE') {
const disabled = await softApiStep(module, 'platform disables client inbox template', {
method: 'PUT', path: `${templateBase}/${template.id}/status`, token: platformToken,
body: { targetStatus: 'DISABLED', version: template.version },
}, ({ json }) => {
requireSuccess(json)
assertEqual(json.data?.status, 'DISABLED', 'notification template final status')
}, ({ json }) => ({ templateId: json?.data?.id, status: json?.data?.status, version: json?.data?.version }))
if (disabled?.json?.data) template = disabled.json.data
}
report.notificationInbox = {
templateId: report.resources.notificationTemplateId,
templateCode,
templateStatus: template?.status ?? null,
messageIds: [...report.resources.notificationMessageIds],
retainedAsRead: true,
}
}
}
async function accountSecurityModule() {
const consumedRefreshToken = merchantRefreshToken
const rotated = await apiStep('account-security', 'rotate refresh token', {
@@ -1830,6 +2067,7 @@ function requiredOwnerBackendPermissions() {
'merchant:member:edit', 'merchant:member:grant-role', 'merchant:member:status',
'merchant:member:reset-password', 'merchant:role:list', 'merchant:role:create',
'merchant:role:edit', 'merchant:role:status', 'merchant:role:delete', 'merchant:role:grant',
'merchant:notification:view', 'merchant:notification:read', 'merchant:notification:read-all',
]
}
@@ -1840,6 +2078,7 @@ function visibleClientMenus(backendPermissions) {
if (permissions.has('merchant:profile:view')) menus.push('company:profile:view')
if (permissions.has('merchant:member:list')) menus.push('company:member:list')
if (permissions.has('merchant:role:list')) menus.push('company:role:list', 'company:permission:list')
if (permissions.has('merchant:notification:view')) menus.push('notification:inbox:view')
return menus.sort()
}