add frontend application structure with authentication, generation features, and integration tests
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""Frontend integration tests — backend API calls are fully mocked."""
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
os.environ.setdefault("FLASK_SECRET_KEY", "test-secret")
|
||||
os.environ.setdefault("BACKEND_URL", "http://backend-mock")
|
||||
|
||||
from frontend.app.main import app # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app.config["TESTING"] = True
|
||||
app.config["WTF_CSRF_ENABLED"] = False
|
||||
with app.test_client() as c:
|
||||
yield c
|
||||
|
||||
|
||||
def _mock_response(status_code: int, json_data: dict) -> MagicMock:
|
||||
m = MagicMock()
|
||||
m.status_code = status_code
|
||||
m.json.return_value = json_data
|
||||
return m
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Index redirect
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_index_redirects_to_login(client):
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 302
|
||||
assert "/login" in resp.headers["Location"]
|
||||
|
||||
|
||||
def test_index_redirects_to_dashboard_when_logged_in(client):
|
||||
with client.session_transaction() as sess:
|
||||
sess["access_token"] = "tok"
|
||||
sess["refresh_token"] = "ref"
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 302
|
||||
assert "/dashboard" in resp.headers["Location"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Login
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_login_page_renders(client):
|
||||
resp = client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
assert b"Log in" in resp.data
|
||||
|
||||
|
||||
def test_login_success(client):
|
||||
mock = _mock_response(200, {"access_token": "acc", "refresh_token": "ref"})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.post("/login", data={"email": "u@example.com", "password": "secret"})
|
||||
assert resp.status_code == 302
|
||||
assert "/dashboard" in resp.headers["Location"]
|
||||
|
||||
|
||||
def test_login_failure_shows_error(client):
|
||||
mock = _mock_response(401, {"detail": "Invalid credentials."})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.post("/login", data={"email": "u@example.com", "password": "wrong"})
|
||||
assert resp.status_code == 200
|
||||
assert b"Invalid email or password" in resp.data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Register
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_register_page_renders(client):
|
||||
resp = client.get("/register")
|
||||
assert resp.status_code == 200
|
||||
assert b"Create account" in resp.data
|
||||
|
||||
|
||||
def test_register_success_redirects_to_login(client):
|
||||
mock = _mock_response(201, {"id": "abc", "email": "u@example.com", "role": "user"})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.post("/register", data={"email": "u@example.com", "password": "secret123"})
|
||||
assert resp.status_code == 302
|
||||
assert "/login" in resp.headers["Location"]
|
||||
|
||||
|
||||
def test_register_duplicate_shows_error(client):
|
||||
mock = _mock_response(409, {"detail": "Email already registered."})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.post("/register", data={"email": "dup@example.com", "password": "secret123"})
|
||||
assert resp.status_code == 200
|
||||
assert b"Email already registered" in resp.data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logout
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_logout_clears_session_and_redirects(client):
|
||||
with client.session_transaction() as sess:
|
||||
sess["access_token"] = "tok"
|
||||
sess["refresh_token"] = "ref"
|
||||
mock = _mock_response(204, {})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.get("/logout")
|
||||
assert resp.status_code == 302
|
||||
assert "/login" in resp.headers["Location"]
|
||||
with client.session_transaction() as sess:
|
||||
assert "access_token" not in sess
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dashboard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_dashboard_requires_login(client):
|
||||
resp = client.get("/dashboard")
|
||||
assert resp.status_code == 302
|
||||
assert "/login" in resp.headers["Location"]
|
||||
|
||||
|
||||
def test_dashboard_renders_user_info(client):
|
||||
with client.session_transaction() as sess:
|
||||
sess["access_token"] = "tok"
|
||||
mock = _mock_response(200, {"id": "1", "email": "u@example.com", "role": "user"})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.get("/dashboard")
|
||||
assert resp.status_code == 200
|
||||
assert b"u@example.com" in resp.data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_generate_page_requires_login(client):
|
||||
resp = client.get("/generate")
|
||||
assert resp.status_code == 302
|
||||
assert "/login" in resp.headers["Location"]
|
||||
|
||||
|
||||
def test_generate_page_renders(client):
|
||||
with client.session_transaction() as sess:
|
||||
sess["access_token"] = "tok"
|
||||
resp = client.get("/generate")
|
||||
assert resp.status_code == 200
|
||||
assert b"Generate" in resp.data
|
||||
|
||||
|
||||
def test_generate_text_success(client):
|
||||
with client.session_transaction() as sess:
|
||||
sess["access_token"] = "tok"
|
||||
mock = _mock_response(200, {"id": "g1", "model": "openai/gpt-4o", "content": "Hello world", "usage": None})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.post("/generate", data={
|
||||
"type": "text", "model": "openai/gpt-4o", "prompt": "Say hello"
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert b"Hello world" in resp.data
|
||||
|
||||
|
||||
def test_generate_image_success(client):
|
||||
with client.session_transaction() as sess:
|
||||
sess["access_token"] = "tok"
|
||||
mock = _mock_response(200, {
|
||||
"id": "g2", "model": "openai/dall-e-3",
|
||||
"images": [{"url": "https://example.com/img.png", "revised_prompt": None, "b64_json": None}]
|
||||
})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.post("/generate", data={
|
||||
"type": "image", "model": "openai/dall-e-3", "prompt": "A cat"
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert b"example.com/img.png" in resp.data
|
||||
|
||||
|
||||
def test_generate_upstream_error_shows_message(client):
|
||||
with client.session_transaction() as sess:
|
||||
sess["access_token"] = "tok"
|
||||
mock = _mock_response(502, {"detail": "OpenRouter error: timeout"})
|
||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||
resp = client.post("/generate", data={
|
||||
"type": "text", "model": "openai/gpt-4o", "prompt": "Hi"
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert b"OpenRouter error" in resp.data
|
||||
Reference in New Issue
Block a user