"""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 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) -> MagicMock: m = MagicMock() m.status_code = status_code m.json.return_value = json_data return m def _set_auth(client, role: str = "user"): with client.session_transaction() as sess: sess["access_token"] = "tok" sess["refresh_token"] = "ref" sess["user_role"] = role sess["user_email"] = "u@example.com" # --------------------------------------------------------------------------- # 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): _set_auth(client) 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): login_mock = _mock_response( 200, {"access_token": "acc", "refresh_token": "ref"}) me_mock = _mock_response( 200, {"id": "1", "email": "u@example.com", "role": "user"}) with patch("frontend.app.main.httpx.request", side_effect=[login_mock, me_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_stores_role_in_session(client): login_mock = _mock_response( 200, {"access_token": "acc", "refresh_token": "ref"}) me_mock = _mock_response( 200, {"id": "1", "email": "admin@example.com", "role": "admin"}) with patch("frontend.app.main.httpx.request", side_effect=[login_mock, me_mock]): client.post( "/login", data={"email": "admin@example.com", "password": "secret"}) with client.session_transaction() as sess: assert sess["user_role"] == "admin" 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): _set_auth(client) 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): _set_auth(client) me_mock = _mock_response( 200, {"id": "1", "email": "u@example.com", "role": "user"}) images_mock = _mock_response(200, []) gen_images_mock = _mock_response(200, []) gen_videos_mock = _mock_response(200, []) with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock, gen_videos_mock]): resp = client.get("/dashboard") assert resp.status_code == 200 assert b"u@example.com" in resp.data # --------------------------------------------------------------------------- # Generate — redirect + separate pages # --------------------------------------------------------------------------- def test_generate_redirects_to_text(client): _set_auth(client) resp = client.get("/generate") assert resp.status_code == 302 assert "/generate/text" in resp.headers["Location"] def test_generate_text_page_renders(client): _set_auth(client) resp = client.get("/generate/text") assert resp.status_code == 200 assert b"Text Chat" in resp.data def test_generate_text_requires_login(client): resp = client.get("/generate/text") assert resp.status_code == 302 assert "/login" in resp.headers["Location"] def test_generate_text_success(client): _set_auth(client) gen_mock = _mock_response( 200, {"id": "g1", "model": "openai/gpt-4o", "content": "Hello world", "usage": None}) models_mock = _mock_response(200, [ {"id": "openai/gpt-4o", "name": "GPT-4o", "modality": "text"} ]) with patch("frontend.app.main.httpx.request", side_effect=[gen_mock, models_mock]): resp = client.post( "/generate/text", data={"model": "openai/gpt-4o", "prompt": "Say hello", "action": "send"}) assert resp.status_code == 200 assert b"Hello world" in resp.data assert b"chat-bubble--assistant" in resp.data def test_generate_text_page_shows_optional_system_prompt(client): _set_auth(client) models_mock = _mock_response(200, []) with patch("frontend.app.main.httpx.request", return_value=models_mock): resp = client.get("/generate/text") assert resp.status_code == 200 assert b"System prompt (optional)" in resp.data assert b'name="system_prompt"' in resp.data def test_generate_text_forwards_system_prompt(client): _set_auth(client) gen_mock = _mock_response( 200, {"id": "g1", "model": "openai/gpt-4o", "content": "Hello world", "usage": None}) models_mock = _mock_response(200, [ {"id": "openai/gpt-4o", "name": "GPT-4o", "modality": "text"} ]) with patch("frontend.app.main.httpx.request", side_effect=[gen_mock, models_mock]) as mock_request: resp = client.post( "/generate/text", data={ "model": "openai/gpt-4o", "prompt": "Say hello", "system_prompt": "You are concise.", "action": "send", }, ) assert resp.status_code == 200 first_call_kwargs = mock_request.call_args_list[0].kwargs assert first_call_kwargs["json"]["system_prompt"] == "You are concise." # Messages array sent (not bare prompt) assert "messages" in first_call_kwargs["json"] def test_generate_text_chat_history_accumulates(client): """Second message includes prior user+assistant turns in messages array.""" _set_auth(client) turn1_gen = _mock_response( 200, {"id": "g1", "model": "openai/gpt-4o", "content": "Turn 1 reply", "usage": None}) turn1_models = _mock_response( 200, [{"id": "openai/gpt-4o", "name": "GPT-4o", "modality": "text"}]) turn2_gen = _mock_response( 200, {"id": "g2", "model": "openai/gpt-4o", "content": "Turn 2 reply", "usage": None}) turn2_models = _mock_response( 200, [{"id": "openai/gpt-4o", "name": "GPT-4o", "modality": "text"}]) with patch("frontend.app.main.httpx.request", side_effect=[turn1_gen, turn1_models]): client.post( "/generate/text", data={"model": "openai/gpt-4o", "prompt": "First", "action": "send"}) with patch("frontend.app.main.httpx.request", side_effect=[turn2_gen, turn2_models]) as mock_req: resp = client.post( "/generate/text", data={"model": "openai/gpt-4o", "prompt": "Second", "action": "send"}) assert resp.status_code == 200 assert b"Turn 1 reply" in resp.data assert b"Turn 2 reply" in resp.data # Backend received 3 messages: First(user), Turn1(assistant), Second(user) sent_messages = mock_req.call_args_list[0].kwargs["json"]["messages"] assert len(sent_messages) == 3 assert sent_messages[0]["role"] == "user" and sent_messages[0]["content"] == "First" assert sent_messages[1]["role"] == "assistant" assert sent_messages[2]["role"] == "user" and sent_messages[2]["content"] == "Second" def test_generate_text_clear_resets_history(client): """Clear action removes session history and redirects.""" _set_auth(client) gen_mock = _mock_response( 200, {"id": "g1", "model": "openai/gpt-4o", "content": "Reply", "usage": None}) models_mock = _mock_response( 200, [{"id": "openai/gpt-4o", "name": "GPT-4o", "modality": "text"}]) with patch("frontend.app.main.httpx.request", side_effect=[gen_mock, models_mock]): client.post( "/generate/text", data={"model": "openai/gpt-4o", "prompt": "Hi", "action": "send"}) clear_resp = client.post("/generate/text", data={"action": "clear"}) assert clear_resp.status_code == 302 models_mock2 = _mock_response( 200, [{"id": "openai/gpt-4o", "name": "GPT-4o", "modality": "text"}]) with patch("frontend.app.main.httpx.request", return_value=models_mock2): get_resp = client.get("/generate/text") assert b"No messages yet" in get_resp.data def test_generate_image_page_renders(client): _set_auth(client) resp = client.get("/generate/image") assert resp.status_code == 200 assert b"Image Generation" in resp.data assert b"reference_image" in resp.data def test_generate_image_success(client): _set_auth(client) 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/image", data={ "model": "openai/dall-e-3", "prompt": "A cat", "n": "1", "size": "1024x1024" }) assert resp.status_code == 200 assert b"example.com/img.png" in resp.data def test_generate_video_page_renders(client): _set_auth(client) resp = client.get("/generate/video") assert resp.status_code == 200 assert b"Video Generation" in resp.data def test_generate_video_text_mode(client): _set_auth(client) mock = _mock_response( 200, {"id": "v1", "model": "openai/sora-2-pro", "status": "queued"}) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.post("/generate/video", data={ "mode": "text", "model": "openai/sora-2-pro", "prompt": "A sunset", "aspect_ratio": "16:9", "duration_seconds": "10", "resolution": "720p", }) assert resp.status_code == 200 assert b"queued" in resp.data def test_generate_video_image_mode(client): _set_auth(client) mock = _mock_response( 200, {"id": "v2", "model": "openai/sora-2-pro", "status": "processing"}) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.post("/generate/video", data={ "mode": "image", "model": "openai/sora-2-pro", "image_url": "https://example.com/img.png", "prompt": "Pan right", "aspect_ratio": "16:9" }) assert resp.status_code == 200 assert b"processing" in resp.data def test_generate_upstream_error_shows_message(client): _set_auth(client) gen_mock = _mock_response(502, {"detail": "OpenRouter error: timeout"}) models_mock = _mock_response(200, [ {"id": "openai/gpt-4o", "name": "GPT-4o", "modality": "text"} ]) with patch("frontend.app.main.httpx.request", side_effect=[gen_mock, models_mock]): resp = client.post( "/generate/text", data={"model": "openai/gpt-4o", "prompt": "Hi"}) assert resp.status_code == 200 assert b"OpenRouter error" in resp.data # --------------------------------------------------------------------------- # Admin # --------------------------------------------------------------------------- def test_admin_requires_login(client): resp = client.get("/admin") assert resp.status_code == 302 assert "/login" in resp.headers["Location"] def test_admin_requires_admin_role(client): _set_auth(client, role="user") resp = client.get("/admin") assert resp.status_code == 302 assert "/dashboard" in resp.headers["Location"] def test_admin_page_renders(client): _set_auth(client, role="admin") stats_mock = _mock_response( 200, {"total_users": 5, "active_refresh_tokens": 3, "admin_users": 1}) users_mock = _mock_response(200, [ {"id": "u1", "email": "a@example.com", "role": "admin"}, {"id": "u2", "email": "b@example.com", "role": "user"}, ]) with patch("frontend.app.main.httpx.request", side_effect=[stats_mock, users_mock]): resp = client.get("/admin") assert resp.status_code == 200 assert b"a@example.com" in resp.data assert b"b@example.com" in resp.data def test_admin_set_role(client): _set_auth(client, role="admin") mock = _mock_response( 200, {"id": "u2", "email": "b@example.com", "role": "admin"}) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.post("/admin/users/u2/role", data={"role": "admin"}) assert resp.status_code == 302 assert "/admin" in resp.headers["Location"] def test_admin_delete_user(client): _set_auth(client, role="admin") mock = _mock_response(204, {}) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.post("/admin/users/u2/delete") assert resp.status_code == 302 assert "/admin" in resp.headers["Location"] # --------------------------------------------------------------------------- # Profile # --------------------------------------------------------------------------- def test_profile_requires_login(client): resp = client.get("/users/profile") assert resp.status_code == 302 assert "/login" in resp.headers["Location"] def test_profile_page_renders(client): _set_auth(client) 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("/users/profile") assert resp.status_code == 200 assert b"Profile" in resp.data assert b"u@example.com" in resp.data def test_profile_update_email(client): _set_auth(client) mock = _mock_response( 200, {"id": "1", "email": "new@example.com", "role": "user"}) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.post( "/users/profile", data={"email": "new@example.com", "password": ""}) assert resp.status_code == 302 assert "/users/profile" in resp.headers["Location"] def test_profile_update_failure(client): _set_auth(client) mock = _mock_response(422, {"detail": "Invalid email."}) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.post( "/users/profile", data={"email": "bad", "password": ""}) # redirects regardless, flash message shown on next GET assert resp.status_code == 302 # --------------------------------------------------------------------------- # GET /generate/video/status (polling proxy) # --------------------------------------------------------------------------- def test_video_status_proxy_completed(client): _set_auth(client) mock = _mock_response(200, { "id": "v1", "model": "", "status": "completed", "video_url": "https://example.com/video.mp4", "unsigned_urls": ["https://example.com/video.mp4"], }) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.get( "/generate/video/status", query_string={ "polling_url": "https://openrouter.ai/api/v1/videos/v1"}, ) assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "completed" assert data["video_url"] == "https://example.com/video.mp4" def test_video_status_proxy_processing(client): _set_auth(client) mock = _mock_response( 200, {"id": "v1", "model": "", "status": "processing"}) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.get( "/generate/video/status", query_string={ "polling_url": "https://openrouter.ai/api/v1/videos/v1"}, ) assert resp.status_code == 200 assert resp.get_json()["status"] == "processing" def test_video_status_proxy_requires_login(client): resp = client.get( "/generate/video/status", query_string={"polling_url": "https://openrouter.ai/api/v1/videos/v1"}, ) assert resp.status_code == 302 assert "/login" in resp.headers["Location"] def test_video_status_proxy_missing_url(client): _set_auth(client) resp = client.get("/generate/video/status") assert resp.status_code == 400 assert b"polling_url" in resp.data def test_video_generate_renders_polling_ui(client): """When response has polling_url, template shows polling div.""" _set_auth(client) mock = _mock_response(200, { "id": "v1", "model": "openai/sora-2-pro", "status": "queued", "polling_url": "https://openrouter.ai/api/v1/videos/v1", }) with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.post("/generate/video", data={ "mode": "text", "model": "openai/sora-2-pro", "prompt": "A sunset", "aspect_ratio": "16:9", }) assert resp.status_code == 200 assert b"video-poll-status" in resp.data # --------------------------------------------------------------------------- # Image upload — frontend proxy + dashboard # --------------------------------------------------------------------------- def test_dashboard_shows_uploaded_images(client): _set_auth(client) me_mock = _mock_response( 200, {"id": "1", "email": "u@example.com", "role": "user"}) images_mock = _mock_response(200, [ {"id": "img-1", "filename": "cat.png", "content_type": "image/png", "size_bytes": 1024, "created_at": "2026-04-29T10:00:00"}, ]) gen_images_mock = _mock_response(200, []) gen_videos_mock = _mock_response(200, []) with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock, gen_videos_mock]): resp = client.get("/dashboard") assert resp.status_code == 200 assert b"cat.png" in resp.data assert b"img-1" in resp.data def test_dashboard_shows_generated_images(client): _set_auth(client) me_mock = _mock_response( 200, {"id": "1", "email": "u@example.com", "role": "user"}) images_mock = _mock_response(200, []) gen_images_mock = _mock_response(200, [ { "id": "gen-1", "model_id": "google/gemini-2.5-flash-image", "prompt": "A cat on the moon", "image_data": "data:image/png;base64,abc123", "created_at": "2026-04-29T10:00:00", } ]) gen_videos_mock = _mock_response(200, []) with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock, gen_videos_mock]): resp = client.get("/dashboard") assert resp.status_code == 200 assert b"Generated images" in resp.data assert b"A cat on the moon" in resp.data assert b"data:image/png;base64,abc123" in resp.data def test_dashboard_no_images_section_when_empty(client): _set_auth(client) me_mock = _mock_response( 200, {"id": "1", "email": "u@example.com", "role": "user"}) images_mock = _mock_response(200, []) gen_images_mock = _mock_response(200, []) gen_videos_mock = _mock_response(200, []) with patch("frontend.app.main.httpx.request", side_effect=[me_mock, images_mock, gen_images_mock, gen_videos_mock]): resp = client.get("/dashboard") assert resp.status_code == 200 assert b"Uploaded reference images" not in resp.data def test_serve_uploaded_image_proxy(client): _set_auth(client) img_bytes = b"\x89PNG\r\n\x1a\n" mock = MagicMock() mock.status_code = 200 mock.content = img_bytes mock.headers = {"content-type": "image/png"} with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.get("/images/img-1/file") assert resp.status_code == 200 assert resp.content_type == "image/png" assert resp.data == img_bytes def test_serve_uploaded_image_requires_login(client): resp = client.get("/images/img-1/file") assert resp.status_code == 302 assert "/login" in resp.headers["Location"] def test_serve_uploaded_image_not_found_proxied(client): _set_auth(client) mock = _mock_response(404, {"detail": "Image not found."}) mock.content = b"" with patch("frontend.app.main.httpx.request", return_value=mock): resp = client.get("/images/bad-id/file") assert resp.status_code == 404 def test_generate_image_uploads_reference_then_generates(client): _set_auth(client) gen_mock = _mock_response(200, { "id": "g2", "model": "openai/dall-e-3", "images": [{"url": "https://example.com/out.png", "revised_prompt": None, "b64_json": None}] }) # No file field → upload branch skipped; only generate call is made with patch("frontend.app.main.httpx.request", return_value=gen_mock): resp = client.post("/generate/image", data={ "model": "openai/dall-e-3", "prompt": "A cat", "n": "1", "size": "1024x1024", }, content_type="multipart/form-data") assert resp.status_code == 200 assert b"example.com/out.png" in resp.data