新增 Debug EML 上传调试页面
This commit is contained in:
102
client/src/tests/debugEmlService.spec.ts
Normal file
102
client/src/tests/debugEmlService.spec.ts
Normal 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>)
|
||||
})
|
||||
})
|
||||
150
client/src/tests/debugEmlView.spec.ts
Normal file
150
client/src/tests/debugEmlView.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user