fix(support): stop faking refund approvals and ticket submissions in the support modal
This commit is contained in:
parent
e243e5282c
commit
819bc9d789
6 changed files with 580 additions and 355 deletions
139
apps/desktop/tests/unit/support-contact-redteam-r1-27.test.ts
Normal file
139
apps/desktop/tests/unit/support-contact-redteam-r1-27.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// tests/unit/support-contact-redteam-r1-27.test.ts
|
||||
// 고객지원 모달이 가짜 환불 승인·가짜 티켓 접수를 표시하던 결함의 회귀 테스트.
|
||||
// - 모달 소스에 가짜 흐름(무조건 eligible, alert 환불, 랜덤 티켓 번호, 하드코딩 진단)이 없어야 한다.
|
||||
// - 문의는 mailto 초안으로만 나가고, 메인 프로세스가 실패를 돌려주면 성공으로 보지 않는다.
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
MAX_MAILTO_BODY_CHARS,
|
||||
SUPPORT_EMAIL,
|
||||
buildSupportMailto,
|
||||
createElectronSupportChannel,
|
||||
formatSupportDiagnostics,
|
||||
isSupportCategory,
|
||||
validateSupportDraft,
|
||||
type SupportSystemApi,
|
||||
} from '../../src/renderer/components/support/support-contact'
|
||||
|
||||
const SUPPORT_DIR = join(__dirname, '../../src/renderer/components/support')
|
||||
|
||||
function supportSources(): Array<{ name: string; text: string }> {
|
||||
return readdirSync(SUPPORT_DIR)
|
||||
.filter((name) => /\.(ts|tsx)$/.test(name))
|
||||
.map((name) => ({ name, text: readFileSync(join(SUPPORT_DIR, name), 'utf8') }))
|
||||
}
|
||||
|
||||
describe('SupportModal — 가짜 처리 흐름 제거', () => {
|
||||
it('환불 승인·티켓 번호·하드코딩 진단을 만들어내는 코드가 없다', () => {
|
||||
const forbidden: RegExp[] = [
|
||||
/\balert\s*\(/,
|
||||
/Math\.random/,
|
||||
/'eligible'/,
|
||||
/TCK-/,
|
||||
/RTX 3080/,
|
||||
/환불 승인/,
|
||||
/전액 환불 대상/,
|
||||
/성공적으로 접수/,
|
||||
/Payple/,
|
||||
]
|
||||
for (const { name, text } of supportSources()) {
|
||||
for (const pattern of forbidden) {
|
||||
expect(pattern.test(text), `${name} contains ${pattern}`).toBe(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('모달은 IO 포트를 통해 실제 시스템 API로 연결된다', () => {
|
||||
const modal = readFileSync(join(SUPPORT_DIR, 'SupportModal.tsx'), 'utf8')
|
||||
expect(modal).toContain('createElectronSupportChannel(window.electronAPI.system)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('support-contact 정책', () => {
|
||||
const diag = { appVersion: '1.9.0', platform: 'win32' }
|
||||
|
||||
it('제목·내용이 비면 검증에 실패한다', () => {
|
||||
expect(validateSupportDraft({ category: 'general', subject: ' ', details: '' })).toEqual({
|
||||
ok: false,
|
||||
missing: ['subject', 'details'],
|
||||
})
|
||||
expect(validateSupportDraft({ category: 'billing', subject: 'a', details: 'b' })).toEqual({ ok: true })
|
||||
})
|
||||
|
||||
it('mailto URL에 수신 주소·분류·실제 진단 정보를 인코딩해 담는다', () => {
|
||||
const url = buildSupportMailto({ category: 'billing', subject: '환불 & 문의', details: 'line1\nline2' }, diag)
|
||||
expect(url.startsWith(`mailto:${SUPPORT_EMAIL}?subject=`)).toBe(true)
|
||||
const params = new URLSearchParams(url.slice(url.indexOf('?') + 1))
|
||||
expect(params.get('subject')).toBe('[D3RO Voice][billing] 환불 & 문의')
|
||||
const body = params.get('body') ?? ''
|
||||
expect(body).toContain('line1\nline2')
|
||||
expect(body).toContain('Category: billing')
|
||||
expect(body).toContain('D3RO Voice 1.9.0 • win32')
|
||||
expect(url).not.toContain(' ')
|
||||
})
|
||||
|
||||
it('긴 본문은 잘라서 mailto 길이를 제한한다', () => {
|
||||
const url = buildSupportMailto({ category: 'general', subject: 's', details: 'x'.repeat(10_000) }, diag)
|
||||
const body = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('body') ?? ''
|
||||
expect(body.length).toBeLessThan(MAX_MAILTO_BODY_CHARS + 200)
|
||||
})
|
||||
|
||||
it('진단 정보를 모르면 unknown으로 표시한다 (가짜 하드웨어 문구 금지)', () => {
|
||||
expect(formatSupportDiagnostics({ appVersion: null, platform: null })).toBe('D3RO Voice unknown • unknown')
|
||||
})
|
||||
|
||||
it('분류 값 가드', () => {
|
||||
expect(isSupportCategory('billing')).toBe(true)
|
||||
expect(isSupportCategory('refund_auto')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createElectronSupportChannel', () => {
|
||||
function fakeApi(overrides: Partial<SupportSystemApi> = {}): SupportSystemApi {
|
||||
return {
|
||||
openExternal: vi.fn(async () => ({ success: true as const, data: undefined })),
|
||||
getVersion: vi.fn(async () => ({ success: true as const, data: '1.9.0' })),
|
||||
getPlatform: vi.fn(async () => ({ success: true as const, data: 'win32' })),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
it('openExternal 성공일 때만 true', async () => {
|
||||
const api = fakeApi()
|
||||
const channel = createElectronSupportChannel(api)
|
||||
await expect(channel.openMail('mailto:x@example.com')).resolves.toBe(true)
|
||||
expect(api.openExternal).toHaveBeenCalledWith({ url: 'mailto:x@example.com' })
|
||||
})
|
||||
|
||||
it('메인 프로세스가 거절하거나 예외가 나면 false (성공을 꾸며내지 않는다)', async () => {
|
||||
const rejected = createElectronSupportChannel(
|
||||
fakeApi({
|
||||
openExternal: async () => ({ success: false as const, error: { code: 1, message: 'nope' } }),
|
||||
}),
|
||||
)
|
||||
await expect(rejected.openMail('mailto:x@example.com')).resolves.toBe(false)
|
||||
|
||||
const thrown = createElectronSupportChannel(
|
||||
fakeApi({
|
||||
openExternal: async () => {
|
||||
throw new Error('ipc down')
|
||||
},
|
||||
}),
|
||||
)
|
||||
await expect(thrown.openMail('mailto:x@example.com')).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('진단 조회 실패는 null로 둔다', async () => {
|
||||
const channel = createElectronSupportChannel(
|
||||
fakeApi({
|
||||
getVersion: async () => {
|
||||
throw new Error('boom')
|
||||
},
|
||||
getPlatform: async () => ({ success: false as const, error: { code: 1, message: 'x' } }),
|
||||
}),
|
||||
)
|
||||
await expect(channel.getDiagnostics()).resolves.toEqual({ appVersion: null, platform: null })
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue