"""check_no_gms_signin.sh: no app may link Google's non-free sign-in stack. Each test writes a throwaway Flutter-shaped project (pubspec.lock, optional android/ gradle files) or a fake APK built with zipfile, runs the real script, and asserts on the exit code and the message. """ from __future__ import annotations import subprocess import zipfile from pathlib import Path SCRIPT = Path(__file__).resolve().parent.parent / "check_no_gms_signin.sh" # The package keys of kuhy's real kartka lock (web-only GIS button). KARTKA_KEYS = ( "google_account_token", "google_identity_services_web", "google_sign_in_platform_interface", "google_sign_in_web", ) def _lock(root: Path, *packages: str) -> Path: """Write a pubspec.lock under `root` with the given package keys.""" root.mkdir(parents=True, exist_ok=True) body = ["# Generated by pub", "packages:"] for name in ("collection", *packages): body += [ f" {name}:", ' dependency: "direct main"', " description:", f" name: {name}", " source: hosted", ' version: "1.0.0"', ] body += ["sdks:", ' dart: ">=3.0.0 <4.0.0"', ""] lock = root / "pubspec.lock" lock.write_text("\n".join(body), encoding="utf-8") return lock def _run(*args: str | Path) -> subprocess.CompletedProcess[str]: return subprocess.run( ["bash", str(SCRIPT), *map(str, args)], capture_output=True, text=True, check=False, ) def test_clean_lock_passes(tmp_path: Path) -> None: lock = _lock(tmp_path, "google_account_token", "http") assert _run(lock).returncode == 0 def test_google_sign_in_android_fails_and_names_the_fix(tmp_path: Path) -> None: lock = _lock(tmp_path, "google_sign_in_android") proc = _run(lock) assert proc.returncode == 1 assert str(lock) in proc.stderr assert "google_sign_in_android" in proc.stderr assert "google_account_token-v0.1.0" in proc.stderr assert "google_account_token/README.md" in proc.stderr def test_app_facing_google_sign_in_fails(tmp_path: Path) -> None: proc = _run(_lock(tmp_path, "google_sign_in")) assert proc.returncode == 1 assert "google_sign_in " in proc.stderr or "google_sign_in\n" in proc.stderr def test_kartka_style_web_only_passes(tmp_path: Path) -> None: assert _run(_lock(tmp_path, *KARTKA_KEYS)).returncode == 0 def test_web_pieces_fail_once_android_is_linked(tmp_path: Path) -> None: proc = _run(_lock(tmp_path, *KARTKA_KEYS, "google_sign_in_android")) assert proc.returncode == 1 assert "google_sign_in_web" in proc.stderr def test_gradle_play_services_auth_fails(tmp_path: Path) -> None: lock = _lock(tmp_path) gradle = tmp_path / "android" / "app" / "build.gradle.kts" gradle.parent.mkdir(parents=True) gradle.write_text( 'dependencies {\n implementation("com.google.gms:google-services:4")\n' ' implementation("com.google.android.gms:play-services-auth:21.3.0")\n}\n', encoding="utf-8", ) proc = _run(lock) assert proc.returncode == 1 assert str(gradle) in proc.stderr assert "play-services-auth" in proc.stderr def test_versions_toml_googleid_fails(tmp_path: Path) -> None: lock = _lock(tmp_path) toml = tmp_path / "android" / "gradle" / "libs.versions.toml" toml.parent.mkdir(parents=True) toml.write_text( 'googleid = "com.google.android.libraries.identity.googleid:googleid:1.1"\n', encoding="utf-8", ) assert _run(lock).returncode == 1 def test_firebase_google_services_plugin_passes(tmp_path: Path) -> None: lock = _lock(tmp_path) gradle = tmp_path / "android" / "build.gradle" gradle.parent.mkdir(parents=True) gradle.write_text( "// play-services-auth was removed 2026-11-02\n" "classpath 'com.google.gms:google-services:4.4.2'\n", encoding="utf-8", ) assert _run(lock).returncode == 0 def test_directory_mode_finds_nested_locks_and_prunes_build(tmp_path: Path) -> None: _lock(tmp_path / "app", "google_account_token") _lock(tmp_path / "app" / "build" / "x", "google_sign_in_android") _lock(tmp_path / ".dart_tool" / "y", "google_sign_in_android") # Agent worktrees are other checkouts, judged in their own repo. _lock(tmp_path / ".claude" / "worktrees" / "z", "google_sign_in_android") assert _run(tmp_path).returncode == 0 _lock(tmp_path / "other", "google_sign_in_ios") proc = _run(tmp_path) assert proc.returncode == 1 assert "google_sign_in_ios" in proc.stderr def test_missing_explicit_file_is_a_usage_error(tmp_path: Path) -> None: assert _run(tmp_path / "nope.lock").returncode == 2 CREDENTIALS_MARKER = "META-INF/androidx.credentials_credentials-play-services-auth.version" DEX_REF = b"\x00Lcom/google/android/gms/auth/api/signin/GoogleSignInAccount;\x00" * 2 def _apk(path: Path, dex: bytes = b"", markers: tuple[str, ...] = ()) -> Path: """Fake APK: dex strings plus library marker files AGP copies in.""" with zipfile.ZipFile(path, "w") as apk: apk.writestr("AndroidManifest.xml", b"\x03\x00") apk.writestr("classes.dex", b"dex\n035\x00" + dex) apk.writestr("classes2.dex", b"dex\n035\x00Lkotlin/Unit;\x00") for name in markers: apk.writestr(name, b"version=1") return path def test_apk_with_only_dex_references_passes(tmp_path: Path) -> None: """play-services-base references GoogleSignInAccount without linking auth.""" markers = ( "play-services-base.properties", "play-services-basement.properties", "play-services-tasks.properties", "play-services-fido.properties", ) proc = _run("--apk", _apk(tmp_path / "ok.apk", DEX_REF, markers)) assert proc.returncode == 0, proc.stderr def test_apk_without_markers_passes(tmp_path: Path) -> None: assert _run("--apk", _apk(tmp_path / "clean.apk")).returncode == 0 def test_apk_banned_markers_fail_and_are_named(tmp_path: Path) -> None: banned = ( "play-services-auth.properties", "googleid.properties", "play-services-auth-base.properties", "play-services-auth-api-phone.properties", "play-services-auth-blockstore.properties", "play-services-identity-credentials.properties", CREDENTIALS_MARKER, ) for marker in banned: apk = _apk(tmp_path / "bad.apk", DEX_REF, ("play-services-base.properties", marker)) proc = _run("--apk", apk) assert proc.returncode == 1, marker assert marker in proc.stderr assert "google_account_token" in proc.stderr def test_apk_nested_marker_name_is_not_a_root_marker(tmp_path: Path) -> None: """Only APK-root files count; a same-named file in a subdir is not AGP's.""" apk = _apk(tmp_path / "nested.apk", markers=("assets/play-services-auth.properties",)) assert _run("--apk", apk).returncode == 0 def test_apk_missing_file_is_a_usage_error(tmp_path: Path) -> None: assert _run("--apk", tmp_path / "nope.apk").returncode == 2