precis / backend /tests /test_api_fetching.py
compendious's picture
GPU speedup
4a6590d unverified
Raw History Blame Contribute Delete
5.86 kB
import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
# Mock env before importing app
import os
import sys
from pathlib import Path
# Ensure `backend` is on path when running `pytest` from repo root
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "backend"))
os.environ.setdefault("OLLAMA_BASE_URL", "http://127.0.0.1:11434")
os.environ.setdefault("DEFAULT_MODEL", "phi4-mini:latest")
os.environ.setdefault("PRECIS_API_KEY", "test-key")
os.environ.setdefault("AVAILABLE_MODELS", "phi4-mini:latest,qwen:4b")
from app import app
client = TestClient(app)
API_KEY = "test-key"
HEADERS = {"X-API-Key": API_KEY}
def test_health():
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "healthy", "service": "precis"}
@patch("app.httpx.AsyncClient")
def test_status_ollama_reachable(mock_client):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_client.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_resp)
r = client.get("/status")
assert r.status_code == 200
assert "ollama_reachable" in r.json()
@patch("app.httpx.AsyncClient")
def test_models_returns_installed_when_present(mock_client):
# /api/tags returns 2 models, /api/ps returns 1 running
tags_resp = MagicMock()
tags_resp.status_code = 200
tags_resp.content = b'{"models":[{"name":"phi4-mini:latest"},{"name":"qwen:4b"}]}'
tags_resp.json.return_value = {"models": [{"name": "phi4-mini:latest"}, {"name": "qwen:4b"}]}
tags_resp.raise_for_status = MagicMock()
ps_resp = MagicMock()
ps_resp.status_code = 200
ps_resp.content = b'{"models":[{"name":"phi4-mini:latest"}]}'
ps_resp.json.return_value = {"models": [{"name": "phi4-mini:latest"}]}
mock_instance = MagicMock()
mock_instance.get = AsyncMock(side_effect=[tags_resp, ps_resp])
mock_client.return_value.__aenter__.return_value = mock_instance
r = client.get("/models")
assert r.status_code == 200
data = r.json()
assert set(data["available"]) == {"phi4-mini:latest", "qwen:4b"}
assert data["running"] == ["phi4-mini:latest"]
assert data["default"] in data["available"]
@patch("app.httpx.AsyncClient")
def test_models_returns_empty_when_ollama_has_no_models(mock_client):
# Ollama reachable but 0 models — should NOT fallback to AVAILABLE_MODELS
tags_resp = MagicMock()
tags_resp.status_code = 200
tags_resp.content = b'{"models":[]}'
tags_resp.json.return_value = {"models": []}
tags_resp.raise_for_status = MagicMock()
ps_resp = MagicMock()
ps_resp.status_code = 200
ps_resp.content = b'{"models":[]}'
ps_resp.json.return_value = {"models": []}
mock_instance = MagicMock()
mock_instance.get = AsyncMock(side_effect=[tags_resp, ps_resp])
mock_client.return_value.__aenter__.return_value = mock_instance
r = client.get("/models")
assert r.status_code == 200
data = r.json()
# Fixed bug: should be empty, not fallback
assert data["available"] == []
assert data["default"] is None
assert data["running"] == []
@patch("app.httpx.AsyncClient")
def test_models_fallback_when_ollama_unreachable(mock_client):
mock_client.return_value.__aenter__.return_value.get = AsyncMock(side_effect=Exception("connection refused"))
r = client.get("/models")
assert r.status_code == 200
data = r.json()
# Fallback to env when Ollama down
assert "phi4-mini:latest" in data["available"]
assert data["default"] == "phi4-mini:latest"
def test_youtube_id_strips_timestamp():
from helpers.transcript import _extract_video_id, _strip_timestamp
# t=, start, time_continue stripped before ID extraction
assert _extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42s") == "dQw4w9WgXcQ"
assert _extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1m30s&start=30") == "dQw4w9WgXcQ"
assert _extract_video_id("https://youtu.be/dQw4w9WgXcQ?t=120") == "dQw4w9WgXcQ"
assert _extract_video_id("https://www.youtube.com/shorts/dQw4w9WgXcQ?t=10") == "dQw4w9WgXcQ"
# _strip_timestamp removes params
assert "t=" not in _strip_timestamp("https://www.youtube.com/watch?v=abc12345678&t=120s")
assert "start=" not in _strip_timestamp("https://www.youtube.com/watch?v=abc12345678&start=30")
@patch("helpers.transcript.YouTubeTranscriptApi")
def test_transcript_fetch_uses_stripped_id(mock_api):
from helpers.transcript import transcript
import asyncio
mock_instance = MagicMock()
mock_instance.fetch.return_value = [MagicMock(text="hello"), MagicMock(text="world")]
mock_api.return_value = mock_instance
# Should strip t= and fetch with clean ID
title, text = asyncio.run(transcript("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42s"))
# fetch called with clean ID, not including t=
assert mock_instance.fetch.called
args, kwargs = mock_instance.fetch.call_args
assert args[0] == "dQw4w9WgXcQ"
@patch("app.stream_summary", new_callable=AsyncMock)
def test_summarize_transcript_requires_api_key(mock_summary):
mock_summary.return_value = MagicMock()
# No key -> 401, with key -> mocked success (not 401)
r = client.post("/summarize/transcript", json={"text": "hello"})
assert r.status_code == 401
r = client.post("/summarize/transcript", json={"text": "hello"}, headers=HEADERS)
assert r.status_code != 401
mock_summary.assert_called_once()
def test_summarize_file_rejects_non_txt():
import io
files = {"file": ("test.pdf", io.BytesIO(b"hello"), "application/pdf")}
r = client.post("/summarize/file", files=files, headers=HEADERS)
assert r.status_code == 400
assert "Only .txt" in r.text