from __future__ import annotations from datetime import timedelta from typing import Any from fastapi.testclient import TestClient from pytest import MonkeyPatch import app.fleet.service as fleet_service_module from app.core.time import to_iso, utc_now from tests.conftest import csrf_headers def create_account( client: TestClient, payload: dict[str, object], ) -> tuple[dict[str, Any], str]: response = client.post( "/api/v1/accounts", json=payload, headers=csrf_headers(client), ) assert response.status_code == 201, response.text return response.json()["data"], response.headers["location"] def aws_access_payload(name: str = "AWS Access") -> dict[str, object]: return { "provider": "aws", "name": name, "use_default_aws_credentials": False, "aws_access_key_id": "AKIA_API_ACCESS_KEY", "aws_secret_access_key": "api-secret-access-key", "aws_session_token": "api-session-token", } def cloudflare_payload(name: str = "Cloudflare Main") -> dict[str, object]: return { "provider": "cloudflare", "name": name, "cloudflare_api_token": "cloudflare-api-token-value", } def test_account_create_list_get_flags_and_no_plaintext_leak( authenticated_client: TestClient, ) -> None: client = authenticated_client access, access_location = create_account(client, aws_access_payload()) default, _ = create_account( client, { "provider": "aws", "name": "AWS Default Chain", "use_default_aws_credentials": True, }, ) cloudflare, _ = create_account(client, cloudflare_payload()) assert access["config_version"] == 1 assert access["use_default_aws_credentials"] is False assert access["secrets_configured"] == { "aws_access_key_id": True, "aws_secret_access_key": True, "aws_session_token": True, "cloudflare_api_token": False, } assert default["use_default_aws_credentials"] is True assert not any(default["secrets_configured"].values()) assert cloudflare["use_default_aws_credentials"] is None assert cloudflare["secrets_configured"] == { "aws_access_key_id": False, "aws_secret_access_key": False, "aws_session_token": False, "cloudflare_api_token": True, } assert client.get(access_location).json()["data"] == access listed = client.get("/api/v1/accounts") assert listed.status_code == 200 assert [item["id"] for item in listed.json()["data"]] == [ access["id"], default["id"], cloudflare["id"], ] aws_only = client.get("/api/v1/accounts", params={"provider": "aws"}) assert {item["id"] for item in aws_only.json()["data"]} == { access["id"], default["id"], } plaintexts = ( "AKIA_API_ACCESS_KEY", "api-secret-access-key", "api-session-token", "cloudflare-api-token-value", ) serialized_responses = " ".join((listed.text, client.get(access_location).text)) assert all(secret not in serialized_responses for secret in plaintexts) container = client.app.state.container with container.database.connect() as connection: rows = connection.execute( "SELECT account_id, name, nonce, ciphertext FROM account_secrets" ).fetchall() stored = " ".join(str(value) for row in rows for value in tuple(row)) assert all(secret not in stored for secret in plaintexts) def test_account_updates_preserve_blank_secrets_replace_pair_and_enforce_cas( authenticated_client: TestClient, ) -> None: client = authenticated_client aws, location = create_account(client, aws_access_payload()) preserved = client.put( location, json={ "config_version": aws["config_version"], "name": "AWS Access Renamed", "use_default_aws_credentials": False, "aws_access_key_id": None, "aws_secret_access_key": None, "aws_session_token": None, }, headers=csrf_headers(client), ) assert preserved.status_code == 200, preserved.text preserved_data = preserved.json()["data"] assert preserved_data["config_version"] == 2 assert all( preserved_data["secrets_configured"][name] for name in ( "aws_access_key_id", "aws_secret_access_key", "aws_session_token", ) ) assert client.app.state.container.account_repository.resolve_aws(aws["id"])[1] == { "aws_access_key_id": "AKIA_API_ACCESS_KEY", "aws_secret_access_key": "api-secret-access-key", "aws_session_token": "api-session-token", } replaced = client.put( location, json={ "config_version": preserved_data["config_version"], "aws_access_key_id": "AKIA_API_REPLACED_KEY", "aws_secret_access_key": "api-replaced-secret", "aws_session_token": None, }, headers=csrf_headers(client), ) assert replaced.status_code == 200, replaced.text replaced_data = replaced.json()["data"] assert replaced_data["config_version"] == 3 assert replaced_data["secrets_configured"]["aws_session_token"] is False assert client.app.state.container.account_repository.resolve_aws(aws["id"])[1] == { "aws_access_key_id": "AKIA_API_REPLACED_KEY", "aws_secret_access_key": "api-replaced-secret", "aws_session_token": None, } stale = client.put( location, json={"config_version": preserved_data["config_version"], "name": "Stale"}, headers=csrf_headers(client), ) assert stale.status_code == 409 assert stale.json()["type"] == "about:blank#account_config_version_conflict" def test_cloudflare_update_preserves_blank_token( authenticated_client: TestClient, ) -> None: client = authenticated_client cloudflare, cloudflare_location = create_account(client, cloudflare_payload()) cloudflare_preserved = client.put( cloudflare_location, json={ "config_version": cloudflare["config_version"], "name": "Cloudflare Renamed", "cloudflare_api_token": None, }, headers=csrf_headers(client), ) assert cloudflare_preserved.status_code == 200, cloudflare_preserved.text assert cloudflare_preserved.json()["data"]["secrets_configured"]["cloudflare_api_token"] is True assert ( client.app.state.container.account_repository.resolve_cloudflare(cloudflare["id"]) == "cloudflare-api-token-value" ) def test_account_soft_delete_and_provider_payload_validation( authenticated_client: TestClient, ) -> None: client = authenticated_client account, location = create_account(client, cloudflare_payload("Cloudflare Archived")) assert client.delete(location).status_code == 403 archived = client.delete(location, headers=csrf_headers(client)) assert archived.status_code == 200 assert archived.json()["data"]["archived_at"] is not None assert client.get(location).status_code == 404 assert account["id"] not in { item["id"] for item in client.get("/api/v1/accounts").json()["data"] } stored = client.app.state.container.account_repository.get_account( account["id"], include_archived=True ) assert stored is not None and stored.archived_at is not None invalid_payloads = ( { "provider": "aws", "name": "AWS With Cloudflare Token", "use_default_aws_credentials": True, "cloudflare_api_token": "wrong-token", }, { "provider": "cloudflare", "name": "Cloudflare With AWS Key", "cloudflare_api_token": "cf-token", "aws_access_key_id": "AKIA_WRONG", "aws_secret_access_key": "wrong-secret", }, { "provider": "aws", "name": "Incomplete AWS", "aws_access_key_id": "AKIA_ONLY", }, ) for payload in invalid_payloads: response = client.post( "/api/v1/accounts", json=payload, headers=csrf_headers(client), ) assert response.status_code == 422 assert client.get("/api/v1/accounts", params={"provider": "azure"}).status_code == 422 def test_instance_account_binding_validates_provider_missing_and_in_use( authenticated_client: TestClient, managed_instance_payload: dict[str, object], ) -> None: client = authenticated_client aws, aws_location = create_account(client, aws_access_payload("AWS Instance")) cloudflare, cloudflare_location = create_account( client, cloudflare_payload("Cloudflare Instance") ) provider_mismatch = client.post( "/api/v1/instances", json={ **managed_instance_payload, "id": "provider-mismatch-instance", "aws_account_id": cloudflare["id"], "cloudflare_account_id": aws["id"], }, headers=csrf_headers(client), ) assert provider_mismatch.status_code == 422 assert provider_mismatch.json()["type"] == ("about:blank#aws_account_provider_mismatch") missing = client.post( "/api/v1/instances", json={ **managed_instance_payload, "id": "missing-account-instance", "aws_account_id": aws["id"], "cloudflare_account_id": "missing-cloudflare-account", }, headers=csrf_headers(client), ) assert missing.status_code == 422 assert missing.json()["type"] == "about:blank#cloudflare_account_not_found" created = client.post( "/api/v1/instances", json={ **managed_instance_payload, "id": "account-linked-instance", "aws_account_id": aws["id"], "cloudflare_account_id": cloudflare["id"], }, headers=csrf_headers(client), ) assert created.status_code == 201, created.text created_data = created.json()["data"] assert created_data["aws_account_id"] == aws["id"] assert created_data["cloudflare_account_id"] == cloudflare["id"] for location in (aws_location, cloudflare_location): in_use = client.delete(location, headers=csrf_headers(client)) assert in_use.status_code == 409 assert in_use.json()["type"] == "about:blank#account_in_use" instance_location = created.headers["location"] assert client.delete(instance_location, headers=csrf_headers(client)).status_code == 200 assert client.delete(aws_location, headers=csrf_headers(client)).status_code == 200 assert client.delete(cloudflare_location, headers=csrf_headers(client)).status_code == 200 def test_account_writes_are_blocked_by_active_run_and_dns_lock( authenticated_client: TestClient, ) -> None: client = authenticated_client account, location = create_account(client, aws_access_payload("AWS Locked")) container = client.app.state.container now = to_iso() with container.database.connect() as connection, connection: connection.execute( """ INSERT INTO fleet_runs( id, target_type, target_id, target_name, trigger, status, active_slot, total_items, succeeded_items, started_at, updated_at ) VALUES ( 'api-account-run', 'instance', 'target', 'Target', 'manual', 'running', 1, 0, 0, ?, ? ) """, (now, now), ) run_blocked_requests = ( ("post", "/api/v1/accounts", cloudflare_payload("Blocked Cloudflare")), ( "put", location, {"config_version": account["config_version"], "name": "Blocked rename"}, ), ("delete", location, None), ) for method, path, payload in run_blocked_requests: response = client.request( method, path, json=payload, headers=csrf_headers(client), ) assert response.status_code == 409 assert response.json()["type"] == "about:blank#fleet_run_active" with container.database.connect() as connection, connection: connection.execute("DELETE FROM fleet_runs WHERE id = 'api-account-run'") connection.execute( """ INSERT INTO fleet_operation_locks( slot, kind, owner_id, lease_until, created_at ) VALUES (1, 'dns_sync', 'api-account-dns-lock', ?, ?) """, (to_iso(utc_now() + timedelta(minutes=5)), now), ) dns_blocked = client.put( location, json={"config_version": account["config_version"], "name": "DNS blocked"}, headers=csrf_headers(client), ) assert dns_blocked.status_code == 409 assert dns_blocked.json()["type"] == "about:blank#fleet_run_active" def test_region_catalog_requires_auth_and_contains_chinese_metadata( client: TestClient, authenticated_client: TestClient, ) -> None: # Both fixtures refer to the same test client instance for this test. client.cookies.clear() assert client.get("/api/v1/regions").status_code == 401 login = client.post( "/api/v1/auth/login", json={"username": "admin", "password": "correct-horse-battery-staple"}, ) assert login.status_code == 200 response = authenticated_client.get("/api/v1/regions") assert response.status_code == 200 regions = response.json()["data"] assert len(regions) == 19 assert len({region["code"] for region in regions}) == len(regions) by_code = {region["code"]: region for region in regions} assert by_code["ap-east-1"] == { "code": "ap-east-1", "name_zh": "亚太地区(香港)", "icon": "🇭🇰", } assert by_code["ap-southeast-1"]["name_zh"] == "亚太地区(新加坡)" assert by_code["us-east-1"]["name_zh"] == "美国东部(弗吉尼亚北部)" def test_instance_rejects_region_outside_catalog( authenticated_client: TestClient, managed_instance_payload: dict[str, object], ) -> None: response = authenticated_client.post( "/api/v1/instances", json={**managed_instance_payload, "aws_region": "moon-east-1"}, headers=csrf_headers(authenticated_client), ) assert response.status_code == 422 assert any(error["field"] == "aws_region" for error in response.json()["errors"]) def test_fleet_clients_resolve_credentials_from_instance_accounts( authenticated_client: TestClient, managed_instance_payload: dict[str, object], monkeypatch: MonkeyPatch, ) -> None: client = authenticated_client aws, _ = create_account( client, { "provider": "aws", "name": "AWS Client Resolution", "use_default_aws_credentials": False, "aws_access_key_id": "AKIA_BOUND_CLIENT", "aws_secret_access_key": "bound-client-secret", "aws_session_token": "bound-client-session", }, ) cloudflare, _ = create_account( client, { "provider": "cloudflare", "name": "Cloudflare Client Resolution", "cloudflare_api_token": "bound-cloudflare-token", }, ) created = client.post( "/api/v1/instances", json={ **managed_instance_payload, "id": "client-resolution-instance", "aws_account_id": aws["id"], "cloudflare_account_id": cloudflare["id"], "aws_region": "ap-northeast-1", "lightsail_instance_name": "bound-lightsail-node", "cloudflare_zone_id": "bound-zone-id", "cloudflare_record_name": "bound.example.com", }, headers=csrf_headers(client), ) assert created.status_code == 201, created.text captured: dict[str, dict[str, object]] = {} aws_client = object() cloudflare_client = object() def lightsail_factory(**kwargs: object) -> object: captured["aws"] = kwargs return aws_client def cloudflare_factory(**kwargs: object) -> object: captured["cloudflare"] = kwargs return cloudflare_client def reject_legacy_credentials() -> None: raise AssertionError("绑定账号完整时不应读取旧全局凭据") container = client.app.state.container monkeypatch.setattr( fleet_service_module.LightsailClient, "from_credentials", staticmethod(lightsail_factory), ) monkeypatch.setattr( fleet_service_module, "CloudflareClient", cloudflare_factory, ) monkeypatch.setattr( container.integration_service, "credentials", reject_legacy_credentials, ) instance = container.fleet_repository.get_instance("client-resolution-instance") assert instance is not None resolved_aws, resolved_cloudflare = container.fleet_service._clients(instance) assert resolved_aws is aws_client assert resolved_cloudflare is cloudflare_client assert captured["aws"] == { "region": "ap-northeast-1", "instance_name": "bound-lightsail-node", "use_default": False, "secrets": { "aws_access_key_id": "AKIA_BOUND_CLIENT", "aws_secret_access_key": "bound-client-secret", "aws_session_token": "bound-client-session", }, } assert captured["cloudflare"] == { "token": "bound-cloudflare-token", "zone_name": "example.com", "zone_id": "bound-zone-id", "record_name": "bound.example.com", "proxied": False, } def test_rotation_starts_with_bound_accounts_without_global_credentials( authenticated_client: TestClient, managed_instance_payload: dict[str, object], monkeypatch: MonkeyPatch, ) -> None: client = authenticated_client legacy = client.get("/api/v1/integrations") assert legacy.status_code == 200 assert not any(legacy.json()["data"]["secrets_configured"].values()) assert legacy.json()["data"]["use_default_aws_credentials"] is False aws, _ = create_account( client, { "provider": "aws", "name": "AWS Rotation Only", "use_default_aws_credentials": False, "aws_access_key_id": "AKIA_ROTATION_BOUND", "aws_secret_access_key": "rotation-bound-secret", }, ) cloudflare, _ = create_account( client, { "provider": "cloudflare", "name": "Cloudflare Rotation Only", "cloudflare_api_token": "rotation-bound-cf-token", }, ) instance = client.post( "/api/v1/instances", json={ **managed_instance_payload, "id": "account-only-rotation-instance", "aws_account_id": aws["id"], "cloudflare_account_id": cloudflare["id"], "lightsail_instance_name": "account-only-rotation-node", "cloudflare_record_name": "rotation-only.example.com", }, headers=csrf_headers(client), ) assert instance.status_code == 201, instance.text spawned: list[str] = [] container = client.app.state.container monkeypatch.setattr(container.rotation_service, "_spawn", spawned.append) response = client.post( "/api/v1/instances/account-only-rotation-instance/rotations", headers=csrf_headers(client), ) assert response.status_code == 202, response.text run = response.json()["data"] assert response.headers["location"] == f"/api/v1/rotations/{run['id']}" assert run["status"] == "queued" assert spawned == [run["id"]] with container.database.connect() as connection: item = connection.execute( """ SELECT aws_account_id, cloudflare_account_id FROM fleet_run_items WHERE run_id = ? """, (run["id"],), ).fetchone() assert item is not None assert item["aws_account_id"] == aws["id"] assert item["cloudflare_account_id"] == cloudflare["id"] def test_account_catalog_coexists_with_legacy_global_integrations( authenticated_client: TestClient, ) -> None: client = authenticated_client legacy = client.put( "/api/v1/integrations", json={ "aws_access_key_id": "AKIA_LEGACY_ACCESS", "aws_secret_access_key": "legacy-secret-key", "aws_session_token": None, "use_default_aws_credentials": False, "cloudflare_api_token": "legacy-cloudflare-token", }, headers=csrf_headers(client), ) assert legacy.status_code == 200, legacy.text account, _ = create_account(client, cloudflare_payload("Independent Cloudflare")) legacy_after = client.get("/api/v1/integrations") assert legacy_after.status_code == 200 assert legacy_after.json()["data"] == legacy.json()["data"] assert client.get(f"/api/v1/accounts/{account['id']}").status_code == 200