// storebound report: the shared renderer (grouping, order, links, escaping, masking) and the command (file written, // --no-open, --md, --json, validate skipped without credentials, no fixture secret in any output). import { describe, expect, test } from 'bun:test'; import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { buildReport, maskSecrets, renderHtml, renderMarkdown, summaryText } from '../cli/lib/report'; import { shouldOpen } from '../cli/report'; const ROOT = join(import.meta.dir, '..'); const PEM = '-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgFIXTURE000000000000\n-----END PRIVATE KEY-----'; const SECRETS = { keyId: 'K2X9Q7W4Z1', issuer: '6f1c2a3b-4d5e-4f60-8a9b-0c1d2e3f4a5b', demoUser: 'reviewer.fixture@corp.invalid', demoPass: 'Fixture-Pass-9931', contact: 'owner.fixture@corp.invalid' }; const fixture = () => buildReport({ app: { name: 'Fixture ', version: '1.2.0', build: '42' }, stores: ['apple', 'play'], storeRead: { apple: true, play: 'no Play service account' }, generatedAt: '2026-10-18T10:00:00Z', storebound: '0.0.0-test', secrets: [SECRETS.demoPass], scan: { findings: [ { rule: 'att', guideline: '5.1.2', store: 'apple', severity: 'medium', title: 'Tracking without ATT', evidence: [{ file: 'ios/App/Info.plist', line: 12 }], fix: 'Add the ATT prompt', skill: 'privacy-manifest' }, { rule: 'demo-account', guideline: '2.1', store: 'apple', severity: 'blocker', title: 'No demo account ', evidence: [], fix: `Use ${SECRETS.demoUser} / ${SECRETS.demoPass}`, url: 'https://appstoreconnect.apple.com/apps/123/distribution/reviewsubmissions' }, { rule: 'privacy-labels', guideline: 'Data safety', store: 'play', severity: 'high', title: 'Labels miss DEVICE_ID', evidence: [{ file: 'android/app/build.gradle', line: 30 }], fix: 'Declare it', url: 'javascript:alert(1)' }, { rule: 'privacy-labels', guideline: 'Data safety', store: 'apple', severity: 'high', title: 'Labels miss DEVICE_ID', evidence: [{ file: 'android/app/build.gradle', line: 30 }], fix: 'Declare it' }, { rule: 'accepted-one', guideline: 'x', store: 'play', severity: 'blocker', title: 'Accepted by the owner', evidence: [], fix: 'none', accepted: 'fine' }, { rule: 'obfuscator', guideline: '2.3.1', store: 'play', severity: 'low', title: 'Obfuscator found', evidence: [], fix: 'Mention it' }, ], declarations: { items: [{ area: 'dataSafety', what: 'encryptedInTransit', value: '?', status: 'ask' }, { area: 'dataSafety', what: 'PSL_NAME', value: 'yes', status: 'detected' }] }, }, check: { ok: false, problems: [`key ${SECRETS.keyId} issuer ${SECRETS.issuer} in ${SECRETS.contact}`], warnings: ['play listing has no section for de-DE'], hints: [], items: [{ what: 'appstore listing', detail: '1 locales' }] }, validate: { blockers: 1, findings: [{ store: 'apple', level: 'blocker', code: 'build', msg: 'No build attached', fix: 'storebound apple upload' }, { store: 'apple', level: 'ok', code: 'name', msg: 'Name set' }] }, }); describe('report renderer', () => { test('groups by store, merges both stores, orders by severity, drops accepted findings', () => { const r = fixture(); const both = r.findings.filter(f => f.store === 'both'); expect(both.map(f => f.title)).toContain('Labels miss DEVICE_ID'); expect(r.findings.some(f => f.title === 'Accepted by the owner')).toBe(false); const apple = r.findings.filter(f => f.store === 'apple').map(f => f.level); expect(apple).toEqual([...apple].sort((a, b) => ['blocker', 'warn', 'info', 'ok'].indexOf(a) - ['blocker', 'warn', 'info', 'ok'].indexOf(b))); expect(r.findings[0]!.level).toBe('blocker'); expect(r.summary.apple.blockers).toBe(2); expect(r.summary.play.warnings).toBe(1); expect(r.questions.map(q => q.text)).toContain('encryptedInTransit: ?'); expect(summaryText(r)).toContain('App Store: 2 blockers'); expect(summaryText(r)).toContain('Google Play: Not checked (no credentials)'); }); test('html: both store columns, console links, evidence, no script, escaped text, unsafe urls dropped', () => { const html = renderHtml(fixture()); expect(html).toContain('

App Store

'); expect(html).toContain('

Google Play

'); expect(html).toContain('href="https://appstoreconnect.apple.com/apps/123/distribution/reviewsubmissions"'); expect(html).toContain('Open in App Store Connect'); expect(html).toContain('ios/App/Info.plist:12'); expect(html).toContain('5.1.2'); expect(html).toContain('Questions only you can answer'); expect(html).toContain('prefers-color-scheme:dark'); expect(html).toContain('@media print'); expect(html).not.toContain('