feat: add client latest release api

This commit is contained in:
QiuSW
2026-07-07 08:33:49 +08:00
parent f158a08b9d
commit 5f17235a13
13 changed files with 374 additions and 17 deletions
+188 -1
View File
@@ -8,6 +8,7 @@ from unittest.mock import patch
import requests
from cryptography.fernet import Fernet
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.test import TestCase, override_settings
@@ -19,7 +20,7 @@ from rest_framework.views import APIView
from apps.api.authentication import ApiKeyAuthentication
from apps.api.throttles import GenerateRateThrottle
from apps.api.views import ExternalApiView, ModelsView
from apps.api.views import ClientLatestReleaseView, ExternalApiView, ModelsView
from apps.ai.models import AiModel, ModelAlias
from apps.ai.providers import (
AiCapabilityError,
@@ -41,6 +42,7 @@ from apps.billing.payment_gateways import (
from apps.billing.services import RechargePayment
from apps.moderation.models import SensitiveWord
from apps.moderation.providers.keyword import reset_keyword_matcher_cache
from apps.portal.models import DownloadRelease
from apps.users.models import ApiKey
from apps.users.models import UserWallet
@@ -404,6 +406,191 @@ class ModelsCatalogApiTests(TestCase):
self.assertEqual(item["prices"], [])
class ClientLatestReleaseApiTests(TestCase):
url = "/api/v1/client/releases/latest"
def setUp(self):
cache.clear()
self.client = APIClient()
def create_release(
self,
*,
platform: str = DownloadRelease.Platform.WINDOWS,
version: str = "1.0.0",
is_current: bool = True,
external_url: str = "https://download.example.com/cmhub-desktop.exe",
file_name: str = "",
sha256: str = "a" * 64,
release_notes: str = "首版 Windows 客户端",
) -> DownloadRelease:
return DownloadRelease.objects.create(
platform=platform,
version=version,
is_current=is_current,
external_url=external_url,
file=file_name,
sha256=sha256,
release_notes=release_notes,
)
def test_latest_release_is_public_without_api_key_and_returns_current_release(self):
release = self.create_release(
version="1.2.3",
external_url="https://download.example.com/cmhub-1.2.3.exe",
sha256="b" * 64,
release_notes="修复下载入口并补充 SHA256",
)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertNotIn(GenerateRateThrottle, ClientLatestReleaseView.throttle_classes)
self.assertEqual(response.data["platform"], "windows")
self.assertEqual(
set(response.data["release"]),
{"version", "download_url", "sha256", "release_notes", "published_at"},
)
self.assertEqual(response.data["release"]["version"], "1.2.3")
self.assertEqual(
response.data["release"]["download_url"],
"https://download.example.com/cmhub-1.2.3.exe",
)
self.assertEqual(response.data["release"]["sha256"], "b" * 64)
self.assertEqual(
response.data["release"]["release_notes"],
"修复下载入口并补充 SHA256",
)
self.assertEqual(
response.data["release"]["published_at"],
timezone.localtime(release.updated_at).isoformat(),
)
def test_latest_release_ignores_web_session_and_does_not_return_user_data(self):
user = get_user_model().objects.create_user(
username="release-session-user",
email="release-session-user@example.com",
password="password",
)
self.create_release()
self.client.force_login(user)
response = self.client.get(self.url, HTTP_AUTHORIZATION="Bearer sk_cmhub_invalid")
self.assertEqual(response.status_code, 200)
response_body = json.dumps(response.data, ensure_ascii=False)
self.assertNotIn(user.username, response_body)
self.assertNotIn(user.email, response_body)
self.assertNotIn("api_key", response_body)
self.assertNotIn("key_hash", response_body)
def test_latest_release_builds_absolute_file_url(self):
self.create_release(
external_url="",
file_name="downloads/cmhub-desktop-1.0.0.exe",
)
response = self.client.get(self.url, secure=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data["release"]["download_url"],
"https://testserver/media/downloads/cmhub-desktop-1.0.0.exe",
)
def test_latest_release_prefers_external_url_over_uploaded_file(self):
self.create_release(
external_url="https://cdn.example.com/cmhub-desktop-1.0.0.exe",
file_name="downloads/local-secret-name.exe",
)
response = self.client.get(self.url, secure=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data["release"]["download_url"],
"https://cdn.example.com/cmhub-desktop-1.0.0.exe",
)
response_body = json.dumps(response.data, ensure_ascii=False)
self.assertNotIn("local-secret-name.exe", response_body)
self.assertNotIn(str(settings.MEDIA_ROOT), response_body)
def test_latest_release_returns_unpublished_when_no_current_release(self):
self.create_release(version="0.9.0", is_current=False)
response = self.client.get(f"{self.url}?platform=windows")
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
{
"platform": "windows",
"release": None,
"message": "暂未发布",
},
)
def test_latest_release_returns_unpublished_when_current_release_has_no_download_url(self):
self.create_release(external_url="", file_name="")
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertIsNone(response.data["release"])
self.assertEqual(response.data["message"], "暂未发布")
def test_latest_release_rejects_invalid_platform(self):
response = self.client.get(f"{self.url}?platform=android")
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data["error"]["code"], "bad_request")
def test_latest_release_supports_non_windows_platform(self):
self.create_release(
platform=DownloadRelease.Platform.MACOS,
version="2.0.0",
external_url="https://download.example.com/cmhub-2.0.0.dmg",
sha256="c" * 64,
release_notes="macOS 客户端",
)
response = self.client.get(f"{self.url}?platform=macos")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["platform"], "macos")
self.assertEqual(response.data["release"]["version"], "2.0.0")
def test_latest_release_response_does_not_expose_internal_fields(self):
self.create_release(
external_url="",
file_name="downloads/cmhub-desktop-1.0.0.exe",
)
response = self.client.get(self.url, secure=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(set(response.data), {"platform", "release"})
self.assertEqual(
set(response.data["release"]),
{"version", "download_url", "sha256", "release_notes", "published_at"},
)
response_body = json.dumps(response.data, ensure_ascii=False)
for forbidden in (
"id",
"is_current",
"created_at",
"updated_at",
"MEDIA_ROOT",
str(settings.MEDIA_ROOT),
"user",
"email",
"api_key",
"api_key_encrypted",
"model_used",
):
self.assertNotIn(forbidden, response_body)
@override_settings(
PAYMENT_CALLBACK_MODE="mock",
PAYMENT_MOCK_CALLBACK_SECRET="test-payment-callback-secret",