Download tests/test_api.py from rushabh13/anovo-api: direct link, hf CLI and curl.
- Browser
- Download file 20 kB
-
https://huggingface.co/spaces/rushabh13/anovo-api/resolve/main/tests/test_api.py
- Command line
-
hf download hf://spaces/rushabh13/anovo-api/tests/test_api.py
-
curl -L -o test_api.py https://huggingface.co/spaces/rushabh13/anovo-api/resolve/main/tests/test_api.py
20 kB
| """ | |
| Backend API tests for Anovo. | |
| All service calls are mocked so no ML models or external services are required. | |
| """ | |
| import sys | |
| import os | |
| from unittest.mock import patch | |
| from unittest.mock import MagicMock | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| # Ensure the backend directory is on the path when running from backend/ | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) | |
| from main import app # noqa: E402 | |
| client = TestClient(app) | |
| class TestAccountDeletion: | |
| def test_delete_account_removes_user_and_commits(self): | |
| from routers.auth import delete_me | |
| db = MagicMock() | |
| user = MagicMock() | |
| with patch("routers.auth.get_user_by_id", return_value=user): | |
| delete_me(user_id=42, db=db) | |
| db.delete.assert_called_once_with(user) | |
| db.commit.assert_called_once_with() | |
| # ── Health ──────────────────────────────────────────────────────────────────── | |
| class TestHealth: | |
| def test_health_check(self): | |
| response = client.get("/") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["status"] == "ok" | |
| assert data["service"] == "Anovo API" | |
| # ── Paraphrase ──────────────────────────────────────────────────────────────── | |
| class TestParaphrase: | |
| def test_paraphrase_success(self): | |
| with patch( | |
| "routers.paraphrase._paraphrase", | |
| return_value=("A quick fox leapt over a lazy dog.", "standard"), | |
| ): | |
| response = client.post("/api/paraphrase", json={"text": "The quick brown fox jumps over the lazy dog.", "intensity": 3}) # noqa: E501 | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["original"] == "The quick brown fox jumps over the lazy dog." | |
| assert data["paraphrased"] == "A quick fox leapt over a lazy dog." | |
| assert data["intensity"] == 3 | |
| assert data["writing_mode"] == "standard" | |
| def test_paraphrase_intensity_bounds(self): | |
| with patch("routers.paraphrase._paraphrase", return_value=("result", "standard")): | |
| r1 = client.post("/api/paraphrase", json={"text": "Hello world.", "intensity": 1}) | |
| r5 = client.post("/api/paraphrase", json={"text": "Hello world.", "intensity": 5}) | |
| assert r1.status_code == 200 | |
| assert r5.status_code == 200 | |
| def test_paraphrase_invalid_intensity(self): | |
| response = client.post("/api/paraphrase", json={"text": "Hello world.", "intensity": 10}) | |
| assert response.status_code == 422 | |
| def test_paraphrase_empty_text(self): | |
| response = client.post("/api/paraphrase", json={"text": "", "intensity": 3}) | |
| assert response.status_code == 422 | |
| def test_paraphrase_service_error(self): | |
| with patch("routers.paraphrase._paraphrase", side_effect=RuntimeError("model unavailable")): | |
| response = client.post("/api/paraphrase", json={"text": "Hello.", "intensity": 3}) | |
| assert response.status_code == 500 | |
| def test_contextual_refine_options(self): | |
| with patch( | |
| "routers.paraphrase._refine_selection", | |
| return_value=["Clear writing makes ideas easier to share.", "Good prose communicates ideas clearly."], | |
| ): | |
| response = client.post( | |
| "/api/paraphrase/refine", | |
| json={ | |
| "text": "Good writing helps people communicate ideas clearly.", | |
| "selected_text": "Good writing helps people communicate ideas clearly.", | |
| "kind": "sentence", | |
| "writing_mode": "fluency", | |
| "intensity": 3, | |
| "count": 2, | |
| }, | |
| ) | |
| assert response.status_code == 200 | |
| assert len(response.json()["suggestions"]) == 2 | |
| # ── Grammar ─────────────────────────────────────────────────────────────────── | |
| class TestGrammar: | |
| _good_errors = [ | |
| { | |
| "message": "Possible agreement error", | |
| "offset": 5, | |
| "length": 3, | |
| "replacements": ["is"], | |
| "rule_id": "AGREEMENT", | |
| "category": "GRAMMAR", | |
| } | |
| ] | |
| def test_grammar_check_with_errors(self): | |
| with patch("routers.grammar.check_grammar", return_value=self._good_errors): | |
| response = client.post("/api/grammar-check", json={"text": "This are wrong.", "language": "en-US"}) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["error_count"] == 1 | |
| assert data["errors"][0]["rule_id"] == "AGREEMENT" | |
| def test_grammar_check_no_errors(self): | |
| with patch("routers.grammar.check_grammar", return_value=[]): | |
| response = client.post("/api/grammar-check", json={"text": "This is correct.", "language": "en-US"}) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["error_count"] == 0 | |
| assert data["errors"] == [] | |
| def test_grammar_service_unavailable(self): | |
| with patch("routers.grammar.check_grammar", side_effect=RuntimeError("LanguageTool unreachable")): | |
| response = client.post("/api/grammar-check", json={"text": "Hello.", "language": "en-US"}) | |
| assert response.status_code == 503 | |
| def test_grammar_empty_text(self): | |
| response = client.post("/api/grammar-check", json={"text": "", "language": "en-US"}) | |
| assert response.status_code == 422 | |
| # ── Summarize ───────────────────────────────────────────────────────────────── | |
| _LONG_TEXT = ( | |
| "Artificial intelligence is transforming the world in many ways. " | |
| "From healthcare to finance, AI is being applied to solve complex problems. " | |
| "Machine learning models can now outperform humans in specific tasks. " | |
| "However, ethical concerns and bias remain significant challenges. " | |
| "Research into explainable AI aims to make models more transparent and trustworthy." | |
| ) | |
| class TestSummarize: | |
| def test_summarize_paragraph(self): | |
| with patch("routers.summarize._summarize", return_value="AI is changing the world."): | |
| response = client.post("/api/summarize", json={"text": _LONG_TEXT, "mode": "paragraph", "max_length": 150}) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["summary"] == "AI is changing the world." | |
| assert data["mode"] == "paragraph" | |
| def test_summarize_bullet(self): | |
| bullets = "• AI is transforming healthcare.\n• Ethical concerns exist." | |
| with patch("routers.summarize._summarize", return_value=bullets): | |
| response = client.post("/api/summarize", json={"text": _LONG_TEXT, "mode": "bullet", "max_length": 150}) | |
| assert response.status_code == 200 | |
| assert response.json()["mode"] == "bullet" | |
| def test_summarize_text_too_short(self): | |
| response = client.post("/api/summarize", json={"text": "Too short.", "mode": "paragraph", "max_length": 150}) | |
| assert response.status_code == 422 | |
| def test_summarize_service_error(self): | |
| with patch("routers.summarize._summarize", side_effect=Exception("GPU OOM")): | |
| response = client.post("/api/summarize", json={"text": _LONG_TEXT, "mode": "paragraph", "max_length": 150}) | |
| assert response.status_code == 500 | |
| # ── Translate ───────────────────────────────────────────────────────────────── | |
| class TestTranslate: | |
| def test_translate_success(self): | |
| # _translate now also reports the source language it used (BUG-013). | |
| with patch("routers.translate._translate", return_value=("Bonjour le monde", "en")): | |
| response = client.post("/api/translate", json={"text": "Hello world", "source_language": "en", "target_language": "fr"}) # noqa: E501 | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["translated"] == "Bonjour le monde" | |
| assert data["source_language"] == "en" | |
| assert data["target_language"] == "fr" | |
| def test_translate_reports_detected_language(self): | |
| with patch("routers.translate._translate", return_value=("Hello", "fr")): | |
| response = client.post("/api/translate", json={"text": "Bonjour", "source_language": "auto", "target_language": "en"}) # noqa: E501 | |
| data = response.json() | |
| assert data["detected_language"] == "fr" | |
| assert data["detected_language_name"] == "French" | |
| def test_translate_empty_text(self): | |
| response = client.post("/api/translate", json={"text": "", "source_language": "en", "target_language": "fr"}) | |
| assert response.status_code == 422 | |
| def test_translate_service_error(self): | |
| with patch("routers.translate._translate", side_effect=Exception("model not found")): | |
| response = client.post("/api/translate", json={"text": "Hello", "source_language": "en", "target_language": "xx"}) # noqa: E501 | |
| assert response.status_code == 500 | |
| # ── Humanize ────────────────────────────────────────────────────────────────── | |
| class TestHumanize: | |
| def test_humanize_success(self): | |
| mock_result = {"humanized": "Humanized text here.", "steps": {}} | |
| with patch("routers.humanize._humanize", return_value=mock_result): | |
| response = client.post("/api/humanize", json={"text": "The utilization of artificial intelligence is widespread."}) # noqa: E501 | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["humanized"] == "Humanized text here." | |
| def test_humanize_empty_text(self): | |
| response = client.post("/api/humanize", json={"text": ""}) | |
| assert response.status_code == 422 | |
| def test_humanize_service_error(self): | |
| with patch("routers.humanize._humanize", side_effect=Exception("pipeline failed")): | |
| response = client.post("/api/humanize", json={"text": "Some text to humanize."}) | |
| assert response.status_code == 500 | |
| # ── Plagiarism ──────────────────────────────────────────────────────────────── | |
| class TestPlagiarism: | |
| def test_plagiarism_detected(self): | |
| result = {"similarity_score": 0.95, "is_plagiarized": True, "threshold": 0.8} | |
| with patch("routers.plagiarism.check_plagiarism", return_value=result): | |
| response = client.post("/api/plagiarism-check", json={"text": "This is the copied text.", "reference_text": "This is the copied text."}) # noqa: E501 | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["is_plagiarized"] is True | |
| assert abs(data["similarity_score"] - 0.95) < 1e-6 | |
| def test_plagiarism_not_detected(self): | |
| result = {"similarity_score": 0.2, "is_plagiarized": False, "threshold": 0.8} | |
| with patch("routers.plagiarism.check_plagiarism", return_value=result): | |
| response = client.post("/api/plagiarism-check", json={"text": "The sky is blue.", "reference_text": "Python is a programming language."}) # noqa: E501 | |
| assert response.status_code == 200 | |
| assert response.json()["is_plagiarized"] is False | |
| def test_plagiarism_empty_text(self): | |
| response = client.post("/api/plagiarism-check", json={"text": "", "reference_text": "Some reference."}) | |
| assert response.status_code == 422 | |
| # ── Tone ────────────────────────────────────────────────────────────────────── | |
| class TestTone: | |
| _tone_result = { | |
| "tones": [ | |
| {"label": "formal", "score": 0.8}, | |
| {"label": "casual", "score": 0.1}, | |
| {"label": "persuasive", "score": 0.05}, | |
| {"label": "informative", "score": 0.05}, | |
| ], | |
| "primary_tone": "formal", | |
| } | |
| def test_tone_detect_success(self): | |
| with patch("routers.tone.detect_tone", return_value=self._tone_result): | |
| response = client.post("/api/tone-detect", json={"text": "We must address this critical issue immediately."}) # noqa: E501 | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["primary_tone"] == "formal" | |
| assert len(data["tones"]) == 4 | |
| def test_tone_empty_text(self): | |
| response = client.post("/api/tone-detect", json={"text": ""}) | |
| assert response.status_code == 422 | |
| def test_tone_service_error(self): | |
| with patch("routers.tone.detect_tone", side_effect=Exception("zero-shot failed")): | |
| response = client.post("/api/tone-detect", json={"text": "Some text."}) | |
| assert response.status_code == 500 | |
| # ── Co-Writer ───────────────────────────────────────────────────────────────── | |
| class TestCoWriter: | |
| def test_cowrite_success(self): | |
| suggestions = [" is a rapidly growing field.", " continues to evolve rapidly.", " shapes the modern economy."] | |
| # generate_suggestions now also returns a truncation advisory (BUG-038). | |
| with patch("routers.cowriter.generate_suggestions", return_value=(suggestions, "standard", None)): | |
| response = client.post("/api/co-write", json={"text": "Artificial intelligence", "max_tokens": 50, "num_suggestions": 3}) # noqa: E501 | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert len(data["suggestions"]) == 3 | |
| assert data["action"] == "continue" | |
| assert data["tone"] == "match" | |
| assert data["model_used"] == "standard" | |
| assert data["prompt"] == "Artificial intelligence" | |
| # Default tone is "match"; a two-word draft has no voice to copy. | |
| assert "Match my voice" in data["advisory"] | |
| def test_cowrite_no_advisory_with_an_explicit_voice(self): | |
| with patch("routers.cowriter.generate_suggestions", return_value=(["a", "b", "c"], "standard", None)): | |
| response = client.post("/api/co-write", json={ | |
| "text": "Artificial intelligence", "max_tokens": 50, | |
| "num_suggestions": 3, "tone": "professional", | |
| }) | |
| assert response.json()["advisory"] is None | |
| def test_cowrite_refuses_json_input(self): | |
| # BUG-035, QA re-test: warning while still inventing prose from | |
| # {"name":"John"} was rejected. The co-writer now declines. | |
| with patch("routers.cowriter.generate_suggestions", return_value=(["a", "b", "c"], "standard", None)): | |
| response = client.post( | |
| "/api/co-write", | |
| json={"text": '{"name":"John","city":"Mumbai"}', "max_tokens": 50, "num_suggestions": 3}, | |
| ) | |
| assert response.status_code == 422 | |
| assert "JSON" in response.json()["detail"] | |
| def test_cowrite_refuses_input_with_nothing_to_work_from(self, text): | |
| # BUG-040, BUG-041 and the emoji case. | |
| with patch("routers.cowriter.generate_suggestions", return_value=(["a"], "standard", None)): | |
| response = client.post( | |
| "/api/co-write", json={"text": text, "max_tokens": 50, "num_suggestions": 1} | |
| ) | |
| assert response.status_code == 422 | |
| def test_cowrite_truncation_advisory_still_surfaces_for_prose(self): | |
| with patch("routers.cowriter.generate_suggestions", | |
| return_value=(["a"], "standard", "Your draft was longer than the model's context window.")): | |
| response = client.post( | |
| "/api/co-write", | |
| json={"text": "A long prose draft about product launches.", "max_tokens": 50, "num_suggestions": 1}, | |
| ) | |
| assert "context window" in response.json()["advisory"] | |
| def test_cowrite_empty_text(self): | |
| response = client.post("/api/co-write", json={"text": "", "max_tokens": 50, "num_suggestions": 3}) | |
| assert response.status_code == 422 | |
| def test_cowrite_invalid_suggestions(self): | |
| response = client.post("/api/co-write", json={"text": "Hello", "max_tokens": 50, "num_suggestions": 10}) | |
| assert response.status_code == 422 | |
| def test_cowrite_service_error(self): | |
| with patch("routers.cowriter.generate_suggestions", side_effect=Exception("generation failed")): | |
| response = client.post("/api/co-write", json={"text": "The future of", "max_tokens": 50, "num_suggestions": 2}) # noqa: E501 | |
| assert response.status_code == 500 | |
| # ── Documents ───────────────────────────────────────────────────────────────── | |
| class TestDocuments: | |
| def test_extract_text_document_for_unified_workspace(self): | |
| response = client.post( | |
| "/api/documents/extract", | |
| files={"file": ("draft.txt", b"First paragraph.\n\nSecond paragraph.", "text/plain")}, | |
| ) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["filename"] == "draft.txt" | |
| assert data["text"] == "First paragraph.\n\nSecond paragraph." | |
| assert data["character_count"] == len(data["text"]) | |
| def test_extract_rejects_unsupported_document(self): | |
| response = client.post( | |
| "/api/documents/extract", | |
| files={"file": ("draft.pdf", b"not a pdf", "application/pdf")}, | |
| ) | |
| assert response.status_code == 400 | |
| # ── Chat ────────────────────────────────────────────────────────────────────── | |
| class TestChat: | |
| def test_chat_success(self): | |
| with patch("routers.chat.chat", return_value="AI is a branch of computer science."): | |
| response = client.post("/api/chat", json={"message": "What is AI?", "mode": "general", "history": []}) | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["reply"] == "AI is a branch of computer science." | |
| assert data["mode"] == "general" | |
| def test_chat_with_history(self): | |
| history = [{"role": "user", "content": "Hi"}, {"role": "assistant", "content": "Hello!"}] | |
| with patch("routers.chat.chat", return_value="Sure, happy to help!"): | |
| response = client.post("/api/chat", json={"message": "Can you help me?", "mode": "academic", "history": history}) # noqa: E501 | |
| assert response.status_code == 200 | |
| def test_chat_empty_message(self): | |
| response = client.post("/api/chat", json={"message": "", "mode": "general", "history": []}) | |
| assert response.status_code == 422 | |
| def test_chat_ollama_unavailable(self): | |
| with patch("routers.chat.chat", side_effect=RuntimeError("Ollama not reachable")): | |
| response = client.post("/api/chat", json={"message": "Hello", "mode": "general", "history": []}) | |
| assert response.status_code == 503 | |