fix(support): stop faking refund approvals and ticket submissions in the support modal

This commit is contained in:
Yun Chan 2026-09-28 00:54:02 +09:00
parent e243e5282c
commit 819bc9d789
6 changed files with 580 additions and 355 deletions

View 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 })
})
})