feat: 修复 H5 底部导航覆盖 + 更新项目进度文档
## H5 底部导航修复 (Bug #10) - 精简 App.vue,移除重复 tabbar,仅保留全局样式 - uni-page 设置 height: calc(100% - 50px) + overflow-y: auto - 内容区域精确停在底部导航上方,独立滚动不再叠加 - 恢复 custom-tab-bar 组件 ## 项目进度文档 - PROGRESS.md 更新至 10 个 Bug 修复 - 新增 H5 底部导航修复记录 - 新增历史变更条目
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from unittest.mock import patch, AsyncMock
|
||||
from app.models.customer import Conversation, Message
|
||||
from app.models.quotation import Quotation, QuotationItem
|
||||
from app.models.user import Product
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TestTranslateAPI:
|
||||
async def test_translate_unauthorized(self, client: AsyncClient):
|
||||
response = await client.post(
|
||||
"/api/v1/translate",
|
||||
json={"text": "Hello", "target_lang": "zh"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_translate_success(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.translation.TranslationService.translate") as mock:
|
||||
mock.return_value = {
|
||||
"translated_text": "你好",
|
||||
"source_lang": "en",
|
||||
"provider_used": "mock",
|
||||
"from_cache": False,
|
||||
}
|
||||
response = await client.post(
|
||||
"/api/v1/translate",
|
||||
headers=auth_headers,
|
||||
json={"text": "Hello", "target_lang": "zh"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["translated_text"] == "你好"
|
||||
|
||||
async def test_translate_with_context(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.translation.TranslationService.translate") as mock:
|
||||
mock.return_value = {
|
||||
"translated_text": "FOB 上海 价格",
|
||||
"source_lang": "en",
|
||||
"provider_used": "mock",
|
||||
}
|
||||
response = await client.post(
|
||||
"/api/v1/translate",
|
||||
headers=auth_headers,
|
||||
json={"text": "FOB Shanghai price", "target_lang": "zh", "context": "trade"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestReplyAPI:
|
||||
async def test_reply_unauthorized(self, client: AsyncClient):
|
||||
response = await client.post(
|
||||
"/api/v1/translate/reply",
|
||||
json={"inquiry": "How much?", "tone": "professional"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_reply_success(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.translation.TranslationService.generate_reply") as mock:
|
||||
mock.return_value = [
|
||||
{"reply": "Thank you for your inquiry.", "tone": "professional", "provider": "mock"},
|
||||
{"reply": "Thanks for reaching out!", "tone": "friendly", "provider": "mock"},
|
||||
]
|
||||
response = await client.post(
|
||||
"/api/v1/translate/reply",
|
||||
headers=auth_headers,
|
||||
json={"inquiry": "How much for 500 units?", "tone": "professional", "count": 2},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["suggestions"]) == 2
|
||||
assert data["count"] == 2
|
||||
|
||||
async def test_reply_with_context(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.translation.TranslationService.generate_reply") as mock:
|
||||
mock.return_value = [{"reply": "Our price is $10/unit.", "tone": "professional", "provider": "mock"}]
|
||||
response = await client.post(
|
||||
"/api/v1/translate/reply",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"inquiry": "Price?",
|
||||
"tone": "professional",
|
||||
"count": 1,
|
||||
"context": {"product": "Widget X", "price": "$10"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestExtractAPI:
|
||||
async def test_extract_unauthorized(self, client: AsyncClient):
|
||||
response = await client.post(
|
||||
"/api/v1/translate/extract",
|
||||
json={"text": "I want 500pcs of red widgets FOB Shanghai", "extract_type": "inquiry"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_extract_success(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.translation.TranslationService.extract_info") as mock:
|
||||
mock.return_value = {
|
||||
"intent": "purchase",
|
||||
"product_interest": "widgets",
|
||||
"quantity": "500",
|
||||
}
|
||||
response = await client.post(
|
||||
"/api/v1/translate/extract",
|
||||
headers=auth_headers,
|
||||
json={"text": "I want 500pcs of red widgets FOB Shanghai", "extract_type": "inquiry"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "extracted" in data
|
||||
|
||||
|
||||
class TestTTSAPI:
|
||||
async def test_tts_get_unauthorized(self, client: AsyncClient):
|
||||
response = await client.get("/api/v1/translate/tts?text=hello&lang=en")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_tts_get_empty_text(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/translate/tts?text=&lang=en", headers=auth_headers)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestMarketingAPI:
|
||||
async def test_marketing_unauthorized(self, client: AsyncClient):
|
||||
response = await client.post(
|
||||
"/api/v1/marketing/generate",
|
||||
json={"product_name": "Widget", "description": "A great widget", "target": "US buyers"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_marketing_success(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.marketing.MarketingService.generate") as mock:
|
||||
mock.return_value = [
|
||||
{"content": "Buy our widget!", "style": "professional", "provider": "mock"},
|
||||
]
|
||||
response = await client.post(
|
||||
"/api/v1/marketing/generate",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"product_name": "Widget X",
|
||||
"description": "High quality widget",
|
||||
"category": "tools",
|
||||
"target": "US importers",
|
||||
"style": "professional",
|
||||
"count": 1,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["count"] >= 1
|
||||
assert "results" in data
|
||||
|
||||
async def test_marketing_keywords(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.marketing.MarketingService.generate_keywords") as mock:
|
||||
mock.return_value = ["widget", "tool", "quality"]
|
||||
response = await client.post(
|
||||
"/api/v1/marketing/keywords",
|
||||
headers=auth_headers,
|
||||
json={"product_name": "Widget", "description": "A widget", "count": 5},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "keywords" in response.json()
|
||||
|
||||
|
||||
class TestProductAPI:
|
||||
async def test_create_product(self, client: AsyncClient, auth_headers):
|
||||
response = await client.post(
|
||||
"/api/v1/products",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"name": "Test Product",
|
||||
"description": "A test product",
|
||||
"category": "electronics",
|
||||
"price": "10.50",
|
||||
"price_unit": "USD",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["name"] == "Test Product"
|
||||
|
||||
async def test_list_products(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/products", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
|
||||
async def test_update_product(self, client: AsyncClient, auth_headers, db_session, test_user):
|
||||
product = Product(
|
||||
user_id=test_user.id,
|
||||
name="Old Name",
|
||||
category="tools",
|
||||
is_active=True,
|
||||
)
|
||||
db_session.add(product)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.patch(
|
||||
f"/api/v1/products/{product.id}",
|
||||
headers=auth_headers,
|
||||
json={"name": "New Name", "price": "20.00"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "New Name"
|
||||
|
||||
async def test_delete_product(self, client: AsyncClient, auth_headers, db_session, test_user):
|
||||
product = Product(user_id=test_user.id, name="To Delete")
|
||||
db_session.add(product)
|
||||
await db_session.commit()
|
||||
pid = product.id
|
||||
|
||||
response = await client.delete(f"/api/v1/products/{pid}", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = await client.get(f"/api/v1/products/{pid}", headers=auth_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestQuotationAPI:
|
||||
async def test_create_quotation(self, client: AsyncClient, auth_headers, db_session, test_user):
|
||||
from app.models.customer import Customer
|
||||
customer = Customer(user_id=test_user.id, name="Test Buyer")
|
||||
db_session.add(customer)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/quotations",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"customer_id": str(customer.id),
|
||||
"title": "Test Quote",
|
||||
"items": [
|
||||
{"product_name": "Widget", "quantity": 100, "unit_price": 10.0},
|
||||
],
|
||||
"currency": "USD",
|
||||
"payment_terms": "T/T",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["title"] == "Test Quote"
|
||||
assert len(data["items"]) == 1
|
||||
|
||||
async def test_list_quotations(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/quotations", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert "items" in response.json()
|
||||
|
||||
async def test_quotation_pdf_not_found(self, client: AsyncClient, auth_headers):
|
||||
import uuid
|
||||
response = await client.get(f"/api/v1/quotations/{uuid.uuid4()}/pdf", headers=auth_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_quotation_status_update(self, client: AsyncClient, auth_headers, db_session, test_user):
|
||||
from app.models.customer import Customer
|
||||
customer = Customer(user_id=test_user.id, name="Status Test Buyer")
|
||||
db_session.add(customer)
|
||||
await db_session.commit()
|
||||
|
||||
q = Quotation(user_id=test_user.id, customer_id=customer.id, title="Status Test", status="draft")
|
||||
db_session.add(q)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.patch(
|
||||
f"/api/v1/quotations/{q.id}/status",
|
||||
headers=auth_headers,
|
||||
json={"status": "sent"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "sent"
|
||||
|
||||
|
||||
class TestAnalyticsAPI:
|
||||
async def test_overview(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/analytics/overview", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "customers" in data
|
||||
assert "translations" in data
|
||||
assert "quotations" in data
|
||||
assert "messages" in data
|
||||
assert "marketing" in data
|
||||
|
||||
async def test_customer_analytics(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/analytics/customers", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_marketing_analytics(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/analytics/marketing", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total_events" in data
|
||||
|
||||
|
||||
class TestOnboardingAPI:
|
||||
async def test_onboarding_status(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/onboarding/status", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "completed" in data
|
||||
assert "product_count" in data
|
||||
|
||||
async def test_onboarding_create_product(self, client: AsyncClient, auth_headers):
|
||||
with patch("app.services.onboarding.OnboardingService.create_product") as mock:
|
||||
mock.return_value = {
|
||||
"id": "mock-id",
|
||||
"name": "Onboarded Product",
|
||||
"marketing_contents": [],
|
||||
"keywords": [],
|
||||
}
|
||||
response = await client.post(
|
||||
"/api/v1/onboarding/product",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"name": "New Product",
|
||||
"description": "Desc",
|
||||
"category": "tools",
|
||||
"target": "US buyers",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Onboarded Product"
|
||||
|
||||
|
||||
class TestExportAPI:
|
||||
async def test_export_customers_csv(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/customers/export/csv", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "text/csv"
|
||||
assert "customers.csv" in response.headers["content-disposition"]
|
||||
|
||||
async def test_export_quotations_csv(self, client: AsyncClient, auth_headers):
|
||||
response = await client.get("/api/v1/quotations/export/csv", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "text/csv"
|
||||
Reference in New Issue
Block a user