mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 10:17:55 +00:00
Pg 4 (#2390)
* feat: postgres tests * feat: mongo cutoff * feat: mongo cutoff * feat: adjust docs and compose files * fix: mini code mongo removals * fix: tests and k8s mongo stuff * feat: test fixes * fix: ruff * fix: vale * Potential fix for pull request finding 'CodeQL / Clear-text logging of sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix: mini suggestions * vale lint fix 2 * fix: codeql columns thing * fix: test mongo * fix: tests coverage * feat: better tests 4 * feat: more tests * feat: decent coverage * fix: ruff fixes * fix: remove mongo mock * feat: enhance workflow engine and API routes; add document retrieval and source handling * feat: e2e tests * fix: mcp, mongo and more * fix: mini codeql warning * fix: agent chunk view * fix: mini issues * fix: more pg fixes * feat: postgres prep on start * feat: qa tests * fix: mini improvements * fix: tests --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Siddhant Rai <siddhant.rai.5686@gmail.com>
This commit is contained in:
312 files changed
+763725
-25159
No files matched your search
@@ -1,71 +0,0 @@
|
||||
import io
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from flask import Flask, request
|
||||
from application.parser.file.constants import SUPPORTED_SOURCE_EXTENSIONS
|
||||
|
||||
|
||||
def test_upload_route_passes_audio_extensions_to_ingest(flask_app, mock_mongo_db):
|
||||
from application.api.user.sources.upload import UploadFile
|
||||
|
||||
app = Flask(__name__)
|
||||
mock_storage = MagicMock()
|
||||
mock_task = SimpleNamespace(id="task-123")
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/upload",
|
||||
method="POST",
|
||||
data={
|
||||
"user": "test_user",
|
||||
"name": "Meeting Notes",
|
||||
"file": (io.BytesIO(b"audio-bytes"), "meeting.wav"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
):
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.ingest.delay",
|
||||
return_value=mock_task,
|
||||
) as mock_delay:
|
||||
resource = UploadFile()
|
||||
response = resource.post()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json["success"] is True
|
||||
|
||||
formats = mock_delay.call_args.args[1]
|
||||
assert formats == list(SUPPORTED_SOURCE_EXTENSIONS)
|
||||
|
||||
|
||||
@patch("application.stt.upload_limits.settings")
|
||||
def test_upload_route_rejects_oversized_audio(
|
||||
mock_limit_settings, flask_app, mock_mongo_db
|
||||
):
|
||||
from application.api.user.sources.upload import UploadFile
|
||||
|
||||
app = Flask(__name__)
|
||||
mock_limit_settings.STT_MAX_FILE_SIZE_MB = 1
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/upload",
|
||||
method="POST",
|
||||
data={
|
||||
"user": "test_user",
|
||||
"name": "Meeting Notes",
|
||||
"file": (io.BytesIO(b"x" * (2 * 1024 * 1024)), "meeting.wav"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
):
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
|
||||
with patch("application.api.user.sources.upload.StorageCreator.get_storage"):
|
||||
resource = UploadFile()
|
||||
response = resource.post()
|
||||
|
||||
assert response.status_code == 413
|
||||
assert "exceeds" in response.json["message"]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,357 +1,629 @@
|
||||
"""Tests for sources routes."""
|
||||
"""Tests for application/api/user/sources/routes.py."""
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import MagicMock, patch
|
||||
from bson import ObjectId
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return Flask(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _patch_db(conn):
|
||||
@contextmanager
|
||||
def _yield():
|
||||
yield conn
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.db_session", _yield
|
||||
), patch(
|
||||
"application.api.user.sources.routes.db_readonly", _yield
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def _seed_source(pg_conn, user, **kwargs):
|
||||
from application.storage.db.repositories.sources import SourcesRepository
|
||||
return SourcesRepository(pg_conn).create(
|
||||
kwargs.pop("name", "src"),
|
||||
user_id=user,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class TestGetProviderFromRemoteData:
|
||||
"""Test the _get_provider_from_remote_data helper function."""
|
||||
def test_returns_none_for_empty(self):
|
||||
from application.api.user.sources.routes import (
|
||||
_get_provider_from_remote_data,
|
||||
)
|
||||
assert _get_provider_from_remote_data(None) is None
|
||||
assert _get_provider_from_remote_data("") is None
|
||||
|
||||
def test_returns_none_for_none_input(self):
|
||||
"""Should return None when remote_data is None."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
def test_returns_from_dict(self):
|
||||
from application.api.user.sources.routes import (
|
||||
_get_provider_from_remote_data,
|
||||
)
|
||||
assert (
|
||||
_get_provider_from_remote_data({"provider": "gdrive"})
|
||||
== "gdrive"
|
||||
)
|
||||
|
||||
result = _get_provider_from_remote_data(None)
|
||||
assert result is None
|
||||
def test_returns_from_json_string(self):
|
||||
from application.api.user.sources.routes import (
|
||||
_get_provider_from_remote_data,
|
||||
)
|
||||
assert (
|
||||
_get_provider_from_remote_data('{"provider": "github"}')
|
||||
== "github"
|
||||
)
|
||||
|
||||
def test_returns_none_for_empty_string(self):
|
||||
"""Should return None when remote_data is empty string."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
|
||||
result = _get_provider_from_remote_data("")
|
||||
assert result is None
|
||||
|
||||
def test_extracts_provider_from_dict(self):
|
||||
"""Should extract provider from dict remote_data."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
|
||||
remote_data = {"provider": "s3", "bucket": "my-bucket"}
|
||||
result = _get_provider_from_remote_data(remote_data)
|
||||
assert result == "s3"
|
||||
|
||||
def test_extracts_provider_from_json_string(self):
|
||||
"""Should extract provider from JSON string remote_data."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
|
||||
remote_data = json.dumps({"provider": "github", "repo": "test/repo"})
|
||||
result = _get_provider_from_remote_data(remote_data)
|
||||
assert result == "github"
|
||||
|
||||
def test_returns_none_for_dict_without_provider(self):
|
||||
"""Should return None when dict has no provider key."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
|
||||
remote_data = {"bucket": "my-bucket", "region": "us-east-1"}
|
||||
result = _get_provider_from_remote_data(remote_data)
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_for_invalid_json(self):
|
||||
"""Should return None for invalid JSON string."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
|
||||
result = _get_provider_from_remote_data("not valid json")
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_for_json_array(self):
|
||||
"""Should return None when JSON parses to non-dict."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
|
||||
result = _get_provider_from_remote_data('["item1", "item2"]')
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_for_non_string_non_dict(self):
|
||||
"""Should return None for other types like int."""
|
||||
from application.api.user.sources.routes import _get_provider_from_remote_data
|
||||
|
||||
result = _get_provider_from_remote_data(123)
|
||||
assert result is None
|
||||
def test_returns_none_for_malformed_json(self):
|
||||
from application.api.user.sources.routes import (
|
||||
_get_provider_from_remote_data,
|
||||
)
|
||||
assert _get_provider_from_remote_data("not-json") is None
|
||||
|
||||
|
||||
def _get_response_status(response):
|
||||
"""Helper to get status code from response (handles both tuple and Response)."""
|
||||
if isinstance(response, tuple):
|
||||
return response[1]
|
||||
return response.status_code
|
||||
class TestCombinedJson:
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
|
||||
def _get_response_json(response):
|
||||
"""Helper to get JSON from response (handles both tuple and Response)."""
|
||||
if isinstance(response, tuple):
|
||||
return response[0].json
|
||||
return response.json
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSyncSourceEndpoint:
|
||||
"""Test the /sync_source endpoint."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sources_collection(self, mock_mongo_db):
|
||||
"""Get mock sources collection."""
|
||||
from application.core.settings import settings
|
||||
|
||||
return mock_mongo_db[settings.MONGO_DB_NAME]["sources"]
|
||||
|
||||
def test_sync_source_returns_401_without_token(self, flask_app):
|
||||
"""Should return 401 when no decoded_token is present."""
|
||||
from flask import Flask
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={"source_id": "123"}
|
||||
):
|
||||
with app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
response = CombinedJson().get()
|
||||
assert response.status_code == 401
|
||||
|
||||
assert _get_response_status(response) == 401
|
||||
def test_returns_default_plus_user_sources(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
def test_sync_source_returns_400_for_missing_source_id(self, flask_app):
|
||||
"""Should return 400 when source_id is missing."""
|
||||
from flask import Flask
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
user = "u-list-sources"
|
||||
_seed_source(pg_conn, user, name="doc1", tokens="100")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context("/api/sync_source", method="POST", json={}):
|
||||
with _patch_db(pg_conn), app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = CombinedJson().get()
|
||||
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
assert response.status_code == 200
|
||||
names = [d["name"] for d in response.json]
|
||||
assert "Default" in names
|
||||
assert "doc1" in names
|
||||
|
||||
# check_required_fields returns a response tuple on missing fields
|
||||
assert response is not None
|
||||
def test_db_error_returns_400(self, app):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
def test_sync_source_returns_400_for_invalid_source_id(self, flask_app):
|
||||
"""Should return 400 for invalid ObjectId."""
|
||||
from flask import Flask
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
@contextmanager
|
||||
def _broken():
|
||||
raise RuntimeError("boom")
|
||||
yield
|
||||
|
||||
app = Flask(__name__)
|
||||
with patch(
|
||||
"application.api.user.sources.routes.db_readonly", _broken
|
||||
), app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = CombinedJson().get()
|
||||
assert response.status_code == 400
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={"source_id": "invalid"}
|
||||
|
||||
class TestPaginatedSources:
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
with app.test_request_context("/api/sources/paginated"):
|
||||
from flask import request
|
||||
request.decoded_token = None
|
||||
response = PaginatedSources().get()
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_returns_pagination_shape(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
user = "u-pag"
|
||||
for i in range(5):
|
||||
_seed_source(pg_conn, user, name=f"doc-{i}")
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sources/paginated?page=1&rows=2"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = PaginatedSources().get()
|
||||
assert response.status_code == 200
|
||||
data = response.json
|
||||
assert data["total"] == 5
|
||||
assert data["totalPages"] == 3
|
||||
assert data["currentPage"] == 1
|
||||
assert len(data["paginated"]) == 2
|
||||
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
def test_search_filter(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
assert _get_response_status(response) == 400
|
||||
assert "Invalid source ID" in _get_response_json(response)["message"]
|
||||
user = "u-search"
|
||||
_seed_source(pg_conn, user, name="Alpha doc")
|
||||
_seed_source(pg_conn, user, name="Beta doc")
|
||||
|
||||
def test_sync_source_returns_404_for_nonexistent_source(
|
||||
self, flask_app, mock_mongo_db
|
||||
):
|
||||
"""Should return 404 when source doesn't exist."""
|
||||
from flask import Flask
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
app = Flask(__name__)
|
||||
source_id = str(ObjectId())
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={"source_id": source_id}
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sources/paginated?search=alpha"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = PaginatedSources().get()
|
||||
assert response.status_code == 200
|
||||
data = response.json
|
||||
assert data["total"] == 1
|
||||
assert data["paginated"][0]["name"] == "Alpha doc"
|
||||
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
def test_pagination_across_multiple_pages(self, app, pg_conn):
|
||||
"""Every seeded doc surfaces exactly once across paginated windows."""
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_mongo_db["docsgpt"]["sources"],
|
||||
user = "u-multi-page"
|
||||
expected = {f"doc-{i}" for i in range(7)}
|
||||
for name in expected:
|
||||
_seed_source(pg_conn, user, name=name)
|
||||
|
||||
seen: set[str] = set()
|
||||
for page in (1, 2, 3):
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
f"/api/sources/paginated?page={page}&rows=3"
|
||||
):
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = PaginatedSources().get()
|
||||
assert response.status_code == 200
|
||||
for doc in response.json["paginated"]:
|
||||
seen.add(doc["name"])
|
||||
assert seen == expected
|
||||
|
||||
assert _get_response_status(response) == 404
|
||||
assert "not found" in _get_response_json(response)["message"]
|
||||
def test_out_of_range_page_returns_empty_window(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
def test_sync_source_returns_400_for_connector_type(
|
||||
self, flask_app, mock_mongo_db, mock_sources_collection
|
||||
):
|
||||
"""Should return 400 for connector sources."""
|
||||
from flask import Flask
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
user = "u-oor"
|
||||
_seed_source(pg_conn, user, name="only-one")
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sources/paginated?page=99&rows=10"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = PaginatedSources().get()
|
||||
assert response.status_code == 200
|
||||
data = response.json
|
||||
assert data["total"] == 1
|
||||
# Prior behavior clamps ``currentPage`` back into range.
|
||||
assert data["currentPage"] == 1
|
||||
assert len(data["paginated"]) == 1
|
||||
|
||||
app = Flask(__name__)
|
||||
source_id = ObjectId()
|
||||
def test_empty_result_set_shape(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
# Insert a connector source
|
||||
mock_sources_collection.insert_one(
|
||||
{
|
||||
"_id": source_id,
|
||||
"user": "test_user",
|
||||
"type": "connector_slack",
|
||||
"name": "Slack Source",
|
||||
}
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sources/paginated?page=1&rows=10"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u-empty"}
|
||||
response = PaginatedSources().get()
|
||||
assert response.status_code == 200
|
||||
data = response.json
|
||||
assert data["total"] == 0
|
||||
# Legacy route returned ``max(1, ceil(0/rows)) == 1`` for empties.
|
||||
assert data["totalPages"] == 1
|
||||
assert data["paginated"] == []
|
||||
|
||||
def test_search_hits_sql_not_post_filter(self, app, pg_conn):
|
||||
"""Search must narrow ``total`` at the DB level, not in Python."""
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
user = "u-sql-search"
|
||||
_seed_source(pg_conn, user, name="needle in a haystack")
|
||||
_seed_source(pg_conn, user, name="plain")
|
||||
_seed_source(pg_conn, user, name="another plain")
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sources/paginated?search=NEEDLE"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = PaginatedSources().get()
|
||||
data = response.json
|
||||
# ``total`` reflects the filtered count — would be 3 if filtering
|
||||
# happened after an unbounded fetch.
|
||||
assert data["total"] == 1
|
||||
assert data["paginated"][0]["name"] == "needle in a haystack"
|
||||
|
||||
def test_response_shape_preserved(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
user = "u-shape"
|
||||
_seed_source(pg_conn, user, name="shape-doc")
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sources/paginated?page=1&rows=10"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = PaginatedSources().get()
|
||||
data = response.json
|
||||
# Top-level contract the frontend relies on.
|
||||
assert set(data.keys()) >= {"total", "totalPages", "currentPage", "paginated"}
|
||||
# Each paginated entry exposes the legacy per-row keys.
|
||||
row = data["paginated"][0]
|
||||
for key in (
|
||||
"id", "name", "date", "model", "location", "tokens",
|
||||
"retriever", "syncFrequency", "provider", "isNested", "type",
|
||||
):
|
||||
assert key in row
|
||||
|
||||
|
||||
class TestDeleteOldIndexes:
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
with app.test_request_context("/api/delete_old?source_id=x"):
|
||||
from flask import request
|
||||
request.decoded_token = None
|
||||
response = DeleteOldIndexes().get()
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_returns_400_missing_id(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
with app.test_request_context("/api/delete_old"):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = DeleteOldIndexes().get()
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_returns_404_missing_source(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/delete_old?source_id=00000000-0000-0000-0000-000000000000"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = DeleteOldIndexes().get()
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_deletes_non_faiss_source(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
user = "u-del-src"
|
||||
src = _seed_source(pg_conn, user, name="remove-me")
|
||||
|
||||
fake_storage = MagicMock()
|
||||
fake_storage.is_directory.return_value = False
|
||||
fake_vs = MagicMock()
|
||||
|
||||
with _patch_db(pg_conn), patch(
|
||||
"application.api.user.sources.routes.settings.VECTOR_STORE",
|
||||
"milvus",
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=fake_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.VectorCreator.create_vectorstore",
|
||||
return_value=fake_vs,
|
||||
), app.test_request_context(
|
||||
f"/api/delete_old?source_id={src['id']}"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = DeleteOldIndexes().get()
|
||||
assert response.status_code == 200
|
||||
fake_vs.delete_index.assert_called_once()
|
||||
|
||||
def test_deletes_faiss_source(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
user = "u-faiss-del"
|
||||
src = _seed_source(
|
||||
pg_conn, user, name="faiss-src", file_path="/tmp/x"
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={"source_id": str(source_id)}
|
||||
fake_storage = MagicMock()
|
||||
fake_storage.file_exists.return_value = True
|
||||
fake_storage.is_directory.return_value = False
|
||||
|
||||
with _patch_db(pg_conn), patch(
|
||||
"application.api.user.sources.routes.settings.VECTOR_STORE",
|
||||
"faiss",
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=fake_storage,
|
||||
), app.test_request_context(
|
||||
f"/api/delete_old?source_id={src['id']}"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = DeleteOldIndexes().get()
|
||||
assert response.status_code == 200
|
||||
assert fake_storage.delete_file.call_count >= 2
|
||||
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
def test_delete_ignores_missing_file_error(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_sources_collection,
|
||||
):
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
|
||||
assert _get_response_status(response) == 400
|
||||
assert "Connector sources" in _get_response_json(response)["message"]
|
||||
|
||||
def test_sync_source_returns_400_for_non_syncable_source(
|
||||
self, flask_app, mock_mongo_db, mock_sources_collection
|
||||
):
|
||||
"""Should return 400 when source has no remote_data."""
|
||||
from flask import Flask
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
app = Flask(__name__)
|
||||
source_id = ObjectId()
|
||||
|
||||
# Insert a source without remote_data
|
||||
mock_sources_collection.insert_one(
|
||||
{
|
||||
"_id": source_id,
|
||||
"user": "test_user",
|
||||
"type": "file",
|
||||
"name": "Local Source",
|
||||
"remote_data": None,
|
||||
}
|
||||
user = "u-nofile"
|
||||
src = _seed_source(
|
||||
pg_conn, user, name="nofile", file_path="/tmp/missing",
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={"source_id": str(source_id)}
|
||||
fake_storage = MagicMock()
|
||||
fake_storage.file_exists.return_value = False
|
||||
fake_storage.is_directory.return_value = False
|
||||
fake_storage.delete_file.side_effect = FileNotFoundError("gone")
|
||||
|
||||
with _patch_db(pg_conn), patch(
|
||||
"application.api.user.sources.routes.settings.VECTOR_STORE",
|
||||
"milvus",
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=fake_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.VectorCreator.create_vectorstore",
|
||||
return_value=MagicMock(),
|
||||
), app.test_request_context(
|
||||
f"/api/delete_old?source_id={src['id']}"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = DeleteOldIndexes().get()
|
||||
assert response.status_code == 200
|
||||
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_sources_collection,
|
||||
):
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
class TestRedirectToSources:
|
||||
def test_redirects(self, app):
|
||||
from application.api.user.sources.routes import RedirectToSources
|
||||
|
||||
assert _get_response_status(response) == 400
|
||||
assert "not syncable" in _get_response_json(response)["message"]
|
||||
with app.test_request_context("/api/combine"):
|
||||
response = RedirectToSources().get()
|
||||
assert response.status_code == 301
|
||||
|
||||
def test_sync_source_triggers_sync_task(
|
||||
self, flask_app, mock_mongo_db, mock_sources_collection
|
||||
):
|
||||
"""Should trigger sync task for valid syncable source."""
|
||||
from flask import Flask
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
app = Flask(__name__)
|
||||
source_id = ObjectId()
|
||||
class TestManageSync:
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
# Insert a valid syncable source
|
||||
mock_sources_collection.insert_one(
|
||||
{
|
||||
"_id": source_id,
|
||||
"user": "test_user",
|
||||
"type": "s3",
|
||||
"name": "S3 Source",
|
||||
"remote_data": json.dumps(
|
||||
{
|
||||
"provider": "s3",
|
||||
"bucket": "my-bucket",
|
||||
"aws_access_key_id": "key",
|
||||
"aws_secret_access_key": "secret",
|
||||
}
|
||||
),
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST",
|
||||
json={"source_id": "x", "sync_frequency": "daily"},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = None
|
||||
response = ManageSync().post()
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_returns_400_missing_fields(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST", json={"source_id": "x"}
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = ManageSync().post()
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_returns_400_invalid_frequency(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync",
|
||||
method="POST",
|
||||
json={"source_id": "x", "sync_frequency": "hourly"},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = ManageSync().post()
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_returns_404_missing_source(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/manage_sync",
|
||||
method="POST",
|
||||
json={
|
||||
"source_id": "00000000-0000-0000-0000-000000000000",
|
||||
"sync_frequency": "daily",
|
||||
"retriever": "classic",
|
||||
}
|
||||
)
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = "task-123"
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={"source_id": str(source_id)}
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = ManageSync().post()
|
||||
assert response.status_code == 404
|
||||
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
def test_updates_sync_frequency(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
from application.storage.db.repositories.sources import SourcesRepository
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_sources_collection,
|
||||
):
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sync_source"
|
||||
) as mock_sync:
|
||||
mock_sync.delay.return_value = mock_task
|
||||
user = "u-sync"
|
||||
src = _seed_source(pg_conn, user, name="sync-src")
|
||||
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/manage_sync",
|
||||
method="POST",
|
||||
json={"source_id": str(src["id"]), "sync_frequency": "weekly"},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = ManageSync().post()
|
||||
assert response.status_code == 200
|
||||
got = SourcesRepository(pg_conn).get_any(str(src["id"]), user)
|
||||
assert got["sync_frequency"] == "weekly"
|
||||
|
||||
assert _get_response_status(response) == 200
|
||||
assert _get_response_json(response)["success"] is True
|
||||
assert _get_response_json(response)["task_id"] == "task-123"
|
||||
|
||||
mock_sync.delay.assert_called_once()
|
||||
call_kwargs = mock_sync.delay.call_args[1]
|
||||
assert call_kwargs["user"] == "test_user"
|
||||
assert call_kwargs["loader"] == "s3"
|
||||
assert call_kwargs["doc_id"] == str(source_id)
|
||||
|
||||
def test_sync_source_handles_task_error(
|
||||
self, flask_app, mock_mongo_db, mock_sources_collection
|
||||
):
|
||||
"""Should return 400 when task fails to start."""
|
||||
from flask import Flask
|
||||
class TestSyncSource:
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
app = Flask(__name__)
|
||||
source_id = ObjectId()
|
||||
|
||||
mock_sources_collection.insert_one(
|
||||
{
|
||||
"_id": source_id,
|
||||
"user": "test_user",
|
||||
"type": "github",
|
||||
"name": "GitHub Source",
|
||||
"remote_data": "https://github.com/test/repo",
|
||||
"sync_frequency": "weekly",
|
||||
"retriever": "classic",
|
||||
}
|
||||
)
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={"source_id": str(source_id)}
|
||||
"/api/sync_source", method="POST", json={"source_id": "x"}
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = None
|
||||
response = SyncSource().post()
|
||||
assert response.status_code == 401
|
||||
|
||||
request.decoded_token = {"sub": "test_user"}
|
||||
def test_returns_400_missing_id(self, app):
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_sources_collection,
|
||||
):
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sync_source"
|
||||
) as mock_sync:
|
||||
mock_sync.delay.side_effect = Exception("Celery error")
|
||||
with app.test_request_context(
|
||||
"/api/sync_source", method="POST", json={}
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = SyncSource().post()
|
||||
assert response.status_code == 400
|
||||
|
||||
resource = SyncSource()
|
||||
response = resource.post()
|
||||
def test_returns_404_missing_source(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
assert _get_response_status(response) == 400
|
||||
assert _get_response_json(response)["success"] is False
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sync_source",
|
||||
method="POST",
|
||||
json={"source_id": "00000000-0000-0000-0000-000000000000"},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = SyncSource().post()
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_returns_400_for_connector_type(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
user = "u-conn"
|
||||
src = _seed_source(
|
||||
pg_conn, user, name="connector-src", type="connector_github",
|
||||
remote_data=json.dumps({"provider": "github"}),
|
||||
)
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sync_source",
|
||||
method="POST",
|
||||
json={"source_id": str(src["id"])},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = SyncSource().post()
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_returns_400_for_non_syncable(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
user = "u-nosync"
|
||||
src = _seed_source(pg_conn, user, name="nosync", type="file")
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/sync_source",
|
||||
method="POST",
|
||||
json={"source_id": str(src["id"])},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = SyncSource().post()
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_triggers_sync_task(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
user = "u-trigger"
|
||||
src = _seed_source(
|
||||
pg_conn, user, name="remote-src", type="github",
|
||||
remote_data=json.dumps({"provider": "github", "url": "x"}),
|
||||
)
|
||||
|
||||
fake_task = MagicMock(id="task-123")
|
||||
with _patch_db(pg_conn), patch(
|
||||
"application.api.user.sources.routes.sync_source.delay",
|
||||
return_value=fake_task,
|
||||
), app.test_request_context(
|
||||
"/api/sync_source",
|
||||
method="POST",
|
||||
json={"source_id": str(src["id"])},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = SyncSource().post()
|
||||
assert response.status_code == 200
|
||||
assert response.json["task_id"] == "task-123"
|
||||
|
||||
def test_sync_task_raises_returns_400(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import SyncSource
|
||||
|
||||
user = "u-fail"
|
||||
src = _seed_source(
|
||||
pg_conn, user, name="fail-src", type="github",
|
||||
remote_data=json.dumps({"provider": "github"}),
|
||||
)
|
||||
|
||||
with _patch_db(pg_conn), patch(
|
||||
"application.api.user.sources.routes.sync_source.delay",
|
||||
side_effect=RuntimeError("boom"),
|
||||
), app.test_request_context(
|
||||
"/api/sync_source",
|
||||
method="POST",
|
||||
json={"source_id": str(src["id"])},
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = SyncSource().post()
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestDirectoryStructure:
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
with app.test_request_context("/api/directory_structure?id=x"):
|
||||
from flask import request
|
||||
request.decoded_token = None
|
||||
response = DirectoryStructure().get()
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_returns_400_missing_id(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
with app.test_request_context("/api/directory_structure"):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = DirectoryStructure().get()
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_returns_404_missing_doc(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
"/api/directory_structure?id=00000000-0000-0000-0000-000000000000"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": "u"}
|
||||
response = DirectoryStructure().get()
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_returns_structure(self, app, pg_conn):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
user = "u-dir"
|
||||
src = _seed_source(
|
||||
pg_conn, user, name="nested",
|
||||
directory_structure={"root": {"a.txt": None}},
|
||||
file_path="/data/nested",
|
||||
remote_data=json.dumps({"provider": "gdrive"}),
|
||||
)
|
||||
|
||||
with _patch_db(pg_conn), app.test_request_context(
|
||||
f"/api/directory_structure?id={src['id']}"
|
||||
):
|
||||
from flask import request
|
||||
request.decoded_token = {"sub": user}
|
||||
response = DirectoryStructure().get()
|
||||
assert response.status_code == 200
|
||||
data = response.json
|
||||
assert data["provider"] == "gdrive"
|
||||
assert data["base_path"] == "/data/nested"
|
||||
@@ -1,965 +0,0 @@
|
||||
"""Tests for source management routes (CombinedJson, PaginatedSources,
|
||||
DeleteByIds, DeleteOldIndexes, ManageSync, DirectoryStructure).
|
||||
|
||||
Note: SyncSource and _get_provider_from_remote_data are already covered in
|
||||
test_routes.py and are NOT duplicated here.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from bson import ObjectId
|
||||
from flask import Flask
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
app = Flask(__name__)
|
||||
return app
|
||||
|
||||
|
||||
def _status(response):
|
||||
if isinstance(response, tuple):
|
||||
return response[1]
|
||||
return response.status_code
|
||||
|
||||
|
||||
def _json(response):
|
||||
if isinstance(response, tuple):
|
||||
return response[0].json
|
||||
return response.json
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CombinedJson (/api/sources)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCombinedJson:
|
||||
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
with app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = CombinedJson().get()
|
||||
|
||||
assert _status(response) == 401
|
||||
|
||||
def test_returns_default_source_plus_user_sources(self, app):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
src_id = ObjectId()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = [
|
||||
{
|
||||
"_id": src_id,
|
||||
"name": "My Doc",
|
||||
"date": "2024-01-01",
|
||||
"tokens": "100",
|
||||
"retriever": "classic",
|
||||
"sync_frequency": "daily",
|
||||
"remote_data": json.dumps({"provider": "github"}),
|
||||
"directory_structure": None,
|
||||
"type": "file",
|
||||
}
|
||||
]
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = CombinedJson().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
data = _json(response)
|
||||
# First entry is always the Default
|
||||
assert data[0]["name"] == "Default"
|
||||
assert data[0]["date"] == "default"
|
||||
# Second entry is user source
|
||||
assert data[1]["id"] == str(src_id)
|
||||
assert data[1]["name"] == "My Doc"
|
||||
assert data[1]["provider"] == "github"
|
||||
assert data[1]["syncFrequency"] == "daily"
|
||||
assert data[1]["is_nested"] is False
|
||||
|
||||
def test_is_nested_true_when_directory_structure_present(self, app):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = [
|
||||
{
|
||||
"_id": ObjectId(),
|
||||
"name": "Nested",
|
||||
"date": "2024-01-01",
|
||||
"directory_structure": {"files": ["a.txt"]},
|
||||
}
|
||||
]
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = CombinedJson().get()
|
||||
|
||||
data = _json(response)
|
||||
assert data[1]["is_nested"] is True
|
||||
|
||||
def test_returns_400_on_db_error(self, app):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.side_effect = Exception("db err")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = CombinedJson().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
def test_type_defaults_to_file(self, app):
|
||||
from application.api.user.sources.routes import CombinedJson
|
||||
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = [
|
||||
{"_id": ObjectId(), "name": "X", "date": "d"}
|
||||
]
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/sources"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = CombinedJson().get()
|
||||
|
||||
data = _json(response)
|
||||
assert data[1]["type"] == "file"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PaginatedSources (/api/sources/paginated)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPaginatedSources:
|
||||
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
with app.test_request_context("/api/sources/paginated"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = PaginatedSources().get()
|
||||
|
||||
assert _status(response) == 401
|
||||
|
||||
def test_returns_paginated_results(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
ids = [ObjectId() for _ in range(3)]
|
||||
docs = [
|
||||
{"_id": ids[i], "name": f"Doc{i}", "date": f"2024-0{i + 1}-01"}
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = mock_cursor
|
||||
mock_cursor.skip.return_value = mock_cursor
|
||||
mock_cursor.limit.return_value = docs
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
mock_collection.count_documents.return_value = 3
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/sources/paginated?page=1&rows=10&sort=date&order=desc"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = PaginatedSources().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
data = _json(response)
|
||||
assert data["total"] == 3
|
||||
assert data["totalPages"] == 1
|
||||
assert data["currentPage"] == 1
|
||||
assert len(data["paginated"]) == 3
|
||||
|
||||
def test_search_filter_applies_regex(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = mock_cursor
|
||||
mock_cursor.skip.return_value = mock_cursor
|
||||
mock_cursor.limit.return_value = []
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
mock_collection.count_documents.return_value = 0
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/sources/paginated?search=test%20doc"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = PaginatedSources().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
# Verify search query was passed
|
||||
query_arg = mock_collection.count_documents.call_args[0][0]
|
||||
assert query_arg["name"]["$regex"] == "test doc"
|
||||
assert query_arg["name"]["$options"] == "i"
|
||||
|
||||
def test_ascending_sort_order(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = mock_cursor
|
||||
mock_cursor.skip.return_value = mock_cursor
|
||||
mock_cursor.limit.return_value = []
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
mock_collection.count_documents.return_value = 0
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/sources/paginated?order=asc&sort=name"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = PaginatedSources().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
mock_cursor.sort.assert_called_once_with("name", 1)
|
||||
|
||||
def test_page_clamped_to_valid_range(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = mock_cursor
|
||||
mock_cursor.skip.return_value = mock_cursor
|
||||
mock_cursor.limit.return_value = []
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
mock_collection.count_documents.return_value = 5 # 1 page with default 10 rows
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/sources/paginated?page=999"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = PaginatedSources().get()
|
||||
|
||||
data = _json(response)
|
||||
assert data["currentPage"] == 1 # clamped
|
||||
|
||||
def test_returns_400_on_db_error(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.count_documents.return_value = 0
|
||||
mock_collection.find.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/sources/paginated"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = PaginatedSources().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
def test_paginated_includes_provider_and_is_nested(self, app):
|
||||
from application.api.user.sources.routes import PaginatedSources
|
||||
|
||||
doc = {
|
||||
"_id": ObjectId(),
|
||||
"name": "S3 Src",
|
||||
"date": "2024-01-01",
|
||||
"remote_data": {"provider": "s3"},
|
||||
"directory_structure": {"dirs": ["a"]},
|
||||
"type": "s3",
|
||||
}
|
||||
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.sort.return_value = mock_cursor
|
||||
mock_cursor.skip.return_value = mock_cursor
|
||||
mock_cursor.limit.return_value = [doc]
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
mock_collection.count_documents.return_value = 1
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/sources/paginated"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = PaginatedSources().get()
|
||||
|
||||
data = _json(response)
|
||||
entry = data["paginated"][0]
|
||||
assert entry["provider"] == "s3"
|
||||
assert entry["isNested"] is True
|
||||
assert entry["type"] == "s3"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DeleteByIds (/api/delete_by_ids)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDeleteByIds:
|
||||
|
||||
def test_returns_400_when_path_missing(self, app):
|
||||
from application.api.user.sources.routes import DeleteByIds
|
||||
|
||||
with app.test_request_context("/api/delete_by_ids"):
|
||||
response = DeleteByIds().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
assert "Missing" in _json(response)["message"]
|
||||
|
||||
def test_returns_200_on_successful_delete(self, app):
|
||||
from application.api.user.sources.routes import DeleteByIds
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.delete_index.return_value = True
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/delete_by_ids?path=id1,id2"):
|
||||
response = DeleteByIds().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
assert _json(response)["success"] is True
|
||||
mock_collection.delete_index.assert_called_once_with(ids="id1,id2")
|
||||
|
||||
def test_returns_400_when_delete_returns_false(self, app):
|
||||
from application.api.user.sources.routes import DeleteByIds
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.delete_index.return_value = False
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/delete_by_ids?path=id1"):
|
||||
response = DeleteByIds().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
def test_returns_400_on_exception(self, app):
|
||||
from application.api.user.sources.routes import DeleteByIds
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.delete_index.side_effect = Exception("fail")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context("/api/delete_by_ids?path=id1"):
|
||||
response = DeleteByIds().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DeleteOldIndexes (/api/delete_old)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDeleteOldIndexes:
|
||||
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
with app.test_request_context("/api/delete_old?source_id=abc"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 401
|
||||
|
||||
def test_returns_400_when_source_id_missing(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
with app.test_request_context("/api/delete_old"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
def test_returns_404_when_doc_not_found(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(f"/api/delete_old?source_id={source_id}"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 404
|
||||
|
||||
def test_deletes_faiss_index_and_file(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
source_id = ObjectId()
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": source_id,
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/doc.pdf",
|
||||
}
|
||||
mock_storage = Mock()
|
||||
mock_storage.file_exists.return_value = True
|
||||
mock_storage.is_directory.return_value = False
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.settings"
|
||||
) as mock_settings:
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
with app.test_request_context(
|
||||
f"/api/delete_old?source_id={source_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
assert _json(response)["success"] is True
|
||||
# Should have checked and deleted faiss files
|
||||
assert mock_storage.delete_file.call_count >= 1
|
||||
mock_collection.delete_one.assert_called_once()
|
||||
|
||||
def test_deletes_non_faiss_vector_index(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
source_id = ObjectId()
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": source_id,
|
||||
"user": "u1",
|
||||
}
|
||||
mock_storage = Mock()
|
||||
mock_vectorstore = Mock()
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.VectorCreator.create_vectorstore",
|
||||
return_value=mock_vectorstore,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.settings"
|
||||
) as mock_settings:
|
||||
mock_settings.VECTOR_STORE = "elasticsearch"
|
||||
with app.test_request_context(
|
||||
f"/api/delete_old?source_id={source_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
mock_vectorstore.delete_index.assert_called_once()
|
||||
|
||||
def test_deletes_directory_of_files(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
source_id = ObjectId()
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": source_id,
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/mydir",
|
||||
}
|
||||
mock_storage = Mock()
|
||||
mock_storage.is_directory.return_value = True
|
||||
mock_storage.list_files.return_value = ["uploads/u1/mydir/a.txt", "uploads/u1/mydir/b.txt"]
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.settings"
|
||||
) as mock_settings:
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
mock_storage.file_exists.return_value = False
|
||||
with app.test_request_context(
|
||||
f"/api/delete_old?source_id={source_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
# Each file in directory should be deleted
|
||||
assert mock_storage.delete_file.call_count == 2
|
||||
|
||||
def test_handles_file_not_found_gracefully(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
source_id = ObjectId()
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": source_id,
|
||||
"user": "u1",
|
||||
"file_path": "uploads/missing.pdf",
|
||||
}
|
||||
mock_storage = Mock()
|
||||
mock_storage.is_directory.side_effect = FileNotFoundError()
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.settings"
|
||||
) as mock_settings:
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
mock_storage.file_exists.return_value = False
|
||||
with app.test_request_context(
|
||||
f"/api/delete_old?source_id={source_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
mock_collection.delete_one.assert_called_once()
|
||||
|
||||
def test_returns_400_on_general_error(self, app):
|
||||
from application.api.user.sources.routes import DeleteOldIndexes
|
||||
|
||||
source_id = ObjectId()
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": source_id,
|
||||
"user": "u1",
|
||||
}
|
||||
mock_storage = Mock()
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.routes.settings"
|
||||
) as mock_settings:
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
mock_storage.file_exists.side_effect = RuntimeError("disk error")
|
||||
with app.test_request_context(
|
||||
f"/api/delete_old?source_id={source_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DeleteOldIndexes().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ManageSync (/api/manage_sync)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestManageSync:
|
||||
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST",
|
||||
json={"source_id": "x", "sync_frequency": "daily"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = ManageSync().post()
|
||||
|
||||
assert _status(response) == 401
|
||||
|
||||
def test_returns_400_missing_fields(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST",
|
||||
json={"source_id": "abc"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSync().post()
|
||||
|
||||
assert response is not None
|
||||
|
||||
def test_returns_400_for_invalid_frequency(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST",
|
||||
json={"source_id": str(ObjectId()), "sync_frequency": "hourly"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSync().post()
|
||||
|
||||
assert _status(response) == 400
|
||||
assert "Invalid frequency" in _json(response)["message"]
|
||||
|
||||
def test_updates_sync_frequency_successfully(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST",
|
||||
json={"source_id": source_id, "sync_frequency": "weekly"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSync().post()
|
||||
|
||||
assert _status(response) == 200
|
||||
assert _json(response)["success"] is True
|
||||
call_args = mock_collection.update_one.call_args
|
||||
assert call_args[0][0]["_id"] == ObjectId(source_id)
|
||||
assert call_args[0][0]["user"] == "u1"
|
||||
assert call_args[0][1]["$set"]["sync_frequency"] == "weekly"
|
||||
|
||||
def test_accepts_all_valid_frequencies(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
mock_collection = Mock()
|
||||
|
||||
for freq in ["never", "daily", "weekly", "monthly"]:
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST",
|
||||
json={"source_id": str(ObjectId()), "sync_frequency": freq},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSync().post()
|
||||
|
||||
assert _status(response) == 200
|
||||
|
||||
def test_returns_400_on_db_error(self, app):
|
||||
from application.api.user.sources.routes import ManageSync
|
||||
|
||||
mock_collection = Mock()
|
||||
mock_collection.update_one.side_effect = Exception("db err")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/manage_sync", method="POST",
|
||||
json={"source_id": str(ObjectId()), "sync_frequency": "daily"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSync().post()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RedirectToSources (/api/combine)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestRedirectToSources:
|
||||
|
||||
def test_redirects_to_sources(self, app):
|
||||
from application.api.user.sources.routes import RedirectToSources
|
||||
|
||||
with app.test_request_context("/api/combine"):
|
||||
response = RedirectToSources().get()
|
||||
|
||||
assert response.status_code == 301
|
||||
assert response.location == "/api/sources"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DirectoryStructure (/api/directory_structure)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDirectoryStructure:
|
||||
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
with app.test_request_context("/api/directory_structure?id=abc"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
assert _status(response) == 401
|
||||
|
||||
def test_returns_400_when_id_missing(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
with app.test_request_context("/api/directory_structure"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
assert "required" in _json(response)["error"]
|
||||
|
||||
def test_returns_400_for_invalid_doc_id(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
with app.test_request_context("/api/directory_structure?id=invalid"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
assert "Invalid" in _json(response)["error"]
|
||||
|
||||
def test_returns_404_when_doc_not_found(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
doc_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(f"/api/directory_structure?id={doc_id}"):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
assert _status(response) == 404
|
||||
assert "not found" in _json(response)["error"]
|
||||
|
||||
def test_returns_directory_structure(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
doc_id = ObjectId()
|
||||
dir_struct = {"dirs": ["a", "b"], "files": ["c.txt"]}
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": doc_id,
|
||||
"user": "u1",
|
||||
"directory_structure": dir_struct,
|
||||
"file_path": "uploads/u1/mydir",
|
||||
"remote_data": json.dumps({"provider": "github"}),
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
f"/api/directory_structure?id={doc_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
assert _status(response) == 200
|
||||
data = _json(response)
|
||||
assert data["success"] is True
|
||||
assert data["directory_structure"] == dir_struct
|
||||
assert data["base_path"] == "uploads/u1/mydir"
|
||||
assert data["provider"] == "github"
|
||||
|
||||
def test_returns_none_provider_when_no_remote_data(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
doc_id = ObjectId()
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": doc_id,
|
||||
"user": "u1",
|
||||
"directory_structure": {},
|
||||
"file_path": "path",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
f"/api/directory_structure?id={doc_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
data = _json(response)
|
||||
assert data["provider"] is None
|
||||
|
||||
def test_handles_invalid_remote_data_json(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
doc_id = ObjectId()
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": doc_id,
|
||||
"user": "u1",
|
||||
"directory_structure": {},
|
||||
"file_path": "path",
|
||||
"remote_data": "not-valid-json{",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
f"/api/directory_structure?id={doc_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
data = _json(response)
|
||||
assert data["success"] is True
|
||||
assert data["provider"] is None
|
||||
|
||||
def test_returns_500_on_general_error(self, app):
|
||||
from application.api.user.sources.routes import DirectoryStructure
|
||||
|
||||
doc_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.routes.sources_collection",
|
||||
mock_collection,
|
||||
):
|
||||
with app.test_request_context(
|
||||
f"/api/directory_structure?id={doc_id}"
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = DirectoryStructure().get()
|
||||
|
||||
assert _status(response) == 500
|
||||
+574
-1507
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user