// 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 { 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 }) }) })