storebound/tests/fastlane-traces.test.ts

76 lines
4.8 KiB
TypeScript

// 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, traceMessage } from '../cli/lib/fastlane-traces';
import { runScan } from '../cli/lib/scan';
function project(files: Record<string, string>): 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('scripts that call the store APIs or upload tools themselves, with the storebound way', () => {
const dir = project({
'scripts/play-upload.mjs': "const url = 'https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications/com.example.app/edits';\n",
'tools/asc.py': "BASE = 'https://api.appstoreconnect.apple.com/v1'\n",
'deploy/ios.sh': 'xcrun altool --upload-app -f app.ipa\n',
'package.json': '{"name":"an-app","scripts":{"ship":"node scripts/play-upload.mjs"}}',
});
const t = findFastlaneTraces(dir);
expect(t.map(x => `${x.file} ${x.store} ${x.via}`).sort()).toEqual(['deploy/ios.sh apple api', 'scripts/play-upload.mjs play api', 'tools/asc.py apple api']);
expect(traceMessage(t.find(x => x.store === 'play')!)).toContain('play_release_testing');
expect(traceMessage(t.find(x => x.file === 'deploy/ios.sh')!)).toStartWith('altool / Transporter upload in deploy/ios.sh:1');
});
test("storebound's own checkout is not reported", () => {
expect(findFastlaneTraces(project({ 'package.json': '{"name":"storebound"}', 'scripts/x.ts': "fetch('https://api.appstoreconnect.apple.com/v1/apps')\n" }))).toEqual([]);
});
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');
});
});
describe('direct store API traces stay short', () => {
test('comment lines are skipped and each file names a tool once', () => {
const dir = mkdtempSync(join(tmpdir(), 'sb-fl2-'));
mkdirSync(join(dir, 'tools'));
writeFileSync(join(dir, 'tools/ship.mjs'), "/**\n * 4. `xcrun altool --upload-package` uploads it\n */\n// altool needs the app id\nconst words = ['xcrun', 'altool', '--upload-package'];\nrun(['xcrun', 'altool', '--validate-app']);\n");
expect(findFastlaneTraces(dir)).toEqual([expect.objectContaining({ file: 'tools/ship.mjs', line: 5, via: 'api' })]);
});
});
describe('test files are not traces', () => {
test('a test that quotes altool is skipped', () => {
const dir = mkdtempSync(join(tmpdir(), 'sb-fl3-'));
mkdirSync(join(dir, 'tools'));
writeFileSync(join(dir, 'tools/ship.test.mjs'), "test('calls altool', () => { run(['xcrun', 'altool']); });\n");
expect(findFastlaneTraces(dir)).toEqual([]);
});
});