From 34bd2da3835054dd5763c285b309589bd392616e Mon Sep 17 00:00:00 2001 From: AnnuKumar Date: Fri, 5 Jun 2026 15:40:25 +0530 Subject: [PATCH] test(backend): add validation coverage for empty file uploads --- backend/tests/test_api.py | 118 +++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 22 deletions(-) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index e38105e..cc1fcb4 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -9,7 +9,6 @@ import services.db_service as db from app import app - _tmp = tempfile.mktemp(suffix=".db") db.DB_PATH = _tmp db.init_db() @@ -67,6 +66,7 @@ def test_get_messages_empty(): r2 = client.get(f"/api/sessions/{sid}/messages") assert r2.json()["count"] == 0 + def test_clear_messages(): r = client.post("/api/sessions/", json={"title": "Clear Test"}) sid = r.json()["id"] @@ -81,14 +81,27 @@ def test_upload_invalid_type(): r = client.post("/api/upload/", files=files, data={"session_id": "s1"}) assert r.status_code == 400 + def test_upload_too_large(monkeypatch): import routes.upload as up + monkeypatch.setattr(up, "MAX_BYTES", 5) files = {"file": ("big.txt", b"x" * 10, "text/plain")} r = client.post("/api/upload/", files=files, data={"session_id": "s1"}) assert r.status_code == 413 +def test_upload_empty_file(): + """ + Verify that the upload endpoint gracefully handles empty files + without causing an application or parsing crash. + """ + files = {"file": ("empty.txt", b"", "text/plain")} + r = client.post("/api/upload/", files=files, data={"session_id": "s1"}) + # It should catch the empty payload with a bad request or handle it via schemas + assert r.status_code in [400, 422, 200] + + # ─── Plugins ───────────────────────────────────────────── def test_list_plugins(): r = client.get("/api/plugins/") @@ -96,37 +109,57 @@ def test_list_plugins(): ids = [p["id"] for p in r.json()["plugins"]] assert "calculator" in ids + def test_calculator_basic(): - r = client.post("/api/plugins/run", json={"plugin":"calculator","input":"2+2"}) + r = client.post("/api/plugins/run", json={"plugin": "calculator", "input": "2+2"}) assert "4" in r.json()["output"] + def test_calculator_advanced(): - r = client.post("/api/plugins/run", json={"plugin":"calculator","input":"sqrt(144)"}) + r = client.post( + "/api/plugins/run", json={"plugin": "calculator", "input": "sqrt(144)"} + ) assert "12" in r.json()["output"] + def test_calculator_blocked(): - r = client.post("/api/plugins/run", json={"plugin":"calculator","input":"__import__('os')"}) + r = client.post( + "/api/plugins/run", json={"plugin": "calculator", "input": "__import__('os')"} + ) assert "Unsafe" in r.json()["output"] or not r.json()["success"] + def test_wordcount(): - r = client.post("/api/plugins/run", json={"plugin":"wordcount","input":"hello world foo bar"}) + r = client.post( + "/api/plugins/run", json={"plugin": "wordcount", "input": "hello world foo bar"} + ) assert "Words: 4" in r.json()["output"] + def test_jsonformat_valid(): - r = client.post("/api/plugins/run", json={"plugin":"jsonformat","input":'{"a":1}'}) + r = client.post( + "/api/plugins/run", json={"plugin": "jsonformat", "input": '{"a":1}'} + ) assert '"a"' in r.json()["output"] + def test_jsonformat_invalid(): - r = client.post("/api/plugins/run", json={"plugin":"jsonformat","input":"not json"}) + r = client.post( + "/api/plugins/run", json={"plugin": "jsonformat", "input": "not json"} + ) assert "Invalid" in r.json()["output"] + def test_summarizer(): long_text = "The quick brown fox jumps over the lazy dog. " * 20 - r = client.post("/api/plugins/run", json={"plugin":"summarizer","input":long_text}) + r = client.post( + "/api/plugins/run", json={"plugin": "summarizer", "input": long_text} + ) assert r.json()["success"] + def test_unknown_plugin(): - r = client.post("/api/plugins/run", json={"plugin":"unknown","input":"test"}) + r = client.post("/api/plugins/run", json={"plugin": "unknown", "input": "test"}) assert r.status_code == 400 @@ -136,22 +169,43 @@ def test_get_settings(): assert r.status_code == 200 assert "default_model" in r.json() + def test_save_settings(): - r = client.put("/api/settings/", json={ - "default_model":"mistral","default_language":"hi", - "temperature":0.5,"max_history_turns":8,"rag_top_k":3,"theme":"dark" - }) + r = client.put( + "/api/settings/", + json={ + "default_model": "mistral", + "default_language": "hi", + "temperature": 0.5, + "max_history_turns": 8, + "rag_top_k": 3, + "theme": "dark", + }, + ) assert r.json()["default_model"] == "mistral" # ─── Models (mocked) ───────────────────────────────────── -@patch("routes.models.ollama_service.is_ollama_running", new_callable=AsyncMock, return_value=False) +@patch( + "routes.models.ollama_service.is_ollama_running", + new_callable=AsyncMock, + return_value=False, +) def test_models_ollama_down(mock): r = client.get("/api/models/") assert r.status_code == 503 -@patch("routes.models.ollama_service.is_ollama_running", new_callable=AsyncMock, return_value=True) -@patch("routes.models.ollama_service.list_models", new_callable=AsyncMock, return_value=[{"name":"llama3","size":"4.7 GB","status":"available"}]) + +@patch( + "routes.models.ollama_service.is_ollama_running", + new_callable=AsyncMock, + return_value=True, +) +@patch( + "routes.models.ollama_service.list_models", + new_callable=AsyncMock, + return_value=[{"name": "llama3", "size": "4.7 GB", "status": "available"}], +) def test_models_list(m1, m2): r = client.get("/api/models/") assert r.status_code == 200 @@ -159,18 +213,35 @@ def test_models_list(m1, m2): # ─── Chat (mocked Ollama) ──────────────────────────────── -@patch("routes.chat.ollama_service.is_ollama_running", new_callable=AsyncMock, return_value=False) +@patch( + "routes.chat.ollama_service.is_ollama_running", + new_callable=AsyncMock, + return_value=False, +) def test_chat_ollama_down(mock): - r = client.post("/api/chat/", json={"message":"hi","session_id":"x","model":"llama3"}) + r = client.post( + "/api/chat/", json={"message": "hi", "session_id": "x", "model": "llama3"} + ) assert r.status_code == 503 -@patch("routes.chat.ollama_service.is_ollama_running", new_callable=AsyncMock, return_value=True) -@patch("routes.chat.ollama_service.chat", new_callable=AsyncMock, return_value="Hello! I'm LocalMind.") -@patch("routes.chat.rag_service.retrieve_context", return_value=("", [])) + +@patch( + "routes.chat.ollama_service.is_ollama_running", + new_callable=AsyncMock, + return_value=True, +) +@patch( + "routes.chat.ollama_service.chat", + new_callable=AsyncMock, + return_value="Hello! I'm LocalMind.", +) +@patch("routes.chat.rag_service.retrieve_context", return_value=("", [])) def test_chat_ok(m1, m2, m3): r = client.post("/api/sessions/", json={"title": "t"}) sid = r.json()["id"] - r2 = client.post("/api/chat/", json={"message": "hello", "session_id": sid, "model": "llama3"}) + r2 = client.post( + "/api/chat/", json={"message": "hello", "session_id": sid, "model": "llama3"} + ) assert r2.status_code == 200 assert "LocalMind" in r2.json()["reply"] @@ -180,6 +251,7 @@ def test_export_not_found(): r = client.get("/api/export/nonexistent/markdown") assert r.status_code == 404 + def test_export_json(): r = client.post("/api/sessions/", json={"title": "Export Test"}) sid = r.json()["id"] @@ -190,6 +262,7 @@ def test_export_json(): data = json.loads(r2.content) assert len(data["messages"]) == 2 + def test_export_markdown(): r = client.post("/api/sessions/", json={"title": "MD Export"}) sid = r.json()["id"] @@ -198,6 +271,7 @@ def test_export_markdown(): assert r2.status_code == 200 assert b"Test question" in r2.content + def test_export_txt(): r = client.post("/api/sessions/", json={"title": "TXT Export"}) sid = r.json()["id"]