import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; void main() { group('certified macOS build scripts', () { test('secret setup stores repeatable encrypted CI inputs', () { final script = _readScript('scripts/setup-appsok-ci-secrets.sh'); expect(script, contains(r'age-keygen -o "$AGE_KEY"')); expect( script, contains(r'SOPS_PATH_REGEX="$(SECRET_FILE="$SECRET_FILE"'), ); expect( script, contains(r'print(re.escape(os.environ["SECRET_FILE"]) + "$")'), ); expect(script, contains(r'path_regex: $SOPS_PATH_REGEX')); expect( script, contains(r'sops --filename-override "$SECRET_FILE" -e "$tmp"'), ); expect(script, contains(r'SOPS_AGE_KEY_FILE="$AGE_KEY" sops -d')); expect(script, contains('security unlock-keychain')); expect(script, contains('security set-key-partition-list')); expect(script, contains('xcrun notarytool store-credentials')); expect(script, contains('xcrun notarytool history --keychain-profile')); expect(script, contains('prompt_secret()')); expect( script, contains('prompt_secret "Mac login/keychain password" KEYCHAIN_PASSWORD'), ); expect( script, contains('prompt_secret "App-specific password" APP_SPECIFIC_PASSWORD'), ); expect( script, contains('prompt_secret "Jenkins API token" JENKINS_API_TOKEN'), ); expect( script, isNot( contains('prompt_plain "Mac login/keychain password" KEYCHAIN_PASSWORD'), ), ); expect( script, isNot( contains('prompt_plain "App-specific password" APP_SPECIFIC_PASSWORD'), ), ); expect( script, isNot(contains('prompt_plain "Jenkins API token" JENKINS_API_TOKEN')), ); expect(script, contains('prompt_plain "Jenkins URL" JENKINS_URL')); expect( script, contains('prompt_plain "Jenkins username" JENKINS_USERNAME'), ); expect(script, contains('"jenkins_url": os.environ["JENKINS_URL"]')); expect( script, contains('"jenkins_username": os.environ["JENKINS_USERNAME"]'), ); expect( script, contains('"jenkins_api_token": os.environ["JENKINS_API_TOKEN"]'), ); }); test('certified build keeps Flutter gates before release packaging', () { final script = _readScript('scripts/build-certified-macos.sh'); _expectOrder(script, 'flutter pub get', 'flutter analyze'); _expectOrder(script, 'flutter analyze', 'flutter test'); _expectOrder(script, 'flutter test', 'flutter build macos'); _expectOrder(script, 'flutter build macos', 'codesign --force'); }); test('certified build signs bundled adb before app notarization', () { final script = _readScript('scripts/build-certified-macos.sh'); expect( script, contains(r'ADB_PATH="$APP_PATH/Contents/Resources/adb-runtime/adb"'), ); expect( script, contains( r'codesign --force --options runtime --timestamp --sign "$IDENTITY" "$ADB_PATH"', ), ); expect( script, contains(r'codesign --verify --strict --verbose=2 "$ADB_PATH"'), ); _expectOrder( script, r'codesign --force --options runtime --timestamp --sign "$IDENTITY" "$ADB_PATH"', '--entitlements macos/Runner/Release.entitlements', ); }); test('certified build notarizes, staples, assesses, and zips artifact', () { final script = _readScript('scripts/build-certified-macos.sh'); expect( script, contains( r'xcrun notarytool submit "$NOTARY_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait', ), ); expect(script, contains(r'xcrun stapler staple -v "$APP_PATH"')); expect( script, contains(r'spctl --assess --type execute --verbose=4 "$APP_PATH"'), ); expect( script, contains( 'FINAL_ZIP="build/macos/Build/Products/Release/AppSok-certified.zip"', ), ); expect(script, contains(r'FINAL_SHA256="$FINAL_ZIP.sha256"')); expect( script, contains(r'shasum -a 256 "$FINAL_ZIP" | tee "$FINAL_SHA256"'), ); expect(script, contains(r'ls -lh "$FINAL_SHA256"')); _expectOrder( script, r'xcrun notarytool submit "$NOTARY_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait', r'xcrun stapler staple -v "$APP_PATH"', ); _expectOrder( script, r'xcrun stapler staple -v "$APP_PATH"', r'spctl --assess --type execute --verbose=4 "$APP_PATH"', ); _expectOrder( script, r'spctl --assess --type execute --verbose=4 "$APP_PATH"', r'ditto -c -k --keepParent "$APP_PATH" "$FINAL_ZIP"', ); _expectOrder( script, r'ditto -c -k --keepParent "$APP_PATH" "$FINAL_ZIP"', r'shasum -a 256 "$FINAL_ZIP" | tee "$FINAL_SHA256"', ); }); test('jenkins job upsert configures thin certified job', () { final script = _readScript('scripts/upsert-jenkins-certified-job.sh'); expect(script, contains('appsok-macos-certified')); expect(script, contains('macbook-ultra')); expect(script, contains('*/main')); expect(script, contains('')); expect(script, contains('com.cloudbees.jenkins.GitHubPushTrigger')); expect(script, isNot(contains('hudson.triggers.SCMTrigger'))); expect(script, contains('./scripts/build-certified-macos.sh')); expect( script, contains('build/macos/Build/Products/Release/AppSok-certified.zip'), ); expect( script, contains( 'build/macos/Build/Products/Release/AppSok-certified.zip.sha256', ), ); expect(script, contains('10')); expect(script, contains('10')); }); test('jenkins job upsert keeps credentials out of generated config', () { final script = _readScript('scripts/upsert-jenkins-certified-job.sh'); final setupScript = _readScript('scripts/setup-appsok-ci-secrets.sh'); expect(script, contains('sops -d')); expect(script, isNot(contains('jenkins_api_token='))); expect(script, isNot(contains(r'-u "$JENKINS_USER:$JENKINS_TOKEN"'))); expect( script, isNot(contains(r'python3 - "$JENKINS_USER" "$JENKINS_TOKEN"')), ); expect(script, contains(r'CURL_CONFIG="$(mktemp)"')); expect(script, contains(r'--config "$CURL_CONFIG"')); expect(script, contains("trap 'rm -f \"\$CURL_CONFIG\"' EXIT")); expect(script, contains('APPLY=false')); expect(script, contains('DRY_RUN=true')); expect(script, contains('--apply')); expect(script, contains('--dry-run')); for (final key in [ 'jenkins_url', 'jenkins_username', 'jenkins_api_token', ]) { expect(script, contains('read_secret')); expect(script, contains(key)); expect(setupScript, contains('"$key": os.environ')); } }); }); } String _readScript(String path) => File(path).readAsStringSync(); void _expectOrder(String source, String before, String after) { final beforeIndex = _requiredIndex(source, before); final afterIndex = _requiredIndex(source, after); expect( beforeIndex, lessThan(afterIndex), reason: '"$before" must appear before "$after"', ); } int _requiredIndex(String source, String needle) { final index = source.indexOf(needle); expect(index, greaterThanOrEqualTo(0), reason: 'missing "$needle"'); return index; }