from __future__ import annotations import time import pytest from fastapi.testclient import TestClient from sqlalchemy import select @pytest.fixture() def license_app(tmp_path, monkeypatch): monkeypatch.setenv("APS_AUTH_PROVIDER", "mock") monkeypatch.setenv("APS_LICENSE_PROVIDER", "mock") monkeypatch.setenv("APS_MOCK_AUTH_SECRET", "user-test-secret") monkeypatch.setenv("APS_MOCK_LICENSE_SECRET", "license-test-secret") monkeypatch.setenv("APS_MOCK_TENANT_UUID", "tenant-license-00000000000000001") monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "license.db")) monkeypatch.setenv("APS_WORLD_PATH", str(tmp_path / "world.json")) from server.auth import licenses, providers from server.db.database import reset_engine from server.state import store as world_store providers._provider = None providers._provider_mode = None licenses._provider = None licenses._provider_mode = None world_store._stores.clear() reset_engine() from server.gateway.app import create_app app = create_app() yield app reset_engine() world_store._stores.clear() providers._provider = None providers._provider_mode = None licenses._provider = None licenses._provider_mode = None def desktop_headers(device_id: str = "device-installation-0001") -> dict[str, str]: return {"X-APS-Client": "desktop", "X-APS-Device-ID": device_id} @pytest.mark.parametrize( ("code", "license_type", "expected_seconds"), [ ("APS-HOUR-DEMO", "hour", 60 * 60), ("APS-DAY-DEMO", "day", 24 * 60 * 60), ("APS-WEEK-DEMO", "week", 7 * 24 * 60 * 60), ("APS-MONTH-DEMO", "month", 30 * 24 * 60 * 60), ("APS-YEAR-DEMO", "year", 365 * 24 * 60 * 60), ], ) def test_timed_license_codes_activate_with_server_expiry(license_app, code, license_type, expected_seconds): client = TestClient(license_app) before = int(time.time()) response = client.post("/api/auth/license/activate", headers=desktop_headers(), json={"code": code}) after = int(time.time()) assert response.status_code == 200 body = response.json() assert body["user"]["auth_kind"] == "license" assert body["user"]["tenant_uuid"] == "tenant-license-00000000000000001" assert body["license"]["type"] == license_type assert before + expected_seconds <= body["license"]["expiresAt"] <= after + expected_seconds assert client.get("/api/auth/me", headers=desktop_headers()).status_code == 200 def test_permanent_license_has_no_business_expiry(license_app): client = TestClient(license_app) response = client.post( "/api/auth/license/activate", headers=desktop_headers(), json={"code": "APS-PERP-DEMO"}, ) assert response.status_code == 200 assert response.json()["license"] == { "type": "permanent", "label": "永久授权", "activatedAt": response.json()["license"]["activatedAt"], "expiresAt": None, "permanent": True, } def test_license_endpoint_rejects_web_and_demo_code_is_reusable_per_device(license_app): first = TestClient(license_app) second = TestClient(license_app) web_attempt = first.post("/api/auth/license/activate", json={ "code": "APS-YEAR-DEMO", "deviceId": "device-installation-0001", }) assert web_attempt.status_code == 403 assert first.post( "/api/auth/license/activate", headers=desktop_headers("device-installation-0001"), json={"code": "APS-YEAR-DEMO"}, ).status_code == 200 second_activation = second.post( "/api/auth/license/activate", headers=desktop_headers("device-installation-0002"), json={"code": "APS-YEAR-DEMO"}, ) assert second_activation.status_code == 200 assert second_activation.json()["user"]["user_id"] != first.get( "/api/auth/me", headers=desktop_headers("device-installation-0001"), ).json()["user"]["user_id"] def test_expired_activation_is_rejected_on_next_api_request(license_app): client = TestClient(license_app) headers = desktop_headers() assert client.post( "/api/auth/license/activate", headers=headers, json={"code": "APS-HOUR-DEMO"}, ).status_code == 200 from server.db.database import get_session from server.db.models import LicenseActivation with get_session() as session: row = session.scalar(select(LicenseActivation)) assert row is not None row.expires_at = int(time.time()) - 1 session.commit() response = client.get("/api/auth/me", headers=headers) assert response.status_code == 401 assert response.json()["error"]["code"] == "LICENSE_EXPIRED" def test_web_and_desktop_sessions_use_separate_credentials(license_app): client = TestClient(license_app) assert client.post("/api/auth/login", json={ "method": "password", "username": "planner", "password": "test", }).status_code == 200 web_user = client.get("/api/auth/me").json()["user"] assert web_user["auth_kind"] == "user" headers = desktop_headers() assert client.get("/api/auth/me", headers=headers).status_code == 401 assert client.post( "/api/auth/license/activate", headers=headers, json={"code": "APS-YEAR-DEMO"}, ).status_code == 200 desktop_user = client.get("/api/auth/me", headers=headers).json()["user"] assert desktop_user["auth_kind"] == "license" assert desktop_user["user_id"] != web_user["user_id"] assert client.get("/api/auth/me").json()["user"]["user_id"] == web_user["user_id"] def test_desktop_installations_do_not_share_projects_without_membership(license_app): first = TestClient(license_app) second = TestClient(license_app) first_headers = desktop_headers("device-installation-owner") second_headers = desktop_headers("device-installation-other") assert first.post( "/api/auth/license/activate", headers=first_headers, json={"code": "APS-YEAR-DEMO"}, ).status_code == 200 assert second.post( "/api/auth/license/activate", headers=second_headers, json={"code": "APS-MONTH-DEMO"}, ).status_code == 200 assert first.post( "/api/projects", headers=first_headers, json={"id": "desktop_private", "name": "本机私有项目"}, ).status_code == 200 workspace = second.get("/api/workspace", headers=second_headers) assert workspace.status_code == 200 assert workspace.json()["projects"] == [] assert second.get("/api/projects/desktop_private/members", headers=second_headers).status_code == 404