新增 Debug EML 上传调试页面

This commit is contained in:
andy
2026-07-09 16:47:39 +08:00
parent 0727dfb8d0
commit 7100612442
14 changed files with 1269 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DebugEmlUploadError, uploadDebugEmlSuperAgentRun } from '@/services/debugEmlService'
const jsonHeaders = {
headers: {
get: (name: string) => (name.toLowerCase() === 'content-type' ? 'application/json' : null),
},
}
function mockJsonResponse(payload: unknown, status = 201): Response {
return {
ok: status >= 200 && status < 300,
status,
json: async () => payload,
text: async () => JSON.stringify(payload),
...jsonHeaders,
} as Response
}
describe('debugEmlService', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('uploads an eml file with FormData and debug key header', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
mockJsonResponse({
debug_run_id: '90001',
source_message_id: '30001',
source_provider: 'DEBUG_EML_UPLOAD',
external_message_id: 'debug-eml-run-90001-abcdef123456',
external_conversation_id: 'debug-thread-90001',
original_eml_oss_url: 'https://oss.example/raw.eml',
original_eml_sha256: 'sha256',
uploaded_media: [],
html_body_with_oss_urls: '<strong>raw</strong>',
html_body_sanitized: '<strong>safe</strong>',
html_sanitize_required: true,
html_render_mode: 'SANITIZED_HTML',
agentbus_like_payload: { schema_version: 'debug-eml-upload-v1' },
superagent_session_id: 'session-1',
superagent_run_id: 'run-1',
superagent_raw_answer: '{"ok":true}',
superagent_parsed_json: { ok: true },
warnings: [],
status: 'SUPERAGENT_SUCCEEDED',
}),
)
const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })
const result = await uploadDebugEmlSuperAgentRun({
hotelId: 'HOTEL-TEST',
debugUploadKey: ' manual-debug-key ',
runLabel: 'frontend-smoke',
file,
})
expect(fetchMock).toHaveBeenCalledTimes(1)
const [url, init] = fetchMock.mock.calls[0]!
expect(url).toBe('/api/system/debug/eml-superagent-runs')
expect(init).toMatchObject({
method: 'POST',
headers: {
Accept: 'application/json',
'X-TH-Hotel-Debug-Upload-Key': 'manual-debug-key',
},
})
expect(init?.headers).not.toHaveProperty('Content-Type')
expect(init?.body).toBeInstanceOf(FormData)
const formData = init?.body as FormData
expect(formData.get('hotel_id')).toBe('HOTEL-TEST')
expect(formData.get('run_label')).toBe('frontend-smoke')
expect(formData.get('file')).toBe(file)
expect(result.debug_run_id).toBe('90001')
})
it('throws a typed safe error for backend error_code responses', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
mockJsonResponse(
{
error_code: 'DEBUG_UPLOAD_KEY_INVALID',
message: 'Debug 上传访问口令缺失或错误。',
},
401,
),
)
const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })
await expect(
uploadDebugEmlSuperAgentRun({
hotelId: 'HOTEL-TEST',
debugUploadKey: 'wrong-key',
file,
}),
).rejects.toMatchObject({
status: 401,
errorCode: 'DEBUG_UPLOAD_KEY_INVALID',
message: 'Debug 上传访问口令缺失或错误。',
} satisfies Partial<DebugEmlUploadError>)
})
})

View File

@@ -0,0 +1,150 @@
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import zhCN from '@/i18n/locales/zh-CN'
import { DebugEmlUploadError } from '@/services/debugEmlService'
import DebugEmlSuperAgentRunView from '@/views/debug/DebugEmlSuperAgentRunView.vue'
vi.mock('@/services/debugEmlService', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/services/debugEmlService')>()
return {
...actual,
uploadDebugEmlSuperAgentRun: vi.fn(),
}
})
const service = await import('@/services/debugEmlService')
function mountView() {
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
messages: {
'zh-CN': zhCN,
},
})
return mount(DebugEmlSuperAgentRunView, {
global: {
plugins: [i18n],
},
})
}
function createResult() {
return {
debug_run_id: '90001',
source_message_id: '30001',
source_provider: 'DEBUG_EML_UPLOAD',
external_message_id: 'debug-eml-run-90001-abcdef123456',
external_conversation_id: 'debug-thread-90001',
original_eml_oss_url: 'https://oss.example/raw.eml',
original_eml_sha256: 'sha256',
uploaded_media: [
{
media_type: 'ORIGINAL_EMAIL',
file_name: 'booking.eml',
content_type: 'message/rfc822',
size_bytes: 1200,
external_url: 'https://oss.example/raw.eml',
external_media_id: 'raw',
object_key: 'debug/eml/raw/booking.eml',
},
],
html_body_with_oss_urls: '<script>alert("raw")</script><strong onclick="bad()">原始 HTML</strong>',
html_body_sanitized: '<strong>安全正文</strong>',
html_sanitize_required: true,
html_render_mode: 'SANITIZED_HTML',
agentbus_like_payload: {
schema_version: 'debug-eml-upload-v1',
source: {
original_message_id: '<original@example.test>',
},
},
superagent_session_id: 'session-1',
superagent_run_id: 'run-1',
superagent_raw_answer: '{"ok":true}',
superagent_parsed_json: { ok: true },
warnings: ['SuperAgent 返回非 JSON 时会展示原始回答'],
status: 'SUPERAGENT_SUCCEEDED',
}
}
async function chooseFile(wrapper: ReturnType<typeof mountView>) {
const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })
const input = wrapper.find('input[type="file"]')
Object.defineProperty(input.element, 'files', {
value: [file],
configurable: true,
})
await input.trigger('change')
return file
}
describe('DebugEmlSuperAgentRunView', () => {
beforeEach(() => {
vi.mocked(service.uploadDebugEmlSuperAgentRun).mockReset()
})
it('submits the selected eml file and renders sanitized debug result', async () => {
vi.mocked(service.uploadDebugEmlSuperAgentRun).mockResolvedValue(createResult())
const wrapper = mountView()
await wrapper.find('input[name="hotel_id"]').setValue('HOTEL-TEST')
await wrapper.find('input[name="debug_upload_key"]').setValue('manual-debug-key')
await wrapper.find('input[name="run_label"]').setValue('frontend-smoke')
const file = await chooseFile(wrapper)
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(service.uploadDebugEmlSuperAgentRun).toHaveBeenCalledWith({
hotelId: 'HOTEL-TEST',
debugUploadKey: 'manual-debug-key',
runLabel: 'frontend-smoke',
file,
})
expect(wrapper.text()).toContain('90001')
expect(wrapper.text()).toContain('30001')
expect(wrapper.text()).toContain('DEBUG_EML_UPLOAD')
expect(wrapper.find('.debug-eml-html-preview').html()).toContain('<strong>安全正文</strong>')
expect(wrapper.find('.debug-eml-html-preview').html()).not.toContain('<script>')
expect(wrapper.find('.debug-eml-html-preview').html()).not.toContain('onclick')
expect(wrapper.text()).toContain('booking.eml')
expect(wrapper.text()).toContain('"schema_version": "debug-eml-upload-v1"')
expect(wrapper.text()).toContain('"ok": true')
})
it('does not render raw html with oss urls by default', async () => {
vi.mocked(service.uploadDebugEmlSuperAgentRun).mockResolvedValue(createResult())
const wrapper = mountView()
await wrapper.find('input[name="hotel_id"]').setValue('HOTEL-TEST')
await wrapper.find('input[name="debug_upload_key"]').setValue('manual-debug-key')
await chooseFile(wrapper)
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(wrapper.find('.debug-eml-html-preview').html()).not.toContain('原始 HTML')
expect(wrapper.find('details.debug-eml-raw-html').text()).toContain('原始 HTML')
})
it('shows mapped error_code messages without exposing the debug key', async () => {
vi.mocked(service.uploadDebugEmlSuperAgentRun).mockRejectedValue(
new DebugEmlUploadError('Debug 上传访问口令缺失或错误。', 401, 'DEBUG_UPLOAD_KEY_INVALID', {
error_code: 'DEBUG_UPLOAD_KEY_INVALID',
}),
)
const wrapper = mountView()
await wrapper.find('input[name="hotel_id"]').setValue('HOTEL-TEST')
await wrapper.find('input[name="debug_upload_key"]').setValue('manual-debug-key')
await chooseFile(wrapper)
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(wrapper.text()).toContain('DEBUG_UPLOAD_KEY_INVALID')
expect(wrapper.text()).toContain('调试上传口令缺失或错误')
expect(wrapper.text()).not.toContain('manual-debug-key')
})
})

View File

@@ -17,6 +17,26 @@ describe('reservation i18n locales', () => {
expect(thTH.nav.taskQueue).toBe('คิวงาน')
expect(thTH.taskList.viewConversation).toBe('ดูเธรดอีเมล')
})
it('covers Debug EML backend error codes in all locales', () => {
const expectedErrorCodes = [
'REQUEST_FIELD_REQUIRED',
'EML_FILE_REQUIRED',
'EML_FILE_TOO_LARGE',
'INVALID_FILE_TYPE',
'DEBUG_UPLOAD_KEY_INVALID',
'EML_PARSE_FAILED',
'OSS_UPLOAD_FAILED',
'SUPERAGENT_OPEN_API_FAILED',
'DEBUG_EML_RUN_FAILED',
] satisfies Array<keyof typeof zhCN.debugEml.errors>
for (const locale of [zhCN, enUS, thTH]) {
for (const errorCode of expectedErrorCodes) {
expect(locale.debugEml.errors[errorCode]).toBeTruthy()
}
}
})
})
function collectKeyPaths(value: unknown, prefix = ''): string[] {

View File

@@ -12,4 +12,8 @@ describe('reservation router', () => {
'reservation-source-message-conversation',
)
})
it('exposes the hidden Debug EML route without adding it to the business menu', () => {
expect(router.resolve('/debug/eml-superagent').name).toBe('debug-eml-superagent')
})
})