Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 96 additions & 22 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"]
Expand All @@ -81,52 +81,85 @@ 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/")
assert r.status_code == 200
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


Expand All @@ -136,41 +169,79 @@ 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
assert len(r.json()["models"]) == 1


# ─── 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"]

Expand All @@ -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"]
Expand All @@ -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"]
Expand All @@ -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"]
Expand Down