import uuid import pytest from fastapi.testclient import TestClient from tests.conftest import ( admin_token, approve_cloud, bearer, png_bytes, register_and_confirm, upload_cloud, ) # --------------------------------------------------------------------------- # create cloud # --------------------------------------------------------------------------- def test_create_cloud_with_type(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"cloud-create-{uuid.uuid4().hex[:8]}@example.com", "cloudpass12", "云图创建者", ) r = upload_cloud(client, info["token"], cloud_type_id="1") assert r.status_code == 201 cloud = r.json()["cloud"] assert cloud["status"] == "pending" assert cloud["cloud_type_id"] == 1 assert cloud["cloud_type_name"] == "积云" badge = r.json()["unlocked_badge"] assert badge["cloud_type_id"] == 1 def test_create_cloud_custom_type(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"custom-{uuid.uuid4().hex[:8]}@example.com", "custompass", "自定义类型", ) r = upload_cloud(client, info["token"], cloud_type_id=None, custom_cloud_type="火烧云") assert r.status_code == 201 cloud = r.json()["cloud"] assert cloud["cloud_type_id"] is None assert cloud["custom_cloud_type"] == "火烧云" assert r.json()["unlocked_badge"] is None def test_create_cloud_missing_type(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"notype-{uuid.uuid4().hex[:8]}@example.com", "notypepass", "缺类型", ) r = upload_cloud(client, info["token"], cloud_type_id=None, custom_cloud_type=None) assert r.status_code == 422 assert "必须选择" in r.json()["detail"] def test_create_cloud_unauthenticated(client: TestClient) -> None: r = upload_cloud(client, "bad-token", cloud_type_id="1") assert r.status_code == 401 def test_create_cloud_lat_lon_mismatch(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"latlon-{uuid.uuid4().hex[:8]}@example.com", "latlonpass", "坐标测试", ) r = upload_cloud(client, info["token"], cloud_type_id="1", latitude="31.23", longitude=None) assert r.status_code == 422 assert "经纬度" in r.json()["detail"] def test_create_duplicate_collection_no_badge(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"dupbadge-{uuid.uuid4().hex[:8]}@example.com", "dupbadge", "重复徽章", ) r1 = upload_cloud(client, info["token"], cloud_type_id="1") assert r1.status_code == 201 assert r1.json()["unlocked_badge"] is not None r2 = upload_cloud(client, info["token"], cloud_type_id="1") assert r2.status_code == 201 assert r2.json()["unlocked_badge"] is None # --------------------------------------------------------------------------- # list gallery # --------------------------------------------------------------------------- def test_list_gallery_empty_while_no_approved(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"gallery-empty-{uuid.uuid4().hex[:8]}@example.com", "gallerypass", "画廊空", ) upload_cloud(client, info["token"], cloud_type_id="1") r = client.get("/api/v1/clouds") assert r.status_code == 200 assert r.json()["total"] == 0 def test_list_gallery_with_approved(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"gallery-{uuid.uuid4().hex[:8]}@example.com", "gallerypass", "画廊有", ) ur = upload_cloud(client, info["token"], cloud_type_id="1") approve_cloud(client, admin_token(client), ur.json()["cloud"]["id"]) r = client.get("/api/v1/clouds") assert r.status_code == 200 assert r.json()["total"] >= 1 def test_list_gallery_search_by_cloud_name(client: TestClient) -> None: r = client.get("/api/v1/clouds", params={"search": "积云"}) assert r.status_code == 200 for item in r.json()["items"]: assert "积云" in item["cloud_type_name"] or "积云" in (item.get("custom_cloud_type") or "") def test_list_gallery_search_by_username(client: TestClient, monkeypatch) -> None: raw = uuid.uuid4().hex[:8] info = register_and_confirm( client, monkeypatch, f"searchuser-{raw}@example.com", "searchuser", raw, ) ur = upload_cloud(client, info["token"], cloud_type_id="1") approve_cloud(client, admin_token(client), ur.json()["cloud"]["id"]) r = client.get("/api/v1/clouds", params={"search": f"@{raw}"}) assert r.status_code == 200 assert r.json()["total"] >= 1 # --------------------------------------------------------------------------- # get single cloud # --------------------------------------------------------------------------- def test_get_own_cloud(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"own-{uuid.uuid4().hex[:8]}@example.com", "ownpass12", "拥有者", ) ur = upload_cloud(client, info["token"], cloud_type_id="1") cloud_id = ur.json()["cloud"]["id"] r = client.get(f"/api/v1/clouds/{cloud_id}", headers=bearer(info["token"])) assert r.status_code == 200 assert r.json()["status"] == "pending" def test_get_others_pending_cloud(client: TestClient, monkeypatch) -> None: owner_info = register_and_confirm( client, monkeypatch, f"owner-{uuid.uuid4().hex[:8]}@example.com", "ownerpass1", "上传者", ) ur = upload_cloud(client, owner_info["token"], cloud_type_id="1") cloud_id = ur.json()["cloud"]["id"] viewer_info = register_and_confirm( client, monkeypatch, f"viewer-{uuid.uuid4().hex[:8]}@example.com", "viewerpass", "路人", ) r = client.get(f"/api/v1/clouds/{cloud_id}", headers=bearer(viewer_info["token"])) assert r.status_code == 404 def test_get_nonexistent_cloud(client: TestClient) -> None: r = client.get(f"/api/v1/clouds/{uuid.uuid4()}") assert r.status_code == 404 # --------------------------------------------------------------------------- # update cloud # --------------------------------------------------------------------------- def test_update_own_cloud(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"upd-{uuid.uuid4().hex[:8]}@example.com", "updpass12", "更新者", ) ur = upload_cloud(client, info["token"], cloud_type_id="1") cloud_id = ur.json()["cloud"]["id"] r = client.patch( f"/api/v1/clouds/{cloud_id}", headers=bearer(info["token"]), json={"description": "更新后的描述"}, ) assert r.status_code == 200 assert r.json()["description"] == "更新后的描述" def test_update_others_cloud(client: TestClient, monkeypatch) -> None: owner_info = register_and_confirm( client, monkeypatch, f"upd-owner-{uuid.uuid4().hex[:8]}@example.com", "ownerpass", "原主", ) ur = upload_cloud(client, owner_info["token"], cloud_type_id="1") cloud_id = ur.json()["cloud"]["id"] other_info = register_and_confirm( client, monkeypatch, f"upd-other-{uuid.uuid4().hex[:8]}@example.com", "otherpass", "别人", ) r = client.patch( f"/api/v1/clouds/{cloud_id}", headers=bearer(other_info["token"]), json={"description": "被改了"}, ) assert r.status_code == 403 # --------------------------------------------------------------------------- # delete cloud # --------------------------------------------------------------------------- def test_delete_own_cloud(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"del-{uuid.uuid4().hex[:8]}@example.com", "delpass12", "删除者", ) ur = upload_cloud(client, info["token"], cloud_type_id="1") cloud_id = ur.json()["cloud"]["id"] r = client.delete(f"/api/v1/clouds/{cloud_id}", headers=bearer(info["token"])) assert r.status_code == 200 assert r.json()["deleted"] == 1 r = client.get(f"/api/v1/clouds/{cloud_id}", headers=bearer(info["token"])) assert r.status_code == 404 def test_delete_others_cloud(client: TestClient, monkeypatch) -> None: owner_info = register_and_confirm( client, monkeypatch, f"del-own-{uuid.uuid4().hex[:8]}@example.com", "ownerpass", "物主", ) ur = upload_cloud(client, owner_info["token"], cloud_type_id="1") cloud_id = ur.json()["cloud"]["id"] other_info = register_and_confirm( client, monkeypatch, f"del-oth-{uuid.uuid4().hex[:8]}@example.com", "otherpass", "外人", ) r = client.delete(f"/api/v1/clouds/{cloud_id}", headers=bearer(other_info["token"])) assert r.status_code == 403 # --------------------------------------------------------------------------- # batch delete # --------------------------------------------------------------------------- def test_batch_delete(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"batch-{uuid.uuid4().hex[:8]}@example.com", "batchpass", "批量删除", ) u1 = upload_cloud(client, info["token"], cloud_type_id="1") u2 = upload_cloud(client, info["token"], cloud_type_id="2") ids = [u1.json()["cloud"]["id"], u2.json()["cloud"]["id"]] r = client.post( "/api/v1/clouds/batch-delete", headers=bearer(info["token"]), json={"ids": ids}, ) assert r.status_code == 200 assert r.json()["deleted"] == 2 def test_batch_delete_partial_invalid(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"batch2-{uuid.uuid4().hex[:8]}@example.com", "batch2pass", "批量部分", ) u1 = upload_cloud(client, info["token"], cloud_type_id="1") ids = [u1.json()["cloud"]["id"], str(uuid.uuid4())] r = client.post( "/api/v1/clouds/batch-delete", headers=bearer(info["token"]), json={"ids": ids}, ) assert r.status_code == 404 # --------------------------------------------------------------------------- # map # --------------------------------------------------------------------------- def test_map_clouds(client: TestClient, monkeypatch) -> None: info = register_and_confirm( client, monkeypatch, f"map-{uuid.uuid4().hex[:8]}@example.com", "mappass12", "地图用户", ) ur = upload_cloud( client, info["token"], cloud_type_id="1", latitude="31.23", longitude="121.47", captured_at="2025-01-01T00:00:00", ) approve_cloud(client, admin_token(client), ur.json()["cloud"]["id"]) r = client.get("/api/v1/clouds/map", params={ "start": "2024-12-31T00:00:00", "end": "2026-01-01T00:00:00", }) assert r.status_code == 200 assert len(r.json()) >= 1 def test_map_invalid_time_range(client: TestClient) -> None: r = client.get("/api/v1/clouds/map", params={ "start": "2025-01-02T00:00:00", "end": "2025-01-01T00:00:00", }) assert r.status_code == 422 assert "晚于开始" in r.json()["detail"]