geeViz.eeAuth.tests.test_discovery_and_modes

Comprehensive tests for credential auto-discovery and Map.view mode selection. Covers the full matrix:

  • Credential types: SA via path env, SA via b64 env, OAuth refresh token (authorized_user file), bare refresh_token dict

  • Quantities: zero, one, multiple

  • Auth modes: auto, proxy, legacy

  • Surfaces: Python SDK init (via ensure_started), Map.view() URL builder

Discovery is exercised against a fake-filesystem + env-patched setup so no real GCP credentials are required.

Functions

test_detect_oauth_project_filters_legacy_from_env_vars_too()

If a user has GOOGLE_CLOUD_PROJECT=earthengine-legacy set (somehow — copy-paste mistake, misguided tutorial), filter that out too — never let the placeholder propagate.

test_detect_oauth_project_filters_out_earthengine_legacy_placeholder()

ee.data._get_state().cloud_api_user_project defaults to the literal string earthengine-legacy BEFORE any ee.Initialize( project=...) runs.

test_detect_oauth_project_filters_sdk_projects()

EE's shared OAuth-client project numbers (e.g. 764086051850) are listed in ee.oauth.SDK_PROJECTS.

test_diagnose_ee_credentials_reads_refresh_token_and_project()

_diagnose_ee_credentials must surface what's in the EE credentials JSON without interpreting it — refresh_token presence and the project field both go through verbatim.

test_diagnose_reports_missing_refresh_token()

When the EE credentials file has no refresh_token field (user deleted it), the diagnosis must say so — that's the signal robust_init uses to NOT silently fall through to ADC.

test_discover_backfills_project_for_oauth_from_adc()

ADC's quota_project_id is the source EE itself uses, so it should take priority over env vars / gcloud-config when neither ee.data state nor an explicit env var is set.

test_discover_backfills_project_for_oauth_from_ee_state()

When ee.Initialize(project='X') has been called, discovery must pick X up as the project for OAuth entries — otherwise the proxy forwards EE calls without x-goog-user-project and the upstream falls back to earthengine-legacy which personal Google accounts can't use → 403 'project not found or deleted'.

test_discover_backfills_project_for_oauth_from_env_var()

Falls back to GOOGLE_CLOUD_PROJECT when ee.data state AND ADC don't have a project (i.e. ee.Initialize never called and the user hasn't run gcloud auth application-default set-quota-project).

test_discover_does_not_overwrite_sa_project_with_oauth_hint()

The project backfill must NOT clobber an SA's project_id that came from its own JSON.

test_discover_falls_back_to_gcloud_config_when_no_other_hint()

When ee.data state is empty, ADC has no quota_project_id, AND env vars are unset, fall back to gcloud config get-value project.

test_discover_finds_adc_path()

Discovers GOOGLE_APPLICATION_CREDENTIALS pointing at an SA JSON.

test_discover_finds_ee_persistent_oauth()

Discovers ~/.config/earthengine/credentials style refresh token.

test_discover_finds_env_default_b64()

Discovers GEE_SERVICE_ACCOUNT_B64 base64-encoded SA.

test_discover_finds_gcloud_adc_default_file()

When gcloud auth application-default login has run, its well-known credentials file at e.g. ~/AppData/Roaming/gcloud/application_default_credentials.json must be picked up so Map.view() can spin up the proxy in ADC-only environments.

test_discover_finds_multiple_simultaneously()

All four sources at once → discovery picks them all up.

test_discover_finds_per_tenant_envs()

Discovers GEE_<NAME>_SERVICE_ACCOUNT for arbitrary names.

test_discover_overwrite_replaces_existing()

overwrite=True replaces existing names.

test_discover_skips_existing_names_by_default()

Already-registered credentials are not overwritten.

test_discover_with_nothing_to_find_returns_empty_list()

test_ensure_started_auto_discovers_and_would_start_with_creds()

When credentials are discoverable, auto mode starts the proxy.

test_ensure_started_idempotent()

Calling ensure_started twice is safe; second returns same state.

test_ensure_started_mode_auto_falls_back_silently_with_no_creds()

mode='auto' returns empty proxy_url so caller falls back to legacy.

test_ensure_started_mode_invalid_raises()

test_ensure_started_mode_legacy_does_nothing()

mode='legacy' must not touch state — Map.view() falls through.

test_ensure_started_mode_proxy_raises_when_no_creds()

mode='proxy' is explicit — fail noisily so users notice.

test_gcloud_default_project_handles_missing_gcloud_silently()

No gcloud installed → return empty, no exception.

test_gcloud_default_project_returns_empty_when_unset()

gcloud config get-value project returning (unset) or empty must produce "" from the helper — never the literal string (unset).

test_geeView_robustInitializer_is_thin_pointer_to_eeAuth()

geeView.robustInitializer must be a thin delegate to geeViz.eeAuth.robust_init; the real flow lives in eeAuth so it's usable from any entry point, not just module-import.

test_initialize_via_proxy_emits_helpful_message_on_earthengine_legacy_403()

When the OAuth-no-project failure mode happens, the error message must tell users how to fix it — listing the four remediation paths — rather than dumping a raw HttpError.

test_initialize_via_proxy_explains_sa_missing_serviceusage_role()

When an SA's project_id IS a real project but the SA itself lacks roles/serviceusage.serviceUsageConsumer on it, the error must name the project, point at the missing role, and offer the project= override as an alternative fix.

test_initialize_via_proxy_uses_anonymous_creds()

The Python SDK init path must use AnonymousCredentials so the SDK doesn't try to attach its own token — the proxy supplies the real one.

test_legacy_mode_url_still_uses_accessToken_template()

When proxy is unavailable, the URL must use the legacy format with the minted token baked in.

test_map_view_calls_ensure_started_in_auto_mode()

Default mode is 'auto'; Map.view() must call ensure_started so discovery + proxy auto-start happens.

test_map_view_legacy_emits_deprecation_warning()

When the legacy path runs, a DeprecationWarning must be emitted.

test_map_view_mode_proxy_propagates_runtime_error()

When GEEVIZ_EEAUTH_MODE=proxy and proxy can't start, Map.view() must surface the error rather than silently falling back.

test_map_view_reads_GEEVIZ_EEAUTH_MODE_env()

Map.view() must consult GEEVIZ_EEAUTH_MODE to pick auto/proxy/legacy.

test_mixed_refresh_and_sa_credentials_coexist()

A registry can hold a personal-account refresh token AND a service account at the same time.

test_multiple_creds_first_is_default()

test_multiple_creds_use_switches()

test_no_creds_use_raises()

test_oauth_default_project_from_quota_project_id()

Authorized_user files don't have project_id, but they often have quota_project_id.

test_oauth_with_explicit_project_override()

User-supplied project argument wins over what's in the JSON.

test_proxy_mode_url_registers_upstream_and_keeps_url_clean()

When proxy is active, Map.view() must: - register the upstream proxy URL with the local HTTP server via _set_ee_api_upstream so it can reverse-proxy /ee-api/* - default to an EMPTY query string — the JS viewer's same-origin window.location.origin + "/ee-api" default does the work - include ?tenant= only when there are multiple registered credentials AND the active one isn't the default (first registered) - never include accessToken / accessTokenCreationTime / projectID — the proxy injects auth + x-goog-user-project server-side

test_proxy_strips_quota_project_on_discovery_path()

EE's own _cloud_api_utils.build_cloud_resource strips quota_project_id from credentials before fetching $discovery/rest — the serviceUsage API rejects discovery requests that carry a consumer project.

test_python_sdk_and_map_viewer_share_same_proxy()

Source-of-truth: both code paths point at the same eeCreds proxy_url, ensuring 'Python SDK + Map viewer' parity.

test_robust_init_calls_ee_initialize_with_no_project()

The defining behavior of the simple flow: call ee.Initialize() with NO project arg and let EE's own resolution chain (credentials.quota_project_id → ADC → env vars) pick the project.

test_robust_init_does_not_prompt_for_project()

The simplest UX is no prompts.

test_robust_init_falls_back_to_authenticate_force_true_localhost()

When ee.Initialize() fails, the interactive fallback must run ee.Authenticate(force=True, auth_mode='localhost') — that's the combination that reliably works for desktop dev.

test_robust_init_final_error_lists_concrete_remedies()

If init fails even after ee.Authenticate(force=True), the error must tell the user exactly which commands to run instead of a generic 'auth failed' message.

test_robust_init_propagates_proxy_mode_errors()

mode='proxy' is explicit — if the user demanded the proxy and it can't start, surface the error rather than falling through.

test_robust_init_raises_in_non_interactive_when_init_fails()

Non-interactive callers (daemons, CI) must get a clear RuntimeError when ee.Initialize() fails, NOT a hanging ee.Authenticate() prompt.

test_robust_init_respects_legacy_env()

GEEVIZ_EEAUTH_MODE=legacy must skip the proxy attempt.

test_robust_init_returns_already_initialized_when_ee_works()

Fast path: when ee.Number(1).getInfo() already succeeds, robust_init must NOT touch anything else.

test_robust_init_syncs_oauth_project_after_init()

After ee.Initialize() succeeds (either auto or after auth), OAuth entries must be synced to the working project — otherwise a subsequent Map.view() would route through the proxy with the stale guess.

test_robust_init_tries_eeAuth_proxy_first()

eeAuth.robust_init must attempt the proxy via ensure_started before any other path.

test_robust_init_uses_ee_initialize_auto_resolution()

When the fast-path and the proxy both fail, robust_init must call plain ee.Initialize() (no project arg) and let EE pick the project from credentials' quota_project_id / ADC.

test_robust_init_verifies_with_getinfo_call()

robust_init must verify each init attempt with a real EE call before declaring success — a silently misconfigured proxy or stale project would otherwise break the next user operation.

test_save_ee_project_to_credentials_file_roundtrip()

_save_ee_project_to_credentials_file must write to the EE credentials JSON's project field — same place earthengine set_project writes — so next session reads it back.

test_single_cred_picks_first_automatically()