Files
group_xinghuo_jinrong/tests/test_platform_api.py
T
zhanghongyu_0626 2b4eaa9725 feat(nav): Implement historical NAV tracking and enhance product API
- Added `list_nav_history` method in `CoreReadOnlyRepository` to retrieve historical NAV data for products over a specified number of days.
- Introduced `get_nav_history` endpoint in `product_service` to return historical NAV sequences, improving product data accessibility.
- Updated `products.py` to include new API endpoint for fetching NAV history, ensuring compliance with access control.
- Enhanced `ready.py` to include a health check for the new functionality, ensuring system readiness.
- Updated documentation to reflect the new testing baseline of 833 passed tests, indicating improved stability and functionality across the application.

This update significantly enhances the product API by providing access to historical NAV data, improving user insights into product performance.
2026-09-11 20:47:18 +08:00

266 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""代销平台 API v0.1(customers/products/advisors/staff/compliance)。"""
from __future__ import annotations
import json
from datetime import datetime, timedelta
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import text
from _ddl import create_sqlite_engine, seed_suitability_matrix
from app.config.settings import settings
from app.main import app
from app.repository.risk_repository import RiskRepository
from app.service.risk import redis_gateway
class FakeGateway:
def publish(self, channel, payload):
pass
def delete(self, *keys):
pass
CUST_A = {"X-Debug-Role": "customer", "X-Debug-Actor": "CUST-1001"}
CUST_B = {"X-Debug-Role": "customer", "X-Debug-Actor": "CUST-1002"}
ADVISOR = {"X-Debug-Role": "advisor", "X-Debug-Actor": "STAFF-10086"}
ANALYST = {"X-Debug-Role": "analyst", "X-Debug-Actor": "STAFF-20001"}
COMPLIANCE = {"X-Debug-Role": "compliance", "X-Debug-Actor": "STAFF-40001"}
OTHER_ADV = {"X-Debug-Role": "advisor", "X-Debug-Actor": "STAFF-99999"}
def _seed_core(engine) -> None:
future = (datetime.now() + timedelta(days=180)).isoformat(sep=" ")
with engine.begin() as conn:
conn.execute(
text(
"INSERT INTO core_customer (customer_id, display_name, age, is_active) "
"VALUES ('CUST-1001','CustomerA',35,1), ('CUST-1002','CustomerB',40,1)"
)
)
conn.execute(
text(
"INSERT INTO core_customer_risk (customer_id, risk_code, expires_at, investor_category) "
"VALUES ('CUST-1001','C2',:exp,'ordinary'), ('CUST-1002','C3',:exp,'ordinary')"
),
{"exp": future},
)
conn.execute(
text(
"INSERT INTO core_customer_advisor (advisor_id, customer_id, rel_status) "
"VALUES ('STAFF-10086','CUST-1001','active')"
)
)
conn.execute(
text(
"INSERT INTO core_product (product_id, product_name, min_risk_code, is_open) "
"VALUES ('PROD-001','稳健债基','R2',1), ('PROD-CLOSED','已下架','R1',0)"
)
)
conn.execute(
text(
"INSERT INTO core_holding (customer_id, product_id, market_value, quantity) "
"VALUES ('CUST-1001','PROD-001',100000,1000)"
)
)
conn.execute(
text(
"INSERT INTO core_trade (trade_id, customer_id, product_id, trade_type, amount, "
"trade_status, traded_at) VALUES ('TRD-P-001','CUST-1001','PROD-001','subscribe',"
"5000,'confirmed',datetime('now','localtime'))"
)
)
conn.execute(
text(
"INSERT INTO core_product_nav (product_id, nav_date, nav, daily_chg_pct) "
"VALUES ('PROD-001', date('now'), 1.25, 0.01), "
"('PROD-001', date('now','-1 day'), 1.24, -0.2), "
"('PROD-001', date('now','-30 day'), 1.10, 0.5)"
)
)
conn.execute(
text(
"INSERT INTO core_staff (staff_id, display_name, staff_type, roles, is_active) "
"VALUES ('STAFF-10086','王理财','advisor',:roles,1), "
"('STAFF-20001','分析员','analyst',:aroles,1)"
),
{"roles": json.dumps(["advisor"]), "aroles": json.dumps(["analyst"])},
)
@pytest.fixture()
def client(monkeypatch):
engine = create_sqlite_engine()
seed_suitability_matrix(engine)
_seed_core(engine)
repo = RiskRepository(engine=engine)
monkeypatch.setattr("app.utils.db.get_engine", lambda name=None: engine)
monkeypatch.setattr("app.repository.core_ro.get_engine", lambda name=None: engine)
from app.api import advisors as advisors_api
from app.api import compliance as compliance_api
from app.api import customers as customers_api
from app.api import deps as deps_mod
from app.api import staff as staff_api
monkeypatch.setattr(deps_mod, "RiskRepository", lambda: repo)
monkeypatch.setattr(customers_api, "_risk_repo", lambda: repo)
monkeypatch.setattr(advisors_api, "_risk_repo", lambda: repo)
monkeypatch.setattr(staff_api, "_risk_repo", lambda: repo)
monkeypatch.setattr(compliance_api, "_risk_repo", lambda: repo)
with TestClient(app) as c:
monkeypatch.setattr(redis_gateway, "_gateway", FakeGateway())
yield c
engine.dispose()
def test_get_customer_owner_ok(client):
r = client.get("/api/customers/CUST-1001", headers=CUST_A)
assert r.status_code == 200
body = r.json()
assert body["code"] == 0
assert body["data"]["customer_id"] == "CUST-1001"
assert body["data"]["display_name"] == "CustomerA"
def test_get_customer_other_forbidden(client):
r = client.get("/api/customers/CUST-1002", headers=CUST_A)
assert r.status_code == 403
assert r.json()["error_code"] == "AUTH_403_NOT_OWNER"
def test_get_customer_analyst_ok(client):
r = client.get("/api/customers/CUST-1002", headers=ANALYST)
assert r.status_code == 200
def test_get_customer_not_found(client):
r = client.get("/api/customers/CUST-404", headers=ANALYST)
assert r.status_code == 404
def test_list_holdings(client):
r = client.get("/api/customers/CUST-1001/holdings", headers=CUST_A)
assert r.status_code == 200
items = r.json()["data"]["items"]
assert len(items) == 1
assert items[0]["product_id"] == "PROD-001"
def test_list_trades(client):
r = client.get("/api/customers/CUST-1001/trades", headers=CUST_A)
assert r.status_code == 200
assert r.json()["data"]["total"] >= 1
def test_list_customer_products(client):
r = client.get("/api/customers/CUST-1001/products", headers=CUST_A)
assert r.status_code == 200
assert r.json()["data"]["total"] >= 1
def test_advisor_roster_self(client):
r = client.get("/api/advisors/STAFF-10086/customers", headers=ADVISOR)
assert r.status_code == 200
items = r.json()["data"]["items"]
assert len(items) == 1
assert items[0]["customer_id"] == "CUST-1001"
def test_advisor_roster_other_forbidden(client):
r = client.get("/api/advisors/STAFF-10086/customers", headers=OTHER_ADV)
assert r.status_code == 403
def test_list_products_excludes_closed(client):
r = client.get("/api/products", headers=ANALYST)
assert r.status_code == 200
ids = [i["product_id"] for i in r.json()["data"]["items"]]
assert "PROD-001" in ids
assert "PROD-CLOSED" not in ids
def test_get_product_and_nav(client):
r = client.get("/api/products/PROD-001", headers=CUST_A)
assert r.status_code == 200
assert r.json()["data"]["product_name"] == "稳健债基"
r2 = client.get("/api/products/PROD-001/nav", headers=CUST_A)
assert r2.status_code == 200
assert r2.json()["data"]["nav"] == 1.25
def test_nav_history_customer_ok(client):
r = client.get("/api/products/PROD-001/nav/history?days=365", headers=CUST_A)
assert r.status_code == 200
data = r.json()["data"]
assert data["product_id"] == "PROD-001"
assert data["count"] >= 3
assert len(data["items"]) >= 3
dates = [it["nav_date"] for it in data["items"]]
assert dates == sorted(dates)
def test_nav_history_analyst_ok(client):
r = client.get("/api/products/PROD-001/nav/history", headers=ANALYST)
assert r.status_code == 200
def test_nav_history_compliance_forbidden(client):
r = client.get("/api/products/PROD-001/nav/history", headers=COMPLIANCE)
assert r.status_code == 403
assert r.json()["error_code"] == "AUTH_403_SCOPE"
def test_nav_history_not_found(client):
r = client.get("/api/products/PROD-404/nav/history", headers=CUST_A)
assert r.status_code == 404
def test_staff_me(client):
r = client.get("/api/staff/me", headers=ADVISOR)
assert r.status_code == 200
assert r.json()["data"]["staff_id"] == "STAFF-10086"
assert "advisor" in r.json()["data"]["roles"]
def test_staff_me_customer_forbidden(client):
r = client.get("/api/staff/me", headers=CUST_A)
assert r.status_code == 403
def test_compliance_suitability_check(client):
r = client.post(
"/api/compliance/suitability-check",
headers=CUST_A,
json={"customer_id": "CUST-1001", "product_id": "PROD-001"},
)
assert r.status_code == 200
data = r.json()["data"]
assert data["is_matched"] is True
assert data["blocked"] is False
def test_platform_no_agent_type_header(client):
"""平台 JWT/debug 通道不要求 X-Agent-Type。"""
r = client.get("/api/customers/CUST-1001", headers=CUST_A)
assert r.status_code == 200
assert "X-Agent-Type" not in CUST_A
def test_desensitize_switch_off_by_default(client, monkeypatch):
monkeypatch.setattr(settings, "platform_response_desensitize", False)
r = client.get("/api/customers/CUST-1001", headers=CUST_A)
assert r.json()["data"]["display_name"] == "CustomerA"
def test_desensitize_switch_on(client, monkeypatch):
monkeypatch.setattr(settings, "platform_response_desensitize", True)
r = client.get("/api/customers/CUST-1001", headers=CUST_A)
assert r.json()["data"]["display_name"] == "C**"