feat(auth): add wechat miniapp customer phone login
Implement Wechat Mini Program phone number login flow for end customers: - add PhoneLoginIn Pydantic request schema - create wechat.py module for Wechat API interactions and phone code exchange - add customer JWT utilities and require_customer authentication dependency - add new public API endpoints: /api/public/auth/phone-login and /api/public/auth/me - add required environment config variables and update example .env - add comprehensive test cases for the new auth flow and endpoints
This commit is contained in:
@@ -5,11 +5,12 @@ from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app import serializers
|
||||
from app.auth import hash_password, require_admin
|
||||
from app.auth import create_customer_token, hash_password, require_admin
|
||||
from app.database import get_db
|
||||
from app.main import create_app
|
||||
from app.models import AdminUser, Campaign, CtaBanner, DemandFeatureCard, DemandForm, DemandHero, DemandRecommendation, DemandRecommendationProduct, Destination, DestinationAlias, DestinationHero, DestinationRegion, HeroSlide, HotelGroup, Lead, MapImage, MediaAsset, Product, ProductImage, RouteSection, RouteSectionProduct, ThemeCard, VehicleOption
|
||||
from app.models import AdminUser, Campaign, CtaBanner, Customer, DemandFeatureCard, DemandForm, DemandHero, DemandRecommendation, DemandRecommendationProduct, Destination, DestinationAlias, DestinationHero, DestinationRegion, HeroSlide, HotelGroup, Lead, MapImage, MediaAsset, Product, ProductImage, RouteSection, RouteSectionProduct, ThemeCard, VehicleOption
|
||||
from app.routers import admin as admin_router
|
||||
from app.routers import public as public_router
|
||||
from app.routers.admin import normalize_detail_sections, normalize_images
|
||||
from app.routers.shared import site_config
|
||||
from app.schemas import (
|
||||
@@ -475,6 +476,65 @@ def test_public_leads_endpoint_accepts_date_only_and_returns_minimal_response():
|
||||
assert fake_db.added[0].travelDate == datetime(2027, 1, 1)
|
||||
|
||||
|
||||
def test_public_phone_login_creates_customer_and_returns_masked_session(monkeypatch):
|
||||
monkeypatch.setattr(public_router, "exchange_phone_code", lambda code: "10000000000", raising=False)
|
||||
fake_db = FakeDb(scalar_values=[None])
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_db] = lambda: fake_db
|
||||
|
||||
try:
|
||||
response = TestClient(app).post("/api/public/auth/phone-login", json={"code": "phone-code"})
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["token"]
|
||||
assert body["customer"] == {"id": "customer-test-id", "phoneMasked": "100****0000"}
|
||||
assert fake_db.added[0].phone == "10000000000"
|
||||
assert fake_db.committed
|
||||
|
||||
|
||||
def test_public_phone_login_requires_wechat_configuration(monkeypatch):
|
||||
def missing_config(_code):
|
||||
raise public_router.WechatConfigError("微信小程序登录未配置")
|
||||
|
||||
monkeypatch.setattr(public_router, "exchange_phone_code", missing_config, raising=False)
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_db] = lambda: FakeDb()
|
||||
|
||||
try:
|
||||
response = TestClient(app).post("/api/public/auth/phone-login", json={"code": "phone-code"})
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.json() == {"message": "微信小程序登录未配置"}
|
||||
|
||||
|
||||
def test_public_me_returns_customer_for_customer_token():
|
||||
customer = Customer(id="customer-test", phone="10000000000")
|
||||
fake_db = FakeDb(get_result=customer)
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_db] = lambda: fake_db
|
||||
token = create_customer_token(customer)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/public/auth/me", headers={"Authorization": f"Bearer {token}"})
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"id": "customer-test", "phoneMasked": "100****0000"}
|
||||
|
||||
|
||||
def test_public_me_requires_customer_token():
|
||||
response = TestClient(create_app()).get("/api/public/auth/me")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {"message": "请先登录"}
|
||||
|
||||
|
||||
def test_admin_destinations_endpoint_uses_precomputed_product_counts():
|
||||
destination = make_destination()
|
||||
fake_db = FakeDb(scalar_results=[[destination]], execute_results=[[(destination.id, 7)]])
|
||||
|
||||
@@ -13,3 +13,15 @@ def test_oss_settings_are_loaded_from_prefixed_environment(monkeypatch):
|
||||
assert settings.oss_access_key_secret == "example-access-key-secret"
|
||||
assert settings.oss_endpoint == "oss-cn-example.aliyuncs.com"
|
||||
assert settings.oss_bucket_name == "example-bucket"
|
||||
|
||||
|
||||
def test_wechat_miniapp_settings_are_loaded_from_environment(monkeypatch):
|
||||
monkeypatch.setenv("WECHAT_MINIAPP_APPID", "wx-test-appid")
|
||||
monkeypatch.setenv("WECHAT_MINIAPP_SECRET", "test-secret")
|
||||
monkeypatch.setenv("CUSTOMER_JWT_EXPIRES_HOURS", "24")
|
||||
|
||||
settings = Settings(_env_file=None)
|
||||
|
||||
assert settings.wechat_miniapp_appid == "wx-test-appid"
|
||||
assert settings.wechat_miniapp_secret == "test-secret"
|
||||
assert settings.customer_jwt_expires_hours == 24
|
||||
|
||||
Reference in New Issue
Block a user