feat: 完善芽说英语品牌Logo与图标配置,完成跨平台云端同步服务开发与自动化部署
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
芽说英语 (SpeakSprout) - 同步服务自动化端到端测试
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
# Use temporary database for tests
|
||||
temp_db = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||
os.environ["DATABASE_PATH"] = temp_db.name
|
||||
os.environ["JWT_SECRET"] = "test-secret-key-1234567890-secure-32b"
|
||||
|
||||
from main import app, init_db
|
||||
|
||||
async def test_full_sync_flow():
|
||||
await init_db()
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
# 1. Health check
|
||||
res = await ac.get("/api/v1/health")
|
||||
assert res.status_code == 200
|
||||
assert res.json()["status"] == "ok"
|
||||
|
||||
# 2. Register
|
||||
reg_res = await ac.post("/api/v1/auth/register", json={
|
||||
"username": "test_user_01",
|
||||
"password": "Password123!",
|
||||
"device_name": "Test iPhone"
|
||||
})
|
||||
assert reg_res.status_code == 200
|
||||
reg_data = reg_res.json()
|
||||
assert reg_data["code"] == 0
|
||||
token = reg_data["data"]["token"]
|
||||
user_id = reg_data["data"]["user_id"]
|
||||
assert token and user_id
|
||||
|
||||
# 3. Duplicate register should fail
|
||||
dup_res = await ac.post("/api/v1/auth/register", json={
|
||||
"username": "test_user_01",
|
||||
"password": "Password123!"
|
||||
})
|
||||
assert dup_res.status_code == 400
|
||||
|
||||
# 4. Login
|
||||
login_res = await ac.post("/api/v1/auth/login", json={
|
||||
"username": "test_user_01",
|
||||
"password": "Password123!",
|
||||
"device_name": "Test Mac"
|
||||
})
|
||||
assert login_res.status_code == 200
|
||||
assert login_res.json()["data"]["token"]
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# 5. Push progress from Device 1 (iPhone)
|
||||
push_1 = await ac.post("/api/v1/sync/push", headers=headers, json={
|
||||
"client_time": "2026-09-16T10:00:00Z",
|
||||
"device_name": "Test iPhone",
|
||||
"progress": {
|
||||
"active_lesson_id": "a0-02",
|
||||
"completed_lesson_ids": ["a0-01"],
|
||||
"completed_segment_ids": ["a0-01-a", "a0-01-b"],
|
||||
"active_step": "speaking",
|
||||
"streak_days": 1,
|
||||
"updated_at": "2026-09-16T10:00:00Z"
|
||||
},
|
||||
"mastery_updates": [
|
||||
{
|
||||
"item_id": "A0-W01",
|
||||
"checkpoint": 1,
|
||||
"status": "learning",
|
||||
"due_at": "2026-09-17T10:00:00Z",
|
||||
"successful_reviews": 1,
|
||||
"attempts": 1,
|
||||
"updated_at": "2026-09-16T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"profile": {
|
||||
"daily_minutes": 20,
|
||||
"show_chinese_hints": True,
|
||||
"updated_at": "2026-09-16T10:00:00Z"
|
||||
}
|
||||
})
|
||||
assert push_1.status_code == 200
|
||||
assert push_1.json()["data"]["synced_mastery_count"] == 1
|
||||
|
||||
# 6. Push progress from Device 2 (Mac) with non-overlapping lesson and higher checkpoint
|
||||
push_2 = await ac.post("/api/v1/sync/push", headers=headers, json={
|
||||
"client_time": "2026-09-16T12:00:00Z",
|
||||
"device_name": "Test Mac",
|
||||
"progress": {
|
||||
"active_lesson_id": "a0-03",
|
||||
"completed_lesson_ids": ["a0-02"],
|
||||
"completed_segment_ids": ["a0-02-a"],
|
||||
"active_step": "dialogue",
|
||||
"streak_days": 2,
|
||||
"updated_at": "2026-09-16T12:00:00Z"
|
||||
},
|
||||
"mastery_updates": [
|
||||
{
|
||||
"item_id": "A0-W01",
|
||||
"checkpoint": 2, # Upgraded checkpoint
|
||||
"status": "reviewing",
|
||||
"due_at": "2026-09-19T12:00:00Z",
|
||||
"successful_reviews": 2,
|
||||
"attempts": 2,
|
||||
"updated_at": "2026-09-16T12:00:00Z"
|
||||
},
|
||||
{
|
||||
"item_id": "A0-W02",
|
||||
"checkpoint": 1,
|
||||
"status": "learning",
|
||||
"due_at": "2026-09-17T12:00:00Z",
|
||||
"successful_reviews": 1,
|
||||
"attempts": 1,
|
||||
"updated_at": "2026-09-16T12:00:00Z"
|
||||
}
|
||||
]
|
||||
})
|
||||
assert push_2.status_code == 200
|
||||
|
||||
# 7. Pull merged progress
|
||||
pull_res = await ac.get("/api/v1/sync/pull", headers=headers)
|
||||
assert pull_res.status_code == 200
|
||||
p_data = pull_res.json()["data"]
|
||||
|
||||
# Verify Union of completed lessons: ["a0-01", "a0-02"]
|
||||
assert "a0-01" in p_data["progress"]["completed_lesson_ids"]
|
||||
assert "a0-02" in p_data["progress"]["completed_lesson_ids"]
|
||||
assert p_data["progress"]["active_lesson_id"] == "a0-03"
|
||||
|
||||
# Verify Mastery items have merged highest checkpoint
|
||||
mastery_map = {item["item_id"]: item for item in p_data["mastery_updates"]}
|
||||
assert mastery_map["A0-W01"]["checkpoint"] == 2
|
||||
assert mastery_map["A0-W02"]["checkpoint"] == 1
|
||||
|
||||
print("\n✅ All Backend Sync API tests passed with flying colors!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(test_full_sync_flow())
|
||||
Reference in New Issue
Block a user