// fastlane left in a project (Fastfile, CI calls to supply / deliver / pilot) is found and named (pitfall 96). import { describe, expect, test } from 'bun:test'; import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { findFastlaneTraces, traceLine } from '../cli/lib/fastlane-traces'; import { runScan } from '../cli/lib/scan'; function project(files: Record): string { const dir = mkdtempSync(join(tmpdir(), 'sb-fl-')); for (const [p, text] of Object.entries(files)) { mkdirSync(join(dir, p, '..'), { recursive: true }); writeFileSync(join(dir, p), text); } return dir; } describe('fastlane traces', () => { test('Fastfile actions, Appfile, and CI / script calls with file:line', () => { const dir = project({ 'android/fastlane/Fastfile': 'lane :ship do\n # supply in a comment is not a call\n supply(track: "internal")\nend\n', 'fastlane/Appfile': 'app_identifier("com.example.game")\n', 'Jenkinsfile': 'stage("upload") {\n sh "bundle exec fastlane supply --aab app.aab"\n}\n', '.github/workflows/ios.yml': 'steps:\n - run: fastlane pilot upload\n', 'scripts/release.sh': 'echo fastlane is fine to mention\n', }); const t = findFastlaneTraces(dir); expect(t).toContainEqual({ file: 'android/fastlane/Fastfile', line: 3, text: 'supply(track: "internal")', store: 'play' }); expect(t).toContainEqual({ file: 'fastlane/Appfile', line: 1, text: 'app_identifier("com.example.game")', store: 'both' }); expect(t).toContainEqual({ file: 'Jenkinsfile', line: 2, text: 'sh "bundle exec fastlane supply --aab app.aab"', store: 'play' }); expect(t).toContainEqual({ file: '.github/workflows/ios.yml', line: 2, text: '- run: fastlane pilot upload', store: 'apple' }); expect(t.some(x => x.file === 'scripts/release.sh')).toBe(false); expect(t.filter(x => x.file === 'android/fastlane/Fastfile')).toHaveLength(1); expect(traceLine(t.find(x => x.file === 'Jenkinsfile')!)).toBe('fastlane in Jenkinsfile:2: sh "bundle exec fastlane supply --aab app.aab"'); }); test('a project without fastlane has no traces', () => { expect(findFastlaneTraces(project({ 'package.json': '{"scripts":{"build":"vite build"}}' }))).toEqual([]); }); test('scan reports fastlane-upload with the evidence lines', async () => { const dir = project({ 'Jenkinsfile': 'sh "fastlane supply"\n' }); const { report } = await runScan(dir, {}); const f = report.findings.find(x => x.rule === 'fastlane-upload' && x.store === 'play'); expect(f?.evidence[0]).toMatchObject({ file: 'Jenkinsfile', line: 1 }); expect(f?.fix).toContain('storebound import fastlane'); }); });