"""OTA:最新版本清单与安装包下载。 不依赖数据库,也不需要登录——App 还没登录就可能在检查更新。 发布目录用 tmp_path 顶替,绝不去碰真实产物。 """ from __future__ import annotations import hashlib import json from pathlib import Path import pytest from httpx import AsyncClient from app.api import app_release @pytest.fixture() def release_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: monkeypatch.setattr(app_release.settings, "apk_dir", str(tmp_path)) return tmp_path def _publish(directory: Path, *, version_code: int = 2, version_name: str = "0.2.0") -> bytes: payload = b"PK\x03\x04 fake apk bytes" name = f"dadaotask-{version_name}.apk" (directory / name).write_bytes(payload) manifest = { "platform": "android", "version_code": version_code, "version_name": version_name, "notes": "修了分类侧栏的层级", "file": name, "size": len(payload), "sha256": hashlib.sha256(payload).hexdigest(), "min_sdk": 26, "published_at": "2026-10-31T02:00:00+00:00", } (directory / "latest.json").write_text(json.dumps(manifest), encoding="utf-8") return payload async def test_latest_release_is_public_and_reports_update( client: AsyncClient, release_dir: Path ) -> None: # 没发布过:明确告诉客户端没有安装包,而不是 500 resp = await client.get("/api/v1/app/release/latest") body = resp.json() assert resp.status_code == 404 assert body["code"] == 9001 async def test_latest_release_payload(client: AsyncClient, release_dir: Path) -> None: _publish(release_dir) resp = await client.get("/api/v1/app/release/latest") assert resp.status_code == 200 data = resp.json()["data"] assert data["version_code"] == 2 assert data["version_name"] == "0.2.0" assert data["notes"] == "修了分类侧栏的层级" assert data["download_url"] == "/api/v1/app/release/download/dadaotask-0.2.0.apk" assert data["has_update"] is False # 没带当前版本就不猜 async def test_latest_release_tells_client_whether_to_upgrade( client: AsyncClient, release_dir: Path ) -> None: _publish(release_dir, version_code=2) older = await client.get("/api/v1/app/release/latest?version_code=1") assert older.json()["data"]["has_update"] is True same = await client.get("/api/v1/app/release/latest?version_code=2") assert same.json()["data"]["has_update"] is False newer = await client.get("/api/v1/app/release/latest?version_code=9") assert newer.json()["data"]["has_update"] is False async def test_download_returns_the_apk_bytes(client: AsyncClient, release_dir: Path) -> None: payload = _publish(release_dir) resp = await client.get("/api/v1/app/release/download/dadaotask-0.2.0.apk") assert resp.status_code == 200 assert resp.headers["content-type"] == "application/vnd.android.package-archive" assert resp.content == payload async def test_download_refuses_files_outside_the_manifest( client: AsyncClient, release_dir: Path ) -> None: _publish(release_dir) resp = await client.get("/api/v1/app/release/download/..%2Fsecret.txt") assert resp.status_code == 404 missing = await client.get("/api/v1/app/release/download/dadaotask-9.9.9.apk") assert missing.status_code == 404 assert missing.json()["code"] == 9001