chore: 优化本地开发环境配置
- 添加 .env.local 支持,app.sh 和 dev-migrate.sh 自动覆盖 - Docker Compose 使用 profiles 区分 dev/prod 环境 - 改进认证 dev session 判断逻辑,使用 test account 配置 - 修复 CoinPackageCard 重复代码问题 - 清理 opencode 配置,移除敏感信息 - 新增 infra/docker/README.md 文档 - 修复 ruff/pyright/flutter lint 错误 - 更新测试用例移除已删除的 country 字段
This commit is contained in:
@@ -13,6 +13,7 @@ This file governs `backend/**` only. Keep it minimal, enforceable, and non-dupli
|
||||
- Python commands must use `uv` (`uv run`, `uv add`).
|
||||
- Backend startup/shutdown must use `./infra/scripts/app.sh`.
|
||||
- Check runtime logs from `./logs/*.log`.
|
||||
- Docker Compose usage: see `infra/docker/README.md` for environment-based service activation.
|
||||
|
||||
## Code Quality Baseline
|
||||
|
||||
|
||||
@@ -189,8 +189,8 @@ class SensitiveWordSettings(BaseModel):
|
||||
|
||||
|
||||
class TestSettings(BaseModel):
|
||||
phone: str = ""
|
||||
password: str = ""
|
||||
email: str = ""
|
||||
code: str = ""
|
||||
|
||||
|
||||
class TaskiqSettings(BaseModel):
|
||||
|
||||
@@ -78,15 +78,18 @@ class SupabaseAuthGateway(AuthServiceGateway):
|
||||
async def create_email_session(
|
||||
self, request: EmailSessionCreateRequest
|
||||
) -> SessionResponse:
|
||||
if config.runtime.environment == "dev":
|
||||
return await create_dev_email_session(
|
||||
request=request,
|
||||
client=self._get_client(),
|
||||
admin_client=self._get_admin_client(),
|
||||
auth_unavailable_detail=AUTH_UNAVAILABLE_DETAIL,
|
||||
is_auth_upstream_unavailable=_is_auth_upstream_unavailable,
|
||||
map_auth_response=_map_auth_response,
|
||||
)
|
||||
test_email = config.test.email.strip()
|
||||
test_code = config.test.code.strip()
|
||||
if test_email and test_code:
|
||||
if request.email == test_email and request.token == test_code:
|
||||
return await create_dev_email_session(
|
||||
request=request,
|
||||
client=self._get_client(),
|
||||
admin_client=self._get_admin_client(),
|
||||
auth_unavailable_detail=AUTH_UNAVAILABLE_DETAIL,
|
||||
is_auth_upstream_unavailable=_is_auth_upstream_unavailable,
|
||||
map_auth_response=_map_auth_response,
|
||||
)
|
||||
|
||||
client = self._get_client()
|
||||
payload: dict[str, Any] = {
|
||||
|
||||
@@ -434,19 +434,16 @@ class PaymentService:
|
||||
return
|
||||
|
||||
try:
|
||||
import jwt as pyjwt
|
||||
|
||||
parts = signed_payload.split(".")
|
||||
if len(parts) < 2:
|
||||
logger.warning("Malformed Apple notification signed_payload")
|
||||
return
|
||||
|
||||
payload_bytes = parts[1] + "=" * (-len(parts[1]) % 4)
|
||||
import base64
|
||||
|
||||
decoded = base64.urlsafe_b64decode(payload_bytes)
|
||||
import json
|
||||
|
||||
payload_bytes = parts[1] + "=" * (-len(parts[1]) % 4)
|
||||
decoded = base64.urlsafe_b64decode(payload_bytes)
|
||||
notification_data: Any = json.loads(decoded)
|
||||
except Exception:
|
||||
logger.exception("Failed to decode Apple server notification payload")
|
||||
|
||||
@@ -500,9 +500,9 @@ class PointsService:
|
||||
id=str(row.id),
|
||||
direction=row.direction,
|
||||
amount=row.amount,
|
||||
balance_after=row.balance_after,
|
||||
change_type=row.change_type,
|
||||
created_at=row.created_at.isoformat(),
|
||||
balanceAfter=row.balance_after,
|
||||
changeType=row.change_type,
|
||||
createdAt=row.created_at.isoformat(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""
|
||||
Integration tests for Apple IAP payment verify flow.
|
||||
|
||||
@@ -7,6 +5,8 @@ Prerequisite: backend must be running via `./infra/scripts/app.sh restart`.
|
||||
These tests hit the live HTTP API against the test database.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
from v1.payments.apple_verifier import (
|
||||
AppleJwsVerifier,
|
||||
VerificationError,
|
||||
VerifiedTransaction,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -571,7 +571,6 @@ class TestHandleServerNotificationRefund:
|
||||
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
||||
)
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
signed_txn = _make_fake_signed_transaction(transaction_id="2000000999999001")
|
||||
|
||||
@@ -10,7 +10,7 @@ from v1.auth.schemas import AuthUser, EmailSessionCreateRequest, SessionResponse
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_email_session_uses_dev_bypass(
|
||||
async def test_create_email_session_uses_test_account_bypass(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
gateway = SupabaseAuthGateway()
|
||||
@@ -28,7 +28,8 @@ async def test_create_email_session_uses_dev_bypass(
|
||||
calls.update(kwargs)
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(gateway_module.config.runtime, "environment", "dev")
|
||||
monkeypatch.setattr(gateway_module.config.test, "email", "test@example.com")
|
||||
monkeypatch.setattr(gateway_module.config.test, "code", "123456")
|
||||
monkeypatch.setattr(
|
||||
gateway_module, "create_dev_email_session", _fake_create_dev_email_session
|
||||
)
|
||||
@@ -47,7 +48,7 @@ async def test_create_email_session_uses_dev_bypass(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_email_session_uses_verify_otp_in_non_dev(
|
||||
async def test_create_email_session_uses_verify_otp_when_test_account_not_configured(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
gateway = SupabaseAuthGateway()
|
||||
@@ -66,7 +67,8 @@ async def test_create_email_session_uses_verify_otp_in_non_dev(
|
||||
user=SimpleNamespace(id="user-2", email="test@example.com"),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(gateway_module.config.runtime, "environment", "prod")
|
||||
monkeypatch.setattr(gateway_module.config.test, "email", "")
|
||||
monkeypatch.setattr(gateway_module.config.test, "code", "")
|
||||
monkeypatch.setattr(
|
||||
gateway,
|
||||
"_get_client",
|
||||
@@ -81,3 +83,79 @@ async def test_create_email_session_uses_verify_otp_in_non_dev(
|
||||
"token": "123456",
|
||||
}
|
||||
assert response.user.email == "test@example.com"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_email_session_uses_verify_otp_when_email_mismatch(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
gateway = SupabaseAuthGateway()
|
||||
request = EmailSessionCreateRequest(email="other@example.com", token="123456")
|
||||
captured_payload: dict[str, str] = {}
|
||||
|
||||
def _verify_otp(payload: dict[str, str]) -> SimpleNamespace:
|
||||
captured_payload.update(payload)
|
||||
return SimpleNamespace(
|
||||
session=SimpleNamespace(
|
||||
access_token="access",
|
||||
refresh_token="refresh",
|
||||
expires_in=3600,
|
||||
token_type="bearer",
|
||||
),
|
||||
user=SimpleNamespace(id="user-3", email="other@example.com"),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(gateway_module.config.test, "email", "test@example.com")
|
||||
monkeypatch.setattr(gateway_module.config.test, "code", "123456")
|
||||
monkeypatch.setattr(
|
||||
gateway,
|
||||
"_get_client",
|
||||
lambda: SimpleNamespace(auth=SimpleNamespace(verify_otp=_verify_otp)),
|
||||
)
|
||||
|
||||
response = await gateway.create_email_session(request)
|
||||
|
||||
assert captured_payload == {
|
||||
"type": "email",
|
||||
"email": "other@example.com",
|
||||
"token": "123456",
|
||||
}
|
||||
assert response.user.email == "other@example.com"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_email_session_uses_verify_otp_when_code_mismatch(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
gateway = SupabaseAuthGateway()
|
||||
request = EmailSessionCreateRequest(email="test@example.com", token="654321")
|
||||
captured_payload: dict[str, str] = {}
|
||||
|
||||
def _verify_otp(payload: dict[str, str]) -> SimpleNamespace:
|
||||
captured_payload.update(payload)
|
||||
return SimpleNamespace(
|
||||
session=SimpleNamespace(
|
||||
access_token="access",
|
||||
refresh_token="refresh",
|
||||
expires_in=3600,
|
||||
token_type="bearer",
|
||||
),
|
||||
user=SimpleNamespace(id="user-4", email="test@example.com"),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(gateway_module.config.test, "email", "test@example.com")
|
||||
monkeypatch.setattr(gateway_module.config.test, "code", "123456")
|
||||
monkeypatch.setattr(
|
||||
gateway,
|
||||
"_get_client",
|
||||
lambda: SimpleNamespace(auth=SimpleNamespace(verify_otp=_verify_otp)),
|
||||
)
|
||||
|
||||
response = await gateway.create_email_session(request)
|
||||
|
||||
assert captured_payload == {
|
||||
"type": "email",
|
||||
"email": "test@example.com",
|
||||
"token": "654321",
|
||||
}
|
||||
assert response.user.email == "test@example.com"
|
||||
|
||||
@@ -17,7 +17,6 @@ class TestParseProfileSettings:
|
||||
"preferences": {
|
||||
"language": "en-US",
|
||||
"timezone": "America/New_York",
|
||||
"country": "US",
|
||||
},
|
||||
"privacy": {"profile_visibility": "private"},
|
||||
"notification": {
|
||||
@@ -32,7 +31,6 @@ class TestParseProfileSettings:
|
||||
assert isinstance(result.preferences, PreferenceSettings)
|
||||
assert result.preferences.language == "en-US"
|
||||
assert result.preferences.timezone == "America/New_York"
|
||||
assert result.preferences.country == "US"
|
||||
assert isinstance(result.notification, NotificationSettings)
|
||||
assert result.notification.allow_notifications is True
|
||||
assert result.notification.allow_vibration is False
|
||||
@@ -45,7 +43,6 @@ class TestParseProfileSettings:
|
||||
assert isinstance(result.preferences, PreferenceSettings)
|
||||
assert result.preferences.language == "zh-CN"
|
||||
assert result.preferences.timezone == "Asia/Shanghai"
|
||||
assert result.preferences.country == "US"
|
||||
assert isinstance(result.notification, NotificationSettings)
|
||||
assert result.notification.allow_notifications is True
|
||||
assert result.notification.allow_vibration is True
|
||||
@@ -60,7 +57,6 @@ class TestParseProfileSettings:
|
||||
|
||||
assert result.preferences.language == "en-US"
|
||||
assert result.preferences.timezone == "Asia/Shanghai"
|
||||
assert result.preferences.country == "US"
|
||||
|
||||
def test_parse_profile_settings_with_partial_notification(self) -> None:
|
||||
raw = {
|
||||
@@ -104,21 +100,11 @@ class TestParseProfileSettings:
|
||||
with pytest.raises(ValueError, match="timezone must be a valid IANA timezone"):
|
||||
parse_profile_settings(raw)
|
||||
|
||||
def test_parse_profile_settings_country_normalized_to_uppercase(self) -> None:
|
||||
raw = {
|
||||
"preferences": {
|
||||
"country": "us",
|
||||
},
|
||||
}
|
||||
result = parse_profile_settings(raw)
|
||||
assert result.preferences.country == "US"
|
||||
|
||||
def test_profile_settings_v1_model_dump(self) -> None:
|
||||
settings = ProfileSettingsV1(
|
||||
preferences=PreferenceSettings(
|
||||
language="en-US",
|
||||
timezone="UTC",
|
||||
country="US",
|
||||
),
|
||||
notification=NotificationSettings(
|
||||
allow_notifications=True,
|
||||
|
||||
Reference in New Issue
Block a user