refactor: rename the application package to docsgpt

The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
This commit is contained in:
Alex committed 2026-09-07 10:20:43 +01:00
1 parent 3e7f1f91b4
commit 574f96341e
984 files changed
+9847 -9682

No files matched your search

+38 -38
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/sources/chunks.py."""
"""Tests for docsgpt/api/user/sources/chunks.py."""
from contextlib import contextmanager
from unittest.mock import MagicMock, patch
@@ -19,19 +19,19 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.sources.chunks.db_readonly", _yield
"docsgpt.api.user.sources.chunks.db_readonly", _yield
):
yield
def _seed_source(pg_conn, user="u", name="src"):
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
return SourcesRepository(pg_conn).create(name, user_id=user)
class TestResolveSource:
def test_returns_none_for_missing(self, pg_conn):
from application.api.user.sources.chunks import _resolve_source
from docsgpt.api.user.sources.chunks import _resolve_source
with _patch_db(pg_conn):
assert (
_resolve_source(
@@ -41,7 +41,7 @@ class TestResolveSource:
)
def test_returns_source_when_found(self, pg_conn):
from application.api.user.sources.chunks import _resolve_source
from docsgpt.api.user.sources.chunks import _resolve_source
src = _seed_source(pg_conn, user="u-resolve")
with _patch_db(pg_conn):
@@ -52,7 +52,7 @@ class TestResolveSource:
class TestGetChunks:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
with app.test_request_context("/api/get_chunks?id=abc"):
from flask import request
@@ -61,7 +61,7 @@ class TestGetChunks:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
with app.test_request_context("/api/get_chunks"):
from flask import request
@@ -70,7 +70,7 @@ class TestGetChunks:
assert response.status_code == 400
def test_returns_400_on_resolve_error(self, app):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
@contextmanager
def _broken():
@@ -78,7 +78,7 @@ class TestGetChunks:
yield
with patch(
"application.api.user.sources.chunks.db_readonly", _broken
"docsgpt.api.user.sources.chunks.db_readonly", _broken
), app.test_request_context("/api/get_chunks?id=abc"):
from flask import request
request.decoded_token = {"sub": "u"}
@@ -86,7 +86,7 @@ class TestGetChunks:
assert response.status_code == 400
def test_returns_404_when_source_missing(self, app, pg_conn):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
with _patch_db(pg_conn), app.test_request_context(
"/api/get_chunks?id=00000000-0000-0000-0000-000000000000"
@@ -97,7 +97,7 @@ class TestGetChunks:
assert response.status_code == 404
def test_returns_paginated_chunks(self, app, pg_conn):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
user = "u-chunks"
src = _seed_source(pg_conn, user=user)
@@ -109,7 +109,7 @@ class TestGetChunks:
]
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
f"/api/get_chunks?id={src['id']}&per_page=2&page=1"
@@ -123,7 +123,7 @@ class TestGetChunks:
assert len(data["chunks"]) == 2
def test_filters_by_path(self, app, pg_conn):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
user = "u-path"
src = _seed_source(pg_conn, user=user)
@@ -135,7 +135,7 @@ class TestGetChunks:
]
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
f"/api/get_chunks?id={src['id']}&path=b/file.txt"
@@ -147,7 +147,7 @@ class TestGetChunks:
assert response.json["total"] == 1
def test_filters_by_search(self, app, pg_conn):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
user = "u-srch"
src = _seed_source(pg_conn, user=user)
@@ -159,7 +159,7 @@ class TestGetChunks:
]
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
f"/api/get_chunks?id={src['id']}&search=cat"
@@ -171,13 +171,13 @@ class TestGetChunks:
assert response.json["total"] == 1
def test_returns_500_on_vector_store_error(self, app, pg_conn):
from application.api.user.sources.chunks import GetChunks
from docsgpt.api.user.sources.chunks import GetChunks
user = "u-err"
src = _seed_source(pg_conn, user=user)
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
side_effect=RuntimeError("boom"),
), app.test_request_context(f"/api/get_chunks?id={src['id']}"):
from flask import request
@@ -188,7 +188,7 @@ class TestGetChunks:
class TestAddChunk:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import AddChunk
from docsgpt.api.user.sources.chunks import AddChunk
with app.test_request_context(
"/api/add_chunk", method="POST",
@@ -200,7 +200,7 @@ class TestAddChunk:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.sources.chunks import AddChunk
from docsgpt.api.user.sources.chunks import AddChunk
with app.test_request_context(
"/api/add_chunk", method="POST", json={"id": "x"}
@@ -213,7 +213,7 @@ class TestAddChunk:
def test_returns_403_inaccessible_source(self, app, pg_conn):
# No ownership and no team editor grant resolves to None, which the
# owner-or-editor gate answers as 403 "Source not accessible".
from application.api.user.sources.chunks import AddChunk
from docsgpt.api.user.sources.chunks import AddChunk
with _patch_db(pg_conn), app.test_request_context(
"/api/add_chunk", method="POST",
@@ -228,7 +228,7 @@ class TestAddChunk:
assert response.status_code == 403
def test_adds_chunk(self, app, pg_conn):
from application.api.user.sources.chunks import AddChunk
from docsgpt.api.user.sources.chunks import AddChunk
user = "u-add"
src = _seed_source(pg_conn, user=user)
@@ -237,7 +237,7 @@ class TestAddChunk:
fake_store.add_chunk.return_value = "chunk-id-1"
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
"/api/add_chunk", method="POST",
@@ -254,13 +254,13 @@ class TestAddChunk:
assert response.json["chunk_id"] == "chunk-id-1"
def test_returns_500_on_vector_error(self, app, pg_conn):
from application.api.user.sources.chunks import AddChunk
from docsgpt.api.user.sources.chunks import AddChunk
user = "u-adderr"
src = _seed_source(pg_conn, user=user)
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
side_effect=RuntimeError("bad"),
), app.test_request_context(
"/api/add_chunk", method="POST",
@@ -274,7 +274,7 @@ class TestAddChunk:
class TestDeleteChunk:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import DeleteChunk
from docsgpt.api.user.sources.chunks import DeleteChunk
with app.test_request_context(
"/api/delete_chunk?id=x&chunk_id=y", method="DELETE"
@@ -287,7 +287,7 @@ class TestDeleteChunk:
def test_returns_403_inaccessible_source(self, app, pg_conn):
# No ownership and no team editor grant resolves to None, which the
# owner-or-editor gate answers as 403 "Source not accessible".
from application.api.user.sources.chunks import DeleteChunk
from docsgpt.api.user.sources.chunks import DeleteChunk
with _patch_db(pg_conn), app.test_request_context(
"/api/delete_chunk?id=00000000-0000-0000-0000-000000000000&chunk_id=c",
@@ -299,7 +299,7 @@ class TestDeleteChunk:
assert response.status_code == 403
def test_deletes_chunk(self, app, pg_conn):
from application.api.user.sources.chunks import DeleteChunk
from docsgpt.api.user.sources.chunks import DeleteChunk
user = "u-del"
src = _seed_source(pg_conn, user=user)
@@ -308,7 +308,7 @@ class TestDeleteChunk:
fake_store.delete_chunk.return_value = True
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
f"/api/delete_chunk?id={src['id']}&chunk_id=c", method="DELETE"
@@ -319,7 +319,7 @@ class TestDeleteChunk:
assert response.status_code == 200
def test_returns_404_chunk_not_found(self, app, pg_conn):
from application.api.user.sources.chunks import DeleteChunk
from docsgpt.api.user.sources.chunks import DeleteChunk
user = "u-missing-chunk"
src = _seed_source(pg_conn, user=user)
@@ -328,7 +328,7 @@ class TestDeleteChunk:
fake_store.delete_chunk.return_value = False
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
f"/api/delete_chunk?id={src['id']}&chunk_id=c", method="DELETE"
@@ -341,7 +341,7 @@ class TestDeleteChunk:
class TestUpdateChunk:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import UpdateChunk
from docsgpt.api.user.sources.chunks import UpdateChunk
with app.test_request_context(
"/api/update_chunk", method="PUT",
@@ -353,7 +353,7 @@ class TestUpdateChunk:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.sources.chunks import UpdateChunk
from docsgpt.api.user.sources.chunks import UpdateChunk
with app.test_request_context(
"/api/update_chunk", method="PUT", json={"id": "x"}
@@ -366,7 +366,7 @@ class TestUpdateChunk:
def test_returns_403_inaccessible_source(self, app, pg_conn):
# No ownership and no team editor grant resolves to None, which the
# owner-or-editor gate answers as 403 "Source not accessible".
from application.api.user.sources.chunks import UpdateChunk
from docsgpt.api.user.sources.chunks import UpdateChunk
with _patch_db(pg_conn), app.test_request_context(
"/api/update_chunk", method="PUT",
@@ -381,7 +381,7 @@ class TestUpdateChunk:
assert response.status_code == 403
def test_returns_404_chunk_not_found(self, app, pg_conn):
from application.api.user.sources.chunks import UpdateChunk
from docsgpt.api.user.sources.chunks import UpdateChunk
user = "u-upd-missing"
src = _seed_source(pg_conn, user=user)
@@ -389,7 +389,7 @@ class TestUpdateChunk:
fake_store.get_chunks.return_value = []
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
"/api/update_chunk", method="PUT",
@@ -401,7 +401,7 @@ class TestUpdateChunk:
assert response.status_code == 404
def test_updates_chunk(self, app, pg_conn):
from application.api.user.sources.chunks import UpdateChunk
from docsgpt.api.user.sources.chunks import UpdateChunk
user = "u-upd"
src = _seed_source(pg_conn, user=user)
@@ -418,7 +418,7 @@ class TestUpdateChunk:
fake_store.delete_chunk.return_value = True
with _patch_db(pg_conn), patch(
"application.api.user.sources.chunks.get_vector_store",
"docsgpt.api.user.sources.chunks.get_vector_store",
return_value=fake_store,
), app.test_request_context(
"/api/update_chunk", method="PUT",
+23 -23
View File
@@ -1,5 +1,5 @@
"""Tests for the GraphRAG graph-view routes in
application/api/user/sources/routes.py.
docsgpt/api/user/sources/routes.py.
The endpoints are read-access gated (owner or team grant). The ``GraphStore`` is
mocked so no live vector store, embeddings, or LLM calls run; the ``sources`` row
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
@pytest.fixture
@@ -30,21 +30,21 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.sources.routes.db_session", _yield
"docsgpt.api.user.sources.routes.db_session", _yield
), patch(
"application.api.user.sources.routes.db_readonly", _yield
"docsgpt.api.user.sources.routes.db_readonly", _yield
):
yield
def _grant_team_access(pg_conn, owner, member, source_id, access_level):
from application.storage.db.repositories.team_members import (
from docsgpt.storage.db.repositories.team_members import (
TeamMembersRepository,
)
from application.storage.db.repositories.team_resource_grants import (
from docsgpt.storage.db.repositories.team_resource_grants import (
TeamResourceGrantsRepository,
)
from application.storage.db.repositories.teams import TeamsRepository
from docsgpt.storage.db.repositories.teams import TeamsRepository
team = TeamsRepository(pg_conn).create(
"Acme", f"acme-{uuid.uuid4().hex[:8]}", owner
@@ -73,7 +73,7 @@ def _graphrag_source(pg_conn, user):
@pytest.mark.unit
class TestSourceGraph:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import SourceGraph
from docsgpt.api.user.sources.routes import SourceGraph
with app.test_request_context("/api/sources/x/graph"):
from flask import request
@@ -82,7 +82,7 @@ class TestSourceGraph:
assert response.status_code == 401
def test_owner_gets_bounded_overview(self, app, pg_conn):
from application.api.user.sources.routes import SourceGraph
from docsgpt.api.user.sources.routes import SourceGraph
user = "u-graph-view-owner"
sid = _graphrag_source(pg_conn, user)
@@ -102,7 +102,7 @@ class TestSourceGraph:
}
with _patch_db(pg_conn), patch(
"application.graphrag.store.GraphStore", return_value=store
"docsgpt.graphrag.store.GraphStore", return_value=store
), app.test_request_context(
f"/api/sources/{sid}/graph?limit=9999"
):
@@ -122,7 +122,7 @@ class TestSourceGraph:
assert args[1] == 9999
def test_empty_graph_returns_empty_lists(self, app, pg_conn):
from application.api.user.sources.routes import SourceGraph
from docsgpt.api.user.sources.routes import SourceGraph
user = "u-graph-view-empty"
sid = _graphrag_source(pg_conn, user)
@@ -131,7 +131,7 @@ class TestSourceGraph:
store.count_nodes.return_value = 0
with _patch_db(pg_conn), patch(
"application.graphrag.store.GraphStore", return_value=store
"docsgpt.graphrag.store.GraphStore", return_value=store
), app.test_request_context(f"/api/sources/{sid}/graph"):
from flask import request
request.decoded_token = {"sub": user}
@@ -144,14 +144,14 @@ class TestSourceGraph:
store.get_graph_overview.assert_not_called()
def test_non_owner_without_grant_404(self, app, pg_conn):
from application.api.user.sources.routes import SourceGraph
from docsgpt.api.user.sources.routes import SourceGraph
owner = "u-graph-view-owner2"
stranger = "u-graph-view-stranger"
sid = _graphrag_source(pg_conn, owner)
with _patch_db(pg_conn), patch(
"application.graphrag.store.GraphStore"
"docsgpt.graphrag.store.GraphStore"
) as mock_store, app.test_request_context(
f"/api/sources/{sid}/graph"
):
@@ -163,7 +163,7 @@ class TestSourceGraph:
mock_store.assert_not_called()
def test_team_viewer_can_read(self, app, pg_conn):
from application.api.user.sources.routes import SourceGraph
from docsgpt.api.user.sources.routes import SourceGraph
owner = "alice-graph-view"
viewer = "bob-graph-view-viewer"
@@ -181,7 +181,7 @@ class TestSourceGraph:
}
with _patch_db(pg_conn), patch(
"application.graphrag.store.GraphStore", return_value=store
"docsgpt.graphrag.store.GraphStore", return_value=store
), app.test_request_context(f"/api/sources/{sid}/graph"):
from flask import request
request.decoded_token = {"sub": viewer}
@@ -194,7 +194,7 @@ class TestSourceGraph:
@pytest.mark.unit
class TestSourceGraphNode:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import SourceGraphNode
from docsgpt.api.user.sources.routes import SourceGraphNode
with app.test_request_context("/api/sources/x/graph/node/n"):
from flask import request
@@ -203,7 +203,7 @@ class TestSourceGraphNode:
assert response.status_code == 401
def test_owner_gets_node_detail_with_chunks(self, app, pg_conn):
from application.api.user.sources.routes import SourceGraphNode
from docsgpt.api.user.sources.routes import SourceGraphNode
user = "u-graph-node-owner"
sid = _graphrag_source(pg_conn, user)
@@ -220,7 +220,7 @@ class TestSourceGraphNode:
}
with _patch_db(pg_conn), patch(
"application.graphrag.store.GraphStore", return_value=store
"docsgpt.graphrag.store.GraphStore", return_value=store
), app.test_request_context(f"/api/sources/{sid}/graph/node/n1"):
from flask import request
request.decoded_token = {"sub": user}
@@ -232,7 +232,7 @@ class TestSourceGraphNode:
store.get_node_detail.assert_called_once_with(sid, "n1")
def test_unknown_node_404(self, app, pg_conn):
from application.api.user.sources.routes import SourceGraphNode
from docsgpt.api.user.sources.routes import SourceGraphNode
user = "u-graph-node-missing"
sid = _graphrag_source(pg_conn, user)
@@ -241,7 +241,7 @@ class TestSourceGraphNode:
store.get_node_detail.return_value = None
with _patch_db(pg_conn), patch(
"application.graphrag.store.GraphStore", return_value=store
"docsgpt.graphrag.store.GraphStore", return_value=store
), app.test_request_context(f"/api/sources/{sid}/graph/node/nope"):
from flask import request
request.decoded_token = {"sub": user}
@@ -250,14 +250,14 @@ class TestSourceGraphNode:
assert response.status_code == 404
def test_non_owner_without_grant_404(self, app, pg_conn):
from application.api.user.sources.routes import SourceGraphNode
from docsgpt.api.user.sources.routes import SourceGraphNode
owner = "u-graph-node-owner2"
stranger = "u-graph-node-stranger"
sid = _graphrag_source(pg_conn, owner)
with _patch_db(pg_conn), patch(
"application.graphrag.store.GraphStore"
"docsgpt.graphrag.store.GraphStore"
) as mock_store, app.test_request_context(
f"/api/sources/{sid}/graph/node/n1"
):
+20 -20
View File
@@ -1,4 +1,4 @@
"""Tests for the GraphRAG enable route in application/api/user/sources/routes.py.
"""Tests for the GraphRAG enable route in docsgpt/api/user/sources/routes.py.
``graphrag_available`` and ``extract_graph.delay`` are mocked so no live
vector store, LLM, or model calls run; the ``sources`` row is real so the
@@ -12,8 +12,8 @@ from unittest.mock import patch
import pytest
from flask import Flask
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.source_config import SourceConfig
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.source_config import SourceConfig
@pytest.fixture
@@ -28,21 +28,21 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.sources.routes.db_session", _yield
"docsgpt.api.user.sources.routes.db_session", _yield
), patch(
"application.api.user.sources.routes.db_readonly", _yield
"docsgpt.api.user.sources.routes.db_readonly", _yield
):
yield
def _grant_team_access(pg_conn, owner, member, source_id, access_level):
from application.storage.db.repositories.team_members import (
from docsgpt.storage.db.repositories.team_members import (
TeamMembersRepository,
)
from application.storage.db.repositories.team_resource_grants import (
from docsgpt.storage.db.repositories.team_resource_grants import (
TeamResourceGrantsRepository,
)
from application.storage.db.repositories.teams import TeamsRepository
from docsgpt.storage.db.repositories.teams import TeamsRepository
team = TeamsRepository(pg_conn).create(
"Acme", f"acme-{uuid.uuid4().hex[:8]}", owner
@@ -59,7 +59,7 @@ def _grant_team_access(pg_conn, owner, member, source_id, access_level):
@pytest.mark.unit
class TestEnableSourceGraphRAG:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import EnableSourceGraphRAG
from docsgpt.api.user.sources.routes import EnableSourceGraphRAG
with app.test_request_context(
"/api/sources/x/graphrag/enable", method="POST"
@@ -70,7 +70,7 @@ class TestEnableSourceGraphRAG:
assert response.status_code == 401
def test_unavailable_returns_400(self, app, pg_conn):
from application.api.user.sources.routes import EnableSourceGraphRAG
from docsgpt.api.user.sources.routes import EnableSourceGraphRAG
user = "u-graph-unavail"
src = SourcesRepository(pg_conn).create(
@@ -80,10 +80,10 @@ class TestEnableSourceGraphRAG:
sid = str(src["id"])
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.graphrag_available",
"docsgpt.api.user.sources.routes.graphrag_available",
return_value=False,
), patch(
"application.api.user.sources.routes.extract_graph.delay"
"docsgpt.api.user.sources.routes.extract_graph.delay"
) as mock_extract, app.test_request_context(
f"/api/sources/{sid}/graphrag/enable", method="POST"
):
@@ -97,7 +97,7 @@ class TestEnableSourceGraphRAG:
assert SourceConfig.parse(got.get("config")).kind == "classic"
def test_owner_sets_config_and_enqueues(self, app, pg_conn):
from application.api.user.sources.routes import EnableSourceGraphRAG
from docsgpt.api.user.sources.routes import EnableSourceGraphRAG
user = "u-graph-owner"
src = SourcesRepository(pg_conn).create(
@@ -108,12 +108,12 @@ class TestEnableSourceGraphRAG:
fake_task = type("T", (), {"id": "task-g"})()
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.graphrag_available",
"docsgpt.api.user.sources.routes.graphrag_available",
return_value=True,
), patch(
"application.worker._reset_graph_for_source",
"docsgpt.worker._reset_graph_for_source",
) as mock_reset, patch(
"application.api.user.sources.routes.extract_graph.delay",
"docsgpt.api.user.sources.routes.extract_graph.delay",
return_value=fake_task,
) as mock_extract, app.test_request_context(
f"/api/sources/{sid}/graphrag/enable", method="POST"
@@ -143,7 +143,7 @@ class TestEnableSourceGraphRAG:
assert key != f"extract-graph:{sid}:"
def test_viewer_rejected_403(self, app, pg_conn):
from application.api.user.sources.routes import EnableSourceGraphRAG
from docsgpt.api.user.sources.routes import EnableSourceGraphRAG
owner = "alice-graph"
viewer = "bob-graph-viewer"
@@ -155,10 +155,10 @@ class TestEnableSourceGraphRAG:
_grant_team_access(pg_conn, owner, viewer, sid, "viewer")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.graphrag_available",
"docsgpt.api.user.sources.routes.graphrag_available",
return_value=True,
), patch(
"application.api.user.sources.routes.extract_graph.delay"
"docsgpt.api.user.sources.routes.extract_graph.delay"
) as mock_extract, app.test_request_context(
f"/api/sources/{sid}/graphrag/enable", method="POST"
):
@@ -177,7 +177,7 @@ class TestConfigPatchCannotSetGraphrag:
"""The config PATCH endpoint must not flip kind to graphrag (D28)."""
def test_patch_kind_graphrag_rejected_400(self, app, pg_conn):
from application.api.user.sources.routes import SourceConfigResource
from docsgpt.api.user.sources.routes import SourceConfigResource
user = "u-patch-graph"
src = SourcesRepository(pg_conn).create(
@@ -16,7 +16,7 @@ import pytest
@pytest.fixture
def client():
from application.app import app as flask_app
from docsgpt.app import app as flask_app
flask_app.config["TESTING"] = True
return flask_app.test_client()
@@ -45,12 +45,12 @@ def _source_row(sid, *, user_id):
def _run(sub, repo, team_shared, client):
patches = [
patch("application.app.handle_auth", return_value={"sub": sub}),
patch("application.app.resolve_roles", return_value=["user"]),
patch("application.api.user.sources.routes.db_readonly", lambda: _cm(Mock())),
patch("application.api.user.sources.routes.SourcesRepository", return_value=repo),
patch("docsgpt.app.handle_auth", return_value={"sub": sub}),
patch("docsgpt.app.resolve_roles", return_value=["user"]),
patch("docsgpt.api.user.sources.routes.db_readonly", lambda: _cm(Mock())),
patch("docsgpt.api.user.sources.routes.SourcesRepository", return_value=repo),
patch(
"application.api.user.sources.routes.visible_with_access",
"docsgpt.api.user.sources.routes.visible_with_access",
return_value=team_shared,
),
]
+25 -25
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/sources/retrieval_test.py."""
"""Tests for docsgpt/api/user/sources/retrieval_test.py."""
import json
import uuid
@@ -20,18 +20,18 @@ def _patch_db(conn):
def _yield():
yield conn
with patch("application.api.user.sources.retrieval_test.db_readonly", _yield):
with patch("docsgpt.api.user.sources.retrieval_test.db_readonly", _yield):
yield
def _grant_team_access(pg_conn, owner, member, source_id, access_level):
from application.storage.db.repositories.team_members import (
from docsgpt.storage.db.repositories.team_members import (
TeamMembersRepository,
)
from application.storage.db.repositories.team_resource_grants import (
from docsgpt.storage.db.repositories.team_resource_grants import (
TeamResourceGrantsRepository,
)
from application.storage.db.repositories.teams import TeamsRepository
from docsgpt.storage.db.repositories.teams import TeamsRepository
team = TeamsRepository(pg_conn).create(
"Acme", f"acme-{uuid.uuid4().hex[:8]}", owner
@@ -48,7 +48,7 @@ def _grant_team_access(pg_conn, owner, member, source_id, access_level):
def _seed_source(pg_conn, user="u", name="src", config=None):
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
repo = SourcesRepository(pg_conn)
src = repo.create(name, user_id=user)
@@ -59,7 +59,7 @@ def _seed_source(pg_conn, user="u", name="src", config=None):
def _post(app, source_id, body, user="u"):
from application.api.user.sources.retrieval_test import SourceSearch
from docsgpt.api.user.sources.retrieval_test import SourceSearch
with app.test_request_context(
f"/api/sources/{source_id}/search",
@@ -75,7 +75,7 @@ def _post(app, source_id, body, user="u"):
class TestSourceSearchGuards:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.retrieval_test import SourceSearch
from docsgpt.api.user.sources.retrieval_test import SourceSearch
with app.test_request_context(
"/api/sources/abc/search",
@@ -96,7 +96,7 @@ class TestSourceSearchGuards:
assert response.status_code == 400
def test_returns_400_for_overlong_query(self, app, pg_conn):
from application.api.user.sources.retrieval_test import MAX_QUERY_LENGTH
from docsgpt.api.user.sources.retrieval_test import MAX_QUERY_LENGTH
src = _seed_source(pg_conn, user="u-long")
with _patch_db(pg_conn):
@@ -153,7 +153,7 @@ class TestSourceSearchAccess:
fake.search.return_value = [{"text": "secret", "filename": "f.md"}]
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
) as dispatcher:
response = _post(
@@ -174,7 +174,7 @@ class TestSourceSearchAccess:
fake.search.return_value = [{"text": "shared chunk", "filename": "f.md"}]
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
):
response = _post(app, str(src["id"]), {"query": "q"}, user=viewer)
@@ -209,7 +209,7 @@ class TestSourceSearchRetrieval:
]
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
):
response = _post(app, str(src["id"]), {"query": "what runs"}, user=user)
@@ -232,7 +232,7 @@ class TestSourceSearchRetrieval:
fake.search.return_value = [{"text": "graph chunk", "filename": "g.md"}]
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
):
response = _post(app, str(src["id"]), {"query": "q"}, user=user)
@@ -244,7 +244,7 @@ class TestSourceSearchRetrieval:
def test_ad_hoc_config_is_passed_to_dispatcher_and_not_persisted(
self, app, pg_conn
):
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-adhoc"
src = _seed_source(pg_conn, user=user)
@@ -253,7 +253,7 @@ class TestSourceSearchRetrieval:
fake.search.return_value = []
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
) as dispatcher:
response = _post(
@@ -290,7 +290,7 @@ class TestSourceSearchRetrieval:
fake.search.return_value = []
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
) as dispatcher:
response = _post(app, str(src["id"]), {"query": "q"}, user=user)
@@ -307,7 +307,7 @@ class TestSourceSearchRetrieval:
fake.search.side_effect = RuntimeError("vector store down")
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
):
response = _post(app, str(src["id"]), {"query": "q"}, user=user)
@@ -324,7 +324,7 @@ class TestPrescreenCostCeiling:
src = _seed_source(pg_conn, user="u-costly")
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher"
"docsgpt.api.user.sources.retrieval_test.Dispatcher"
) as dispatcher:
response = _post(
app,
@@ -354,7 +354,7 @@ class TestPrescreenCostCeiling:
fake.search.return_value = []
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
) as dispatcher:
response = _post(
@@ -395,7 +395,7 @@ class TestPrescreenCostCeiling:
fake.search.return_value = []
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
) as dispatcher:
response = _post(
@@ -417,7 +417,7 @@ class TestPrescreenCostCeiling:
src = _seed_source(pg_conn, user="u-edit-costly", config={"retrieval": saved})
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.Dispatcher"
"docsgpt.api.user.sources.retrieval_test.Dispatcher"
) as dispatcher:
response = _post(
app,
@@ -452,10 +452,10 @@ class TestModelResolution:
fake.search.return_value = []
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.get_default_model_id",
"docsgpt.api.user.sources.retrieval_test.get_default_model_id",
return_value="gpt-4o",
), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
) as dispatcher:
response = _post(app, str(src["id"]), {"query": "q"}, user="u-model")
@@ -472,10 +472,10 @@ class TestModelResolution:
fake.search.return_value = []
with _patch_db(pg_conn), patch(
"application.api.user.sources.retrieval_test.get_default_model_id",
"docsgpt.api.user.sources.retrieval_test.get_default_model_id",
return_value=None,
), patch(
"application.api.user.sources.retrieval_test.Dispatcher",
"docsgpt.api.user.sources.retrieval_test.Dispatcher",
return_value=fake,
) as dispatcher:
response = _post(app, str(src["id"]), {"query": "q"}, user="u-nomodel")
+97 -97
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/sources/routes.py."""
"""Tests for docsgpt/api/user/sources/routes.py."""
import json
from contextlib import contextmanager
@@ -21,15 +21,15 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.sources.routes.db_session", _yield
"docsgpt.api.user.sources.routes.db_session", _yield
), patch(
"application.api.user.sources.routes.db_readonly", _yield
"docsgpt.api.user.sources.routes.db_readonly", _yield
):
yield
def _seed_source(pg_conn, user, **kwargs):
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
return SourcesRepository(pg_conn).create(
kwargs.pop("name", "src"),
user_id=user,
@@ -39,14 +39,14 @@ def _seed_source(pg_conn, user, **kwargs):
class TestGetProviderFromRemoteData:
def test_returns_none_for_empty(self):
from application.api.user.sources.routes import (
from docsgpt.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_from_dict(self):
from application.api.user.sources.routes import (
from docsgpt.api.user.sources.routes import (
_get_provider_from_remote_data,
)
assert (
@@ -55,7 +55,7 @@ class TestGetProviderFromRemoteData:
)
def test_returns_from_json_string(self):
from application.api.user.sources.routes import (
from docsgpt.api.user.sources.routes import (
_get_provider_from_remote_data,
)
assert (
@@ -64,7 +64,7 @@ class TestGetProviderFromRemoteData:
)
def test_returns_none_for_malformed_json(self):
from application.api.user.sources.routes import (
from docsgpt.api.user.sources.routes import (
_get_provider_from_remote_data,
)
assert _get_provider_from_remote_data("not-json") is None
@@ -72,7 +72,7 @@ class TestGetProviderFromRemoteData:
class TestCombinedJson:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import CombinedJson
from docsgpt.api.user.sources.routes import CombinedJson
with app.test_request_context("/api/sources"):
from flask import request
@@ -81,7 +81,7 @@ class TestCombinedJson:
assert response.status_code == 401
def test_returns_default_plus_user_sources(self, app, pg_conn):
from application.api.user.sources.routes import CombinedJson
from docsgpt.api.user.sources.routes import CombinedJson
user = "u-list-sources"
_seed_source(pg_conn, user, name="doc1", tokens="100")
@@ -97,7 +97,7 @@ class TestCombinedJson:
assert "doc1" in names
def test_db_error_returns_400(self, app):
from application.api.user.sources.routes import CombinedJson
from docsgpt.api.user.sources.routes import CombinedJson
@contextmanager
def _broken():
@@ -105,7 +105,7 @@ class TestCombinedJson:
yield
with patch(
"application.api.user.sources.routes.db_readonly", _broken
"docsgpt.api.user.sources.routes.db_readonly", _broken
), app.test_request_context("/api/sources"):
from flask import request
request.decoded_token = {"sub": "u"}
@@ -115,7 +115,7 @@ class TestCombinedJson:
class TestPaginatedSources:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import PaginatedSources
from docsgpt.api.user.sources.routes import PaginatedSources
with app.test_request_context("/api/sources/paginated"):
from flask import request
@@ -124,7 +124,7 @@ class TestPaginatedSources:
assert response.status_code == 401
def test_returns_pagination_shape(self, app, pg_conn):
from application.api.user.sources.routes import PaginatedSources
from docsgpt.api.user.sources.routes import PaginatedSources
user = "u-pag"
for i in range(5):
@@ -144,7 +144,7 @@ class TestPaginatedSources:
assert len(data["paginated"]) == 2
def test_search_filter(self, app, pg_conn):
from application.api.user.sources.routes import PaginatedSources
from docsgpt.api.user.sources.routes import PaginatedSources
user = "u-search"
_seed_source(pg_conn, user, name="Alpha doc")
@@ -163,7 +163,7 @@ class TestPaginatedSources:
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
from docsgpt.api.user.sources.routes import PaginatedSources
user = "u-multi-page"
expected = {f"doc-{i}" for i in range(7)}
@@ -184,7 +184,7 @@ class TestPaginatedSources:
assert seen == expected
def test_out_of_range_page_returns_empty_window(self, app, pg_conn):
from application.api.user.sources.routes import PaginatedSources
from docsgpt.api.user.sources.routes import PaginatedSources
user = "u-oor"
_seed_source(pg_conn, user, name="only-one")
@@ -202,7 +202,7 @@ class TestPaginatedSources:
assert len(data["paginated"]) == 1
def test_empty_result_set_shape(self, app, pg_conn):
from application.api.user.sources.routes import PaginatedSources
from docsgpt.api.user.sources.routes import PaginatedSources
with _patch_db(pg_conn), app.test_request_context(
"/api/sources/paginated?page=1&rows=10"
@@ -219,7 +219,7 @@ class TestPaginatedSources:
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
from docsgpt.api.user.sources.routes import PaginatedSources
user = "u-sql-search"
_seed_source(pg_conn, user, name="needle in a haystack")
@@ -239,7 +239,7 @@ class TestPaginatedSources:
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
from docsgpt.api.user.sources.routes import PaginatedSources
user = "u-shape"
_seed_source(pg_conn, user, name="shape-doc")
@@ -265,7 +265,7 @@ class TestPaginatedSources:
"""A source whose ingest the reconciler escalated to 'stalled'
surfaces ingestStatus='failed' so the UI can badge it.
"""
from application.api.user.sources.routes import PaginatedSources
from docsgpt.api.user.sources.routes import PaginatedSources
user = "u-ingest-status"
src = _seed_source(pg_conn, user, name="stalled-doc", type="file")
@@ -293,7 +293,7 @@ class TestPaginatedSources:
class TestDeleteOldIndexes:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
from docsgpt.api.user.sources.routes import DeleteOldIndexes
with app.test_request_context("/api/delete_old?source_id=x"):
from flask import request
@@ -302,7 +302,7 @@ class TestDeleteOldIndexes:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
from docsgpt.api.user.sources.routes import DeleteOldIndexes
with app.test_request_context("/api/delete_old"):
from flask import request
@@ -311,7 +311,7 @@ class TestDeleteOldIndexes:
assert response.status_code == 400
def test_returns_404_missing_source(self, app, pg_conn):
from application.api.user.sources.routes import DeleteOldIndexes
from docsgpt.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"
@@ -322,7 +322,7 @@ class TestDeleteOldIndexes:
assert response.status_code == 404
def test_deletes_non_faiss_source(self, app, pg_conn):
from application.api.user.sources.routes import DeleteOldIndexes
from docsgpt.api.user.sources.routes import DeleteOldIndexes
user = "u-del-src"
src = _seed_source(pg_conn, user, name="remove-me")
@@ -332,13 +332,13 @@ class TestDeleteOldIndexes:
fake_vs = MagicMock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.settings.VECTOR_STORE",
"docsgpt.api.user.sources.routes.settings.VECTOR_STORE",
"milvus",
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
"docsgpt.api.user.sources.routes.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.routes.VectorCreator.create_vectorstore",
"docsgpt.api.user.sources.routes.VectorCreator.create_vectorstore",
return_value=fake_vs,
), app.test_request_context(
f"/api/delete_old?source_id={src['id']}"
@@ -350,7 +350,7 @@ class TestDeleteOldIndexes:
fake_vs.delete_index.assert_called_once()
def test_deletes_faiss_source(self, app, pg_conn):
from application.api.user.sources.routes import DeleteOldIndexes
from docsgpt.api.user.sources.routes import DeleteOldIndexes
user = "u-faiss-del"
src = _seed_source(
@@ -362,10 +362,10 @@ class TestDeleteOldIndexes:
fake_storage.is_directory.return_value = False
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.settings.VECTOR_STORE",
"docsgpt.api.user.sources.routes.settings.VECTOR_STORE",
"faiss",
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
"docsgpt.api.user.sources.routes.StorageCreator.get_storage",
return_value=fake_storage,
), app.test_request_context(
f"/api/delete_old?source_id={src['id']}"
@@ -377,7 +377,7 @@ class TestDeleteOldIndexes:
assert fake_storage.delete_file.call_count >= 2
def test_delete_ignores_missing_file_error(self, app, pg_conn):
from application.api.user.sources.routes import DeleteOldIndexes
from docsgpt.api.user.sources.routes import DeleteOldIndexes
user = "u-nofile"
src = _seed_source(
@@ -390,13 +390,13 @@ class TestDeleteOldIndexes:
fake_storage.delete_file.side_effect = FileNotFoundError("gone")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.settings.VECTOR_STORE",
"docsgpt.api.user.sources.routes.settings.VECTOR_STORE",
"milvus",
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
"docsgpt.api.user.sources.routes.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.routes.VectorCreator.create_vectorstore",
"docsgpt.api.user.sources.routes.VectorCreator.create_vectorstore",
return_value=MagicMock(),
), app.test_request_context(
f"/api/delete_old?source_id={src['id']}"
@@ -409,7 +409,7 @@ class TestDeleteOldIndexes:
class TestRedirectToSources:
def test_redirects(self, app):
from application.api.user.sources.routes import RedirectToSources
from docsgpt.api.user.sources.routes import RedirectToSources
with app.test_request_context("/api/combine"):
response = RedirectToSources().get()
@@ -418,7 +418,7 @@ class TestRedirectToSources:
class TestManageSync:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import ManageSync
from docsgpt.api.user.sources.routes import ManageSync
with app.test_request_context(
"/api/manage_sync", method="POST",
@@ -430,7 +430,7 @@ class TestManageSync:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.sources.routes import ManageSync
from docsgpt.api.user.sources.routes import ManageSync
with app.test_request_context(
"/api/manage_sync", method="POST", json={"source_id": "x"}
@@ -441,7 +441,7 @@ class TestManageSync:
assert response.status_code == 400
def test_returns_400_invalid_frequency(self, app):
from application.api.user.sources.routes import ManageSync
from docsgpt.api.user.sources.routes import ManageSync
with app.test_request_context(
"/api/manage_sync",
@@ -454,7 +454,7 @@ class TestManageSync:
assert response.status_code == 400
def test_returns_404_missing_source(self, app, pg_conn):
from application.api.user.sources.routes import ManageSync
from docsgpt.api.user.sources.routes import ManageSync
with _patch_db(pg_conn), app.test_request_context(
"/api/manage_sync",
@@ -470,8 +470,8 @@ class TestManageSync:
assert response.status_code == 404
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
from docsgpt.api.user.sources.routes import ManageSync
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-sync"
src = _seed_source(pg_conn, user, name="sync-src")
@@ -491,7 +491,7 @@ class TestManageSync:
class TestSyncSource:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
with app.test_request_context(
"/api/sync_source", method="POST", json={"source_id": "x"}
@@ -502,7 +502,7 @@ class TestSyncSource:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
with app.test_request_context(
"/api/sync_source", method="POST", json={}
@@ -515,7 +515,7 @@ class TestSyncSource:
def test_returns_403_inaccessible_source(self, app, pg_conn):
# No ownership and no team editor grant resolves to None, which the
# owner-or-editor gate answers as 403 "Source not accessible".
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
with _patch_db(pg_conn), app.test_request_context(
"/api/sync_source",
@@ -528,7 +528,7 @@ class TestSyncSource:
assert response.status_code == 403
def test_returns_400_for_connector_type(self, app, pg_conn):
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
user = "u-conn"
src = _seed_source(
@@ -547,7 +547,7 @@ class TestSyncSource:
assert response.status_code == 400
def test_returns_400_for_non_syncable(self, app, pg_conn):
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
user = "u-nosync"
src = _seed_source(pg_conn, user, name="nosync", type="file")
@@ -563,7 +563,7 @@ class TestSyncSource:
assert response.status_code == 400
def test_triggers_sync_task(self, app, pg_conn):
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
user = "u-trigger"
src = _seed_source(
@@ -573,7 +573,7 @@ class TestSyncSource:
fake_task = MagicMock(id="task-123")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.sync_source.delay",
"docsgpt.api.user.sources.routes.sync_source.delay",
return_value=fake_task,
), app.test_request_context(
"/api/sync_source",
@@ -588,7 +588,7 @@ class TestSyncSource:
def test_normalizes_dict_remote_data_before_dispatch(self, app, pg_conn):
"""The route must hand the sync task the normalized URL string."""
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
user = "u-normalize"
src = _seed_source(
@@ -600,7 +600,7 @@ class TestSyncSource:
fake_task = MagicMock(id="task-norm")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.sync_source.delay",
"docsgpt.api.user.sources.routes.sync_source.delay",
return_value=fake_task,
) as mock_delay, app.test_request_context(
"/api/sync_source",
@@ -616,7 +616,7 @@ class TestSyncSource:
assert mock_delay.call_args.kwargs["loader"] == "crawler"
def test_sync_task_raises_returns_400(self, app, pg_conn):
from application.api.user.sources.routes import SyncSource
from docsgpt.api.user.sources.routes import SyncSource
user = "u-fail"
src = _seed_source(
@@ -625,7 +625,7 @@ class TestSyncSource:
)
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.sync_source.delay",
"docsgpt.api.user.sources.routes.sync_source.delay",
side_effect=RuntimeError("boom"),
), app.test_request_context(
"/api/sync_source",
@@ -640,7 +640,7 @@ class TestSyncSource:
class TestReingestSource:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import ReingestSource
from docsgpt.api.user.sources.routes import ReingestSource
with app.test_request_context(
"/api/sources/reingest", method="POST", json={"source_id": "x"}
@@ -651,7 +651,7 @@ class TestReingestSource:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.sources.routes import ReingestSource
from docsgpt.api.user.sources.routes import ReingestSource
with app.test_request_context(
"/api/sources/reingest", method="POST", json={}
@@ -664,7 +664,7 @@ class TestReingestSource:
def test_returns_403_inaccessible_source(self, app, pg_conn):
# No ownership and no team editor grant resolves to None, which the
# owner-or-editor gate answers as 403 "Source not accessible".
from application.api.user.sources.routes import ReingestSource
from docsgpt.api.user.sources.routes import ReingestSource
with _patch_db(pg_conn), app.test_request_context(
"/api/sources/reingest",
@@ -677,14 +677,14 @@ class TestReingestSource:
assert response.status_code == 403
def test_triggers_reingest_task(self, app, pg_conn):
from application.api.user.sources.routes import ReingestSource
from docsgpt.api.user.sources.routes import ReingestSource
user = "u-reingest"
src = _seed_source(pg_conn, user, name="stalled-src", type="file")
fake_task = MagicMock(id="reingest-task-1")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reingest_source_task.delay",
"docsgpt.api.user.sources.routes.reingest_source_task.delay",
return_value=fake_task,
) as mock_delay, app.test_request_context(
"/api/sources/reingest",
@@ -712,14 +712,14 @@ class TestReingestSource:
"""
import uuid
from application.api.user.sources.routes import ReingestSource
from application.storage.db.repositories.team_members import (
from docsgpt.api.user.sources.routes import ReingestSource
from docsgpt.storage.db.repositories.team_members import (
TeamMembersRepository,
)
from application.storage.db.repositories.team_resource_grants import (
from docsgpt.storage.db.repositories.team_resource_grants import (
TeamResourceGrantsRepository,
)
from application.storage.db.repositories.teams import TeamsRepository
from docsgpt.storage.db.repositories.teams import TeamsRepository
owner = "alice-reingest"
editor = "bob-reingest"
@@ -738,7 +738,7 @@ class TestReingestSource:
fake_task = MagicMock(id="reingest-task-editor")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reingest_source_task.delay",
"docsgpt.api.user.sources.routes.reingest_source_task.delay",
return_value=fake_task,
) as mock_delay, app.test_request_context(
"/api/sources/reingest",
@@ -761,7 +761,7 @@ class TestReingestSource:
"""Reingest drops the stale chunk-progress row so the sources
list stops deriving a 'failed' ingest status for the source.
"""
from application.api.user.sources.routes import ReingestSource
from docsgpt.api.user.sources.routes import ReingestSource
user = "u-reingest-clear"
src = _seed_source(pg_conn, user, name="stalled-doc", type="file")
@@ -780,7 +780,7 @@ class TestReingestSource:
fake_task = MagicMock(id="reingest-task-2")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reingest_source_task.delay",
"docsgpt.api.user.sources.routes.reingest_source_task.delay",
return_value=fake_task,
), app.test_request_context(
"/api/sources/reingest",
@@ -802,13 +802,13 @@ class TestReingestSource:
assert remaining == 0
def test_reingest_task_raises_returns_400(self, app, pg_conn):
from application.api.user.sources.routes import ReingestSource
from docsgpt.api.user.sources.routes import ReingestSource
user = "u-reingest-fail"
src = _seed_source(pg_conn, user, name="fail-src", type="file")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reingest_source_task.delay",
"docsgpt.api.user.sources.routes.reingest_source_task.delay",
side_effect=RuntimeError("boom"),
), app.test_request_context(
"/api/sources/reingest",
@@ -823,7 +823,7 @@ class TestReingestSource:
class TestDirectoryStructure:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import DirectoryStructure
from docsgpt.api.user.sources.routes import DirectoryStructure
with app.test_request_context("/api/directory_structure?id=x"):
from flask import request
@@ -832,7 +832,7 @@ class TestDirectoryStructure:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.sources.routes import DirectoryStructure
from docsgpt.api.user.sources.routes import DirectoryStructure
with app.test_request_context("/api/directory_structure"):
from flask import request
@@ -841,7 +841,7 @@ class TestDirectoryStructure:
assert response.status_code == 400
def test_returns_404_missing_doc(self, app, pg_conn):
from application.api.user.sources.routes import DirectoryStructure
from docsgpt.api.user.sources.routes import DirectoryStructure
with _patch_db(pg_conn), app.test_request_context(
"/api/directory_structure?id=00000000-0000-0000-0000-000000000000"
@@ -852,7 +852,7 @@ class TestDirectoryStructure:
assert response.status_code == 404
def test_returns_structure(self, app, pg_conn):
from application.api.user.sources.routes import DirectoryStructure
from docsgpt.api.user.sources.routes import DirectoryStructure
user = "u-dir"
src = _seed_source(
@@ -876,7 +876,7 @@ class TestDirectoryStructure:
class TestSourceConfigResource:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import SourceConfigResource
from docsgpt.api.user.sources.routes import SourceConfigResource
with app.test_request_context(
"/api/sources/x/config", method="PATCH", json={}
@@ -888,7 +888,7 @@ class TestSourceConfigResource:
def test_invalid_config_rejected(self, app, pg_conn):
# Strict-on-write: an unknown field fails validation → 400.
from application.api.user.sources.routes import SourceConfigResource
from docsgpt.api.user.sources.routes import SourceConfigResource
user = "u-cfg-bad"
src = _seed_source(pg_conn, user, name="cfg-src", type="file")
@@ -904,8 +904,8 @@ class TestSourceConfigResource:
assert response.status_code == 400
def test_owner_updates_retrieval_no_reingest(self, app, pg_conn):
from application.api.user.sources.routes import SourceConfigResource
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import SourceConfigResource
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-cfg-owner"
src = _seed_source(pg_conn, user, name="cfg-live", type="file")
@@ -927,7 +927,7 @@ class TestSourceConfigResource:
assert got["config"]["retrieval"]["rephrase_query"] is False
def test_chunking_change_requires_reingest(self, app, pg_conn):
from application.api.user.sources.routes import SourceConfigResource
from docsgpt.api.user.sources.routes import SourceConfigResource
user = "u-cfg-chunk"
src = _seed_source(pg_conn, user, name="cfg-chunk", type="file")
@@ -948,15 +948,15 @@ class TestSourceConfigResource:
# A team VIEWER (not editor) cannot edit config → 403.
import uuid
from application.api.user.sources.routes import SourceConfigResource
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.team_members import (
from docsgpt.api.user.sources.routes import SourceConfigResource
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.team_members import (
TeamMembersRepository,
)
from application.storage.db.repositories.team_resource_grants import (
from docsgpt.storage.db.repositories.team_resource_grants import (
TeamResourceGrantsRepository,
)
from application.storage.db.repositories.teams import TeamsRepository
from docsgpt.storage.db.repositories.teams import TeamsRepository
owner = "alice-cfg"
viewer = "bob-cfg-viewer"
@@ -990,15 +990,15 @@ class TestSourceConfigResource:
# A team EDITOR can edit; the write lands under the OWNER's id.
import uuid
from application.api.user.sources.routes import SourceConfigResource
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.team_members import (
from docsgpt.api.user.sources.routes import SourceConfigResource
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.team_members import (
TeamMembersRepository,
)
from application.storage.db.repositories.team_resource_grants import (
from docsgpt.storage.db.repositories.team_resource_grants import (
TeamResourceGrantsRepository,
)
from application.storage.db.repositories.teams import TeamsRepository
from docsgpt.storage.db.repositories.teams import TeamsRepository
owner = "alice-cfg-edit"
editor = "bob-cfg-editor"
@@ -1032,8 +1032,8 @@ class TestSourceConfigResource:
def test_kind_flip_to_wiki_rejected(self, app, pg_conn):
# Flipping kind to wiki must route through /wiki/convert, not config.
from application.api.user.sources.routes import SourceConfigResource
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import SourceConfigResource
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-cfg-wiki-flip"
src = _seed_source(pg_conn, user, name="cfg-wiki", type="file")
@@ -1050,15 +1050,15 @@ class TestSourceConfigResource:
assert response.status_code == 400
# The kind must NOT have silently flipped.
from application.storage.db.source_config import SourceConfig
from docsgpt.storage.db.source_config import SourceConfig
got = SourcesRepository(pg_conn).get_any(sid, user)
assert SourceConfig.parse(got.get("config")).kind != "wiki"
def test_other_edits_work_on_wiki_source(self, app, pg_conn):
# A wiki source can still edit retrieval (kind stays wiki, no reject).
from application.api.user.sources.routes import SourceConfigResource
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import SourceConfigResource
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-cfg-wiki-edit"
src = _seed_source(
@@ -1083,9 +1083,9 @@ class TestSourceConfigResource:
def test_partial_edit_preserves_wiki_kind(self, app, pg_conn):
# A partial edit that OMITS kind must not demote a wiki to classic
# (SourceConfig.kind defaults to "classic" on a full-replace write).
from application.api.user.sources.routes import SourceConfigResource
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.source_config import SourceConfig
from docsgpt.api.user.sources.routes import SourceConfigResource
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.source_config import SourceConfig
user = "u-cfg-wiki-partial"
src = _seed_source(
@@ -1111,9 +1111,9 @@ class TestSourceConfigResource:
def test_explicit_kind_demotion_from_wiki_rejected(self, app, pg_conn):
# Demoting wiki -> classic via config is rejected; use /wiki/convert.
from application.api.user.sources.routes import SourceConfigResource
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.source_config import SourceConfig
from docsgpt.api.user.sources.routes import SourceConfigResource
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.source_config import SourceConfig
user = "u-cfg-wiki-demote"
src = _seed_source(
+95 -95
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/sources/upload.py."""
"""Tests for docsgpt/api/user/sources/upload.py."""
import io
import json
@@ -21,21 +21,21 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.sources.upload.db_session", _yield
"docsgpt.api.user.sources.upload.db_session", _yield
), patch(
"application.api.user.sources.upload.db_readonly", _yield
"docsgpt.api.user.sources.upload.db_readonly", _yield
):
yield
def _seed_source(pg_conn, user="u", name="src", **kw):
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
return SourcesRepository(pg_conn).create(name, user_id=user, **kw)
class TestEnforceAudioPathSizeLimit:
def test_noop_for_non_audio(self, tmp_path):
from application.api.user.sources.upload import (
from docsgpt.api.user.sources.upload import (
_enforce_audio_path_size_limit,
)
p = tmp_path / "doc.txt"
@@ -43,15 +43,15 @@ class TestEnforceAudioPathSizeLimit:
_enforce_audio_path_size_limit(str(p), "doc.txt")
def test_raises_for_large_audio(self, tmp_path):
from application.api.user.sources.upload import (
from docsgpt.api.user.sources.upload import (
_enforce_audio_path_size_limit,
)
from application.stt.upload_limits import AudioFileTooLargeError
from docsgpt.stt.upload_limits import AudioFileTooLargeError
p = tmp_path / "audio.mp3"
p.write_bytes(b"x" * 100)
with patch(
"application.api.user.sources.upload.enforce_audio_file_size_limit",
"docsgpt.api.user.sources.upload.enforce_audio_file_size_limit",
side_effect=AudioFileTooLargeError("too large"),
):
with pytest.raises(AudioFileTooLargeError):
@@ -60,7 +60,7 @@ class TestEnforceAudioPathSizeLimit:
class TestUploadFile:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
with app.test_request_context("/api/upload", method="POST"):
from flask import request
@@ -69,7 +69,7 @@ class TestUploadFile:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
with app.test_request_context(
"/api/upload", method="POST",
@@ -82,7 +82,7 @@ class TestUploadFile:
assert response.status_code == 400
def test_returns_400_empty_filenames(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
with app.test_request_context(
"/api/upload", method="POST",
@@ -98,16 +98,16 @@ class TestUploadFile:
assert response.status_code == 400
def test_uploads_single_file_successfully(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
fake_task = MagicMock(id="task-1")
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/upload", method="POST",
@@ -125,17 +125,17 @@ class TestUploadFile:
assert response.json["task_id"] == "task-1"
def test_rejects_file_over_configured_upload_limit(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
with patch(
"application.api.user.sources.upload.settings.UPLOAD_MAX_FILE_BYTES",
"docsgpt.api.user.sources.upload.settings.UPLOAD_MAX_FILE_BYTES",
4,
), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
) as apply_async, app.test_request_context(
"/api/upload", method="POST",
data={
@@ -153,13 +153,13 @@ class TestUploadFile:
apply_async.assert_not_called()
def test_storage_error_returns_400(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
fake_storage.save_file.side_effect = RuntimeError("boom")
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), app.test_request_context(
"/api/upload", method="POST",
@@ -175,7 +175,7 @@ class TestUploadFile:
assert response.status_code == 400
def test_uploads_zip_extracts_files(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
import zipfile
# Build an in-memory zip containing 2 files
@@ -189,10 +189,10 @@ class TestUploadFile:
fake_task = MagicMock(id="task-zip")
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/upload", method="POST",
@@ -210,7 +210,7 @@ class TestUploadFile:
assert fake_storage.save_file.call_count >= 2
def test_rejects_zip_with_excessive_expansion_ratio(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
import zipfile
zip_buffer = io.BytesIO()
@@ -220,10 +220,10 @@ class TestUploadFile:
fake_storage = MagicMock()
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
) as apply_async, app.test_request_context(
"/api/upload", method="POST",
data={
@@ -241,7 +241,7 @@ class TestUploadFile:
apply_async.assert_not_called()
def test_accepts_highly_compressible_csv(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
import zipfile
zip_buffer = io.BytesIO()
@@ -251,10 +251,10 @@ class TestUploadFile:
fake_storage = MagicMock()
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
return_value=MagicMock(id="csv-task"),
), app.test_request_context(
"/api/upload",
@@ -274,7 +274,7 @@ class TestUploadFile:
assert fake_storage.save_file.call_count == 1
def test_archive_error_names_file_and_escapes_log_controls(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
import zipfile
zip_buffer = io.BytesIO()
@@ -283,7 +283,7 @@ class TestUploadFile:
zip_buffer.seek(0)
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), patch.object(app.logger, "warning") as warning, app.test_request_context(
"/api/upload",
@@ -310,7 +310,7 @@ class TestUploadFile:
assert "\n" not in logged_values
def test_expands_nested_zip_before_storage(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
import zipfile
inner = io.BytesIO()
@@ -323,10 +323,10 @@ class TestUploadFile:
fake_storage = MagicMock()
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
return_value=MagicMock(id="nested-task"),
), app.test_request_context(
"/api/upload",
@@ -348,7 +348,7 @@ class TestUploadFile:
assert not any(path.endswith(".zip") for path in saved_paths)
def test_office_format_zip_saved_as_is(self, app):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
import zipfile
# .docx is technically a zip but should be saved as-is
@@ -360,10 +360,10 @@ class TestUploadFile:
fake_storage = MagicMock()
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
return_value=MagicMock(id="t"),
), app.test_request_context(
"/api/upload", method="POST",
@@ -381,16 +381,16 @@ class TestUploadFile:
assert fake_storage.save_file.call_count == 1
def test_audio_too_large_returns_413(self, app):
from application.api.user.sources.upload import UploadFile
from application.stt.upload_limits import AudioFileTooLargeError
from docsgpt.api.user.sources.upload import UploadFile
from docsgpt.stt.upload_limits import AudioFileTooLargeError
fake_storage = MagicMock()
with patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload._enforce_audio_path_size_limit",
"docsgpt.api.user.sources.upload._enforce_audio_path_size_limit",
side_effect=AudioFileTooLargeError("too large"),
), app.test_request_context(
"/api/upload", method="POST",
@@ -408,7 +408,7 @@ class TestUploadFile:
class TestUploadRemote:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
with app.test_request_context(
"/api/remote", method="POST",
@@ -421,7 +421,7 @@ class TestUploadRemote:
assert response.status_code == 401
def test_returns_missing_fields(self, app):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
with app.test_request_context(
"/api/remote", method="POST",
@@ -436,11 +436,11 @@ class TestUploadRemote:
assert response.status_code == 400
def test_uploads_github_remote_success(self, app):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
fake_task = MagicMock(id="remote-task-1")
with patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/remote", method="POST",
@@ -457,11 +457,11 @@ class TestUploadRemote:
assert response.json["task_id"] == "remote-task-1"
def test_uploads_url_source(self, app):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
fake_task = MagicMock(id="url-task")
with patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/remote", method="POST",
@@ -477,11 +477,11 @@ class TestUploadRemote:
assert response.status_code == 200
def test_uploads_reddit_source(self, app):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
fake_task = MagicMock(id="reddit-task")
with patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/remote", method="POST",
@@ -497,10 +497,10 @@ class TestUploadRemote:
assert response.status_code == 200
def test_upload_exception_returns_400(self, app):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
with patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
side_effect=RuntimeError("boom"),
), app.test_request_context(
"/api/remote", method="POST",
@@ -518,7 +518,7 @@ class TestUploadRemote:
class TestManageSourceFiles:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
with app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -531,7 +531,7 @@ class TestManageSourceFiles:
assert response.status_code == 401
def test_returns_400_missing_required(self, app):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
with app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -544,7 +544,7 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_returns_400_invalid_operation(self, app):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
with app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -557,7 +557,7 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_returns_404_source_not_found(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
with _patch_db(pg_conn), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -573,13 +573,13 @@ class TestManageSourceFiles:
assert response.status_code == 404
def test_rejects_bad_parent_dir(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-bad-parent"
src = _seed_source(pg_conn, user=user, file_path="/data")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -596,13 +596,13 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_add_no_files_returns_400(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-add-nofile"
src = _seed_source(pg_conn, user=user, file_path="/data")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -615,18 +615,18 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_add_rejects_file_over_upload_limit(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-add-large"
src = _seed_source(pg_conn, user=user, file_path="/data/src")
fake_storage = MagicMock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.upload_limits.settings.UPLOAD_MAX_FILE_BYTES", 4
"docsgpt.upload_limits.settings.UPLOAD_MAX_FILE_BYTES", 4
), patch(
"application.api.user.tasks.reingest_source_task.apply_async"
"docsgpt.api.user.tasks.reingest_source_task.apply_async"
) as apply_async, app.test_request_context(
"/api/manage_source_files", method="POST",
data={
@@ -645,7 +645,7 @@ class TestManageSourceFiles:
apply_async.assert_not_called()
def test_add_files_success(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-add-ok"
src = _seed_source(pg_conn, user=user, file_path="/data/src")
@@ -653,10 +653,10 @@ class TestManageSourceFiles:
fake_storage = MagicMock()
fake_task = MagicMock(id="reingest-1")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -675,13 +675,13 @@ class TestManageSourceFiles:
assert "new.txt" in response.json["added_files"]
def test_remove_missing_file_paths_returns_400(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rm-nolist"
src = _seed_source(pg_conn, user=user, file_path="/data")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -694,13 +694,13 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_remove_invalid_json_file_paths(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rm-bad"
src = _seed_source(pg_conn, user=user, file_path="/data")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -717,13 +717,13 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_remove_rejects_path_traversal(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rm-trav"
src = _seed_source(pg_conn, user=user, file_path="/data")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -740,7 +740,7 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_remove_files_success(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rm-ok"
src = _seed_source(
@@ -753,10 +753,10 @@ class TestManageSourceFiles:
fake_storage.file_exists.return_value = True
fake_task = MagicMock(id="reingest-rm")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -774,13 +774,13 @@ class TestManageSourceFiles:
assert "a.txt" in response.json["removed_files"]
def test_remove_directory_missing_path(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rmdir-missing"
src = _seed_source(pg_conn, user=user, file_path="/data")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -796,13 +796,13 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_remove_directory_rejects_bad_path(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rmdir-bad"
src = _seed_source(pg_conn, user=user, file_path="/data")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=MagicMock(),
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -819,7 +819,7 @@ class TestManageSourceFiles:
assert response.status_code == 400
def test_remove_directory_404_when_not_directory(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rmdir-notdir"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -828,7 +828,7 @@ class TestManageSourceFiles:
fake_storage.is_directory.return_value = False
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -845,7 +845,7 @@ class TestManageSourceFiles:
assert response.status_code == 404
def test_remove_directory_success(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rmdir-ok"
src = _seed_source(
@@ -859,10 +859,10 @@ class TestManageSourceFiles:
fake_task = MagicMock(id="reingest-dir")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -880,7 +880,7 @@ class TestManageSourceFiles:
assert response.json["removed_directory"] == "sub"
def test_remove_directory_storage_failure_returns_500(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "u-rmdir-fail"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -890,7 +890,7 @@ class TestManageSourceFiles:
fake_storage.remove_directory.return_value = False
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -909,14 +909,14 @@ class TestManageSourceFiles:
class TestTaskStatus:
def test_returns_400_missing_task_id(self, app):
from application.api.user.sources.upload import TaskStatus
from docsgpt.api.user.sources.upload import TaskStatus
with app.test_request_context("/api/task_status"):
response = TaskStatus().get()
assert response.status_code == 400
def test_returns_task_status(self, app):
from application.api.user.sources.upload import TaskStatus
from docsgpt.api.user.sources.upload import TaskStatus
fake_task = MagicMock()
fake_task.status = "SUCCESS"
@@ -926,14 +926,14 @@ class TestTaskStatus:
fake_celery.AsyncResult.return_value = fake_task
with patch(
"application.celery_init.celery", fake_celery
"docsgpt.celery_init.celery", fake_celery
), app.test_request_context("/api/task_status?task_id=t-123"):
response = TaskStatus().get()
assert response.status_code == 200
assert response.json["status"] == "SUCCESS"
def test_pending_without_workers_returns_503(self, app):
from application.api.user.sources.upload import TaskStatus
from docsgpt.api.user.sources.upload import TaskStatus
fake_task = MagicMock()
fake_task.status = "PENDING"
@@ -947,25 +947,25 @@ class TestTaskStatus:
fake_celery.control.inspect.return_value = fake_inspect
with patch(
"application.celery_init.celery", fake_celery
"docsgpt.celery_init.celery", fake_celery
), app.test_request_context("/api/task_status?task_id=t-999"):
response = TaskStatus().get()
assert response.status_code == 503
def test_exception_returns_400(self, app):
from application.api.user.sources.upload import TaskStatus
from docsgpt.api.user.sources.upload import TaskStatus
fake_celery = MagicMock()
fake_celery.AsyncResult.side_effect = RuntimeError("boom")
with patch(
"application.celery_init.celery", fake_celery
"docsgpt.celery_init.celery", fake_celery
), app.test_request_context("/api/task_status?task_id=t-err"):
response = TaskStatus().get()
assert response.status_code == 400
def test_non_serializable_info_gets_stringified(self, app):
from application.api.user.sources.upload import TaskStatus
from docsgpt.api.user.sources.upload import TaskStatus
class WeirdObj:
def __str__(self):
@@ -979,7 +979,7 @@ class TestTaskStatus:
fake_celery.AsyncResult.return_value = fake_task
with patch(
"application.celery_init.celery", fake_celery
"docsgpt.celery_init.celery", fake_celery
), app.test_request_context("/api/task_status?task_id=t-weird"):
response = TaskStatus().get()
assert response.status_code == 200
@@ -21,9 +21,9 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.sources.upload.db_session", _yield
"docsgpt.api.user.sources.upload.db_session", _yield
), patch(
"application.api.user.sources.upload.db_readonly", _yield
"docsgpt.api.user.sources.upload.db_readonly", _yield
):
yield
@@ -38,16 +38,16 @@ def _apply_async_mock():
class TestUploadIdempotency:
def test_no_header_enqueues_normally(self, app, pg_conn):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
), app.test_request_context(
"/api/upload", method="POST",
@@ -68,16 +68,16 @@ class TestUploadIdempotency:
def test_header_first_post_records_row(self, app, pg_conn):
from sqlalchemy import text
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
), app.test_request_context(
"/api/upload", method="POST",
@@ -114,16 +114,16 @@ class TestUploadIdempotency:
"""The Celery task body needs the key so ``with_idempotency`` can
record terminal status and ``_derive_source_id`` can pick it up.
"""
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
), app.test_request_context(
"/api/upload", method="POST",
@@ -145,16 +145,16 @@ class TestUploadIdempotency:
)
def test_same_header_second_post_returns_cached(self, app, pg_conn):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
):
with app.test_request_context(
@@ -196,17 +196,17 @@ class TestUploadIdempotency:
"""
from sqlalchemy import text as sql_text
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
def _fire(user):
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
), app.test_request_context(
"/api/upload", method="POST",
@@ -248,7 +248,7 @@ class TestUploadIdempotency:
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
@@ -285,16 +285,16 @@ class TestUploadIdempotency:
# function instead of the mock. Set up patches once, share
# across threads.
with patch(
"application.api.user.sources.upload.db_session",
"docsgpt.api.user.sources.upload.db_session",
_engine_session,
), patch(
"application.api.user.sources.upload.db_readonly",
"docsgpt.api.user.sources.upload.db_readonly",
_engine_readonly,
), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
), ThreadPoolExecutor(max_workers=8) as ex:
responses = list(ex.map(fire, range(8)))
@@ -309,16 +309,16 @@ class TestUploadIdempotency:
def test_empty_header_treated_as_absent(self, app, pg_conn):
from sqlalchemy import text
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
), app.test_request_context(
"/api/upload", method="POST",
@@ -340,16 +340,16 @@ class TestUploadIdempotency:
assert count == 0
def test_oversized_header_rejected_with_400(self, app, pg_conn):
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
oversized = "x" * 257
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
) as mock_apply, app.test_request_context(
"/api/upload", method="POST",
data={
@@ -372,16 +372,16 @@ class TestUploadIdempotency:
"""
from sqlalchemy import text
from application.api.user.sources.upload import UploadFile
from docsgpt.api.user.sources.upload import UploadFile
fake_storage = MagicMock()
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.sources.upload.ingest.apply_async",
"docsgpt.api.user.sources.upload.ingest.apply_async",
apply_mock,
):
with app.test_request_context(
@@ -431,11 +431,11 @@ class TestUploadIdempotency:
class TestRemoteIdempotency:
def test_no_header_enqueues_normally(self, app, pg_conn):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
apply_mock,
), app.test_request_context(
"/api/remote", method="POST",
@@ -454,11 +454,11 @@ class TestRemoteIdempotency:
def test_header_first_post_records_row(self, app, pg_conn):
from sqlalchemy import text
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
apply_mock,
), app.test_request_context(
"/api/remote", method="POST",
@@ -485,11 +485,11 @@ class TestRemoteIdempotency:
assert row[1] == "ingest_remote"
def test_same_header_second_post_returns_cached(self, app, pg_conn):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
apply_mock,
):
with app.test_request_context(
@@ -523,11 +523,11 @@ class TestRemoteIdempotency:
assert apply_mock.call_count == 1
def test_oversized_header_rejected_with_400(self, app, pg_conn):
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
oversized = "x" * 257
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
) as mock_apply, app.test_request_context(
"/api/remote", method="POST",
data={
@@ -554,11 +554,11 @@ class TestRemoteIdempotency:
random uuid, breaking push correlation for the default upload
flow.
"""
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.ingest_remote.apply_async",
"docsgpt.api.user.sources.upload.ingest_remote.apply_async",
apply_mock,
), app.test_request_context(
"/api/remote", method="POST",
@@ -584,12 +584,12 @@ class TestRemoteIdempotency:
"""Same regression as above for the connector branch
(``ingest_connector_task``). The connector path took the
no-key gap independently of the plain remote path."""
from application.api.user.sources.upload import UploadRemote
from docsgpt.api.user.sources.upload import UploadRemote
apply_mock = _apply_async_mock()
# Pick any registered connector — the route only branches on
# ``ConnectorCreator.get_supported_connectors()``.
from application.parser.connectors.connector_creator import (
from docsgpt.parser.connectors.connector_creator import (
ConnectorCreator,
)
supported = ConnectorCreator.get_supported_connectors()
@@ -598,7 +598,7 @@ class TestRemoteIdempotency:
connector_source = next(iter(supported))
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.ingest_connector_task.apply_async",
"docsgpt.api.user.sources.upload.ingest_connector_task.apply_async",
apply_mock,
), app.test_request_context(
"/api/remote", method="POST",
@@ -623,7 +623,7 @@ class TestRemoteIdempotency:
def _seed_source(pg_conn, user="u", **kw):
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
return SourcesRepository(pg_conn).create("manage-src", user_id=user, **kw)
@@ -652,7 +652,7 @@ class TestManageSourceFilesIdempotency:
def test_no_header_enqueues_normally_no_claim_row(self, app, pg_conn):
from sqlalchemy import text
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-noh"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -661,10 +661,10 @@ class TestManageSourceFilesIdempotency:
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
), self._add_request(app, src["id"], user):
from flask import request
@@ -683,7 +683,7 @@ class TestManageSourceFilesIdempotency:
def test_header_records_dedup_row_with_predetermined_id(self, app, pg_conn):
from sqlalchemy import text
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-rec"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -692,10 +692,10 @@ class TestManageSourceFilesIdempotency:
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
), self._add_request(app, src["id"], user, key="mgr-key-1"):
from flask import request
@@ -721,7 +721,7 @@ class TestManageSourceFilesIdempotency:
assert row[2] == "pending"
def test_same_key_second_post_returns_cached(self, app, pg_conn):
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-rep"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -730,10 +730,10 @@ class TestManageSourceFilesIdempotency:
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
):
with self._add_request(app, src["id"], user, key="mgr-rep"):
@@ -776,7 +776,7 @@ class TestManageSourceFilesIdempotency:
SSE correlation silently fails on every idempotent retry and
the user never sees the directory refresh.
"""
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-rmrep"
src = _seed_source(
@@ -804,10 +804,10 @@ class TestManageSourceFilesIdempotency:
)
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
):
with _do_remove():
@@ -834,7 +834,7 @@ class TestManageSourceFilesIdempotency:
"""Same regression as the ``remove`` test, for the
``remove_directory`` branch.
"""
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-rmdir-rep"
src = _seed_source(
@@ -863,10 +863,10 @@ class TestManageSourceFilesIdempotency:
)
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
):
with _do_remove_dir():
@@ -890,8 +890,8 @@ class TestManageSourceFilesIdempotency:
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
from application.api.user.sources.upload import ManageSourceFiles
from application.storage.db.repositories.sources import (
from docsgpt.api.user.sources.upload import ManageSourceFiles
from docsgpt.storage.db.repositories.sources import (
SourcesRepository,
)
@@ -926,16 +926,16 @@ class TestManageSourceFilesIdempotency:
# module-attribute patches once before fanning out so every
# thread sees the mock instead of racing on save/restore.
with patch(
"application.api.user.sources.upload.db_session",
"docsgpt.api.user.sources.upload.db_session",
_engine_session,
), patch(
"application.api.user.sources.upload.db_readonly",
"docsgpt.api.user.sources.upload.db_readonly",
_engine_readonly,
), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
), ThreadPoolExecutor(max_workers=8) as ex:
responses = list(ex.map(fire, range(8)))
@@ -950,7 +950,7 @@ class TestManageSourceFilesIdempotency:
"""
from sqlalchemy import text
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-rmfail"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -961,10 +961,10 @@ class TestManageSourceFilesIdempotency:
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
), app.test_request_context(
"/api/manage_source_files", method="POST",
@@ -1000,7 +1000,7 @@ class TestManageSourceFilesIdempotency:
"""
from sqlalchemy import text
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-storefail"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -1010,10 +1010,10 @@ class TestManageSourceFilesIdempotency:
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
), self._add_request(app, src["id"], user, key="mgr-storefail"):
from flask import request
@@ -1038,7 +1038,7 @@ class TestManageSourceFilesIdempotency:
"""
from sqlalchemy import text
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-brokerdown"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -1049,10 +1049,10 @@ class TestManageSourceFilesIdempotency:
raise ConnectionError("broker unreachable")
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
side_effect=_broker_down,
), self._add_request(app, src["id"], user, key="mgr-brokerdown"):
from flask import request
@@ -1075,8 +1075,8 @@ class TestManageSourceFilesIdempotency:
"""
from sqlalchemy import text
from application.api.user.sources.upload import ManageSourceFiles
from application.storage.db.repositories import sources as src_module
from docsgpt.api.user.sources.upload import ManageSourceFiles
from docsgpt.storage.db.repositories import sources as src_module
user = "alice-mgr-dbfail"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -1092,10 +1092,10 @@ class TestManageSourceFilesIdempotency:
src_module.SourcesRepository.update = _explode
try:
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
), self._add_request(app, src["id"], user, key="mgr-dbfail"):
from flask import request
@@ -1123,7 +1123,7 @@ class TestManageSourceFilesIdempotency:
"""
from sqlalchemy import text
from application.api.user.sources.upload import ManageSourceFiles
from docsgpt.api.user.sources.upload import ManageSourceFiles
user = "alice-mgr-keep"
src = _seed_source(pg_conn, user=user, file_path="/data")
@@ -1132,10 +1132,10 @@ class TestManageSourceFilesIdempotency:
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.sources.upload.StorageCreator.get_storage",
"docsgpt.api.user.sources.upload.StorageCreator.get_storage",
return_value=fake_storage,
), patch(
"application.api.user.tasks.reingest_source_task.apply_async",
"docsgpt.api.user.tasks.reingest_source_task.apply_async",
apply_mock,
), self._add_request(app, src["id"], user, key="mgr-keep"):
from flask import request
+73 -73
View File
@@ -1,4 +1,4 @@
"""Tests for the wiki source routes in application/api/user/sources/routes.py."""
"""Tests for the wiki source routes in docsgpt/api/user/sources/routes.py."""
import uuid
from contextlib import contextmanager
@@ -20,21 +20,21 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.sources.routes.db_session", _yield
"docsgpt.api.user.sources.routes.db_session", _yield
), patch(
"application.api.user.sources.routes.db_readonly", _yield
"docsgpt.api.user.sources.routes.db_readonly", _yield
):
yield
def _grant_team_access(pg_conn, owner, member, source_id, access_level):
from application.storage.db.repositories.team_members import (
from docsgpt.storage.db.repositories.team_members import (
TeamMembersRepository,
)
from application.storage.db.repositories.team_resource_grants import (
from docsgpt.storage.db.repositories.team_resource_grants import (
TeamResourceGrantsRepository,
)
from application.storage.db.repositories.teams import TeamsRepository
from docsgpt.storage.db.repositories.teams import TeamsRepository
team = TeamsRepository(pg_conn).create(
"Acme", f"acme-{uuid.uuid4().hex[:8]}", owner
@@ -50,7 +50,7 @@ def _grant_team_access(pg_conn, owner, member, source_id, access_level):
class TestCreateWikiSource:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import CreateWikiSource
from docsgpt.api.user.sources.routes import CreateWikiSource
with app.test_request_context(
"/api/sources/wiki", method="POST", json={"name": "w"}
@@ -61,7 +61,7 @@ class TestCreateWikiSource:
assert response.status_code == 401
def test_returns_400_missing_name(self, app, pg_conn):
from application.api.user.sources.routes import CreateWikiSource
from docsgpt.api.user.sources.routes import CreateWikiSource
with _patch_db(pg_conn), app.test_request_context(
"/api/sources/wiki", method="POST", json={}
@@ -72,16 +72,16 @@ class TestCreateWikiSource:
assert response.status_code == 400
def test_creates_row_without_ingest(self, app, pg_conn):
from application.api.user.sources.routes import CreateWikiSource
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import CreateWikiSource
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-wiki-create"
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reembed_wiki_page.delay"
"docsgpt.api.user.sources.routes.reembed_wiki_page.delay"
) as mock_reembed, patch(
"application.api.user.tasks.ingest.delay"
"docsgpt.api.user.tasks.ingest.delay"
) as mock_ingest, patch(
"application.api.user.tasks.reingest_source_task.delay"
"docsgpt.api.user.tasks.reingest_source_task.delay"
) as mock_reingest, app.test_request_context(
"/api/sources/wiki", method="POST", json={"name": "My Wiki"}
):
@@ -98,7 +98,7 @@ class TestCreateWikiSource:
# Wiki pages get embedded like any other source, so the row has to name
# the model that did it. NULL reads as "the legacy model" to the boot
# mismatch check, which then reports the source as stale forever.
from application.core.settings import settings
from docsgpt.core.settings import settings
assert row["model"] == settings.EMBEDDINGS_NAME
# No seed content → no re-embed, and never any ingest/reingest task.
@@ -107,20 +107,20 @@ class TestCreateWikiSource:
mock_reingest.assert_not_called()
def test_seed_page_roundtrips_and_only_seed_reembeds(self, app, pg_conn):
from application.api.user.sources.routes import (
from docsgpt.api.user.sources.routes import (
CreateWikiSource,
WikiPage,
WIKI_INDEX_PATH,
)
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-wiki-seed"
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reembed_wiki_page.delay"
"docsgpt.api.user.sources.routes.reembed_wiki_page.delay"
) as mock_reembed, patch(
"application.api.user.tasks.ingest.delay"
"docsgpt.api.user.tasks.ingest.delay"
) as mock_ingest, patch(
"application.api.user.tasks.reingest_source_task.delay"
"docsgpt.api.user.tasks.reingest_source_task.delay"
) as mock_reingest, app.test_request_context(
"/api/sources/wiki",
method="POST",
@@ -154,7 +154,7 @@ class TestCreateWikiSource:
class TestWikiPages:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import WikiPages
from docsgpt.api.user.sources.routes import WikiPages
with app.test_request_context("/api/sources/x/wiki/pages"):
from flask import request
@@ -163,9 +163,9 @@ class TestWikiPages:
assert response.status_code == 401
def test_owner_lists_pages(self, app, pg_conn):
from application.api.user.sources.routes import WikiPages
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
from docsgpt.api.user.sources.routes import WikiPages
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.wiki_pages import WikiPagesRepository
user = "u-wiki-list"
src = SourcesRepository(pg_conn).create(
@@ -190,8 +190,8 @@ class TestWikiPages:
assert via["/index.md"] == "agent"
def test_non_owner_without_grant_404(self, app, pg_conn):
from application.api.user.sources.routes import WikiPages
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import WikiPages
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-wiki-owner"
stranger = "u-wiki-stranger"
@@ -209,9 +209,9 @@ class TestWikiPages:
assert response.status_code == 404
def test_team_viewer_can_read(self, app, pg_conn):
from application.api.user.sources.routes import WikiPages
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
from docsgpt.api.user.sources.routes import WikiPages
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.wiki_pages import WikiPagesRepository
owner = "alice-wiki"
viewer = "bob-wiki-viewer"
@@ -234,8 +234,8 @@ class TestWikiPages:
class TestWikiPage:
def test_returns_400_missing_path(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-wiki-page-nopath"
src = SourcesRepository(pg_conn).create(
@@ -251,8 +251,8 @@ class TestWikiPage:
assert response.status_code == 400
def test_returns_400_traversal_path(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-wiki-page-traversal"
src = SourcesRepository(pg_conn).create(
@@ -268,8 +268,8 @@ class TestWikiPage:
assert response.status_code == 400
def test_returns_404_unknown_page(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-wiki-page-missing"
src = SourcesRepository(pg_conn).create(
@@ -285,9 +285,9 @@ class TestWikiPage:
assert response.status_code == 404
def test_returns_provenance_and_version(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.wiki_pages import WikiPagesRepository
user = "u-wiki-page-provenance"
src = SourcesRepository(pg_conn).create(
@@ -312,9 +312,9 @@ class TestWikiPage:
assert page["updated_at"] is not None
def test_non_owner_without_grant_404(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.wiki_pages import WikiPagesRepository
owner = "u-wiki-pg-owner"
stranger = "u-wiki-pg-stranger"
@@ -335,7 +335,7 @@ class TestWikiPage:
class TestConvertSourceToWiki:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import ConvertSourceToWiki
from docsgpt.api.user.sources.routes import ConvertSourceToWiki
with app.test_request_context(
"/api/sources/x/wiki/convert", method="POST"
@@ -346,9 +346,9 @@ class TestConvertSourceToWiki:
assert response.status_code == 401
def test_blank_source_enabled_inline_no_task(self, app, pg_conn):
from application.api.user.sources.routes import ConvertSourceToWiki
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.source_config import SourceConfig
from docsgpt.api.user.sources.routes import ConvertSourceToWiki
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.source_config import SourceConfig
user = "u-convert-blank"
src = SourcesRepository(pg_conn).create(
@@ -357,7 +357,7 @@ class TestConvertSourceToWiki:
sid = str(src["id"])
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.convert_source_to_wiki.delay"
"docsgpt.api.user.sources.routes.convert_source_to_wiki.delay"
) as mock_convert, app.test_request_context(
f"/api/sources/{sid}/wiki/convert", method="POST"
):
@@ -375,8 +375,8 @@ class TestConvertSourceToWiki:
assert cfg.retrieval.exposure == "agentic_tool"
def test_fileful_source_enqueues_task(self, app, pg_conn):
from application.api.user.sources.routes import ConvertSourceToWiki
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import ConvertSourceToWiki
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-convert-files"
src = SourcesRepository(pg_conn).create(
@@ -387,7 +387,7 @@ class TestConvertSourceToWiki:
fake_task = type("T", (), {"id": "task-xyz"})()
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.convert_source_to_wiki.delay",
"docsgpt.api.user.sources.routes.convert_source_to_wiki.delay",
return_value=fake_task,
) as mock_convert, app.test_request_context(
f"/api/sources/{sid}/wiki/convert", method="POST"
@@ -406,11 +406,11 @@ class TestConvertSourceToWiki:
)
def test_in_progress_ingest_rejected_409(self, app, pg_conn):
from application.api.user.sources.routes import ConvertSourceToWiki
from application.storage.db.repositories.ingest_chunk_progress import (
from docsgpt.api.user.sources.routes import ConvertSourceToWiki
from docsgpt.storage.db.repositories.ingest_chunk_progress import (
IngestChunkProgressRepository,
)
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-convert-ingesting"
src = SourcesRepository(pg_conn).create(
@@ -422,7 +422,7 @@ class TestConvertSourceToWiki:
IngestChunkProgressRepository(pg_conn).init_progress(sid, 5)
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.convert_source_to_wiki.delay"
"docsgpt.api.user.sources.routes.convert_source_to_wiki.delay"
) as mock_convert, app.test_request_context(
f"/api/sources/{sid}/wiki/convert", method="POST"
):
@@ -434,8 +434,8 @@ class TestConvertSourceToWiki:
mock_convert.assert_not_called()
def test_viewer_rejected_403(self, app, pg_conn):
from application.api.user.sources.routes import ConvertSourceToWiki
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import ConvertSourceToWiki
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "alice-convert"
viewer = "bob-convert-viewer"
@@ -447,7 +447,7 @@ class TestConvertSourceToWiki:
_grant_team_access(pg_conn, owner, viewer, sid, "viewer")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.convert_source_to_wiki.delay"
"docsgpt.api.user.sources.routes.convert_source_to_wiki.delay"
) as mock_convert, app.test_request_context(
f"/api/sources/{sid}/wiki/convert", method="POST"
):
@@ -461,7 +461,7 @@ class TestConvertSourceToWiki:
class TestWikiPageEdit:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import WikiPage
from docsgpt.api.user.sources.routes import WikiPage
with app.test_request_context(
"/api/sources/x/wiki/page", method="PUT", json={}
@@ -472,9 +472,9 @@ class TestWikiPageEdit:
assert response.status_code == 401
def test_owner_writes_and_enqueues_reembed(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.wiki_pages import WikiPagesRepository
user = "u-edit-owner"
src = SourcesRepository(pg_conn).create(
@@ -483,7 +483,7 @@ class TestWikiPageEdit:
sid = str(src["id"])
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reembed_wiki_page.delay"
"docsgpt.api.user.sources.routes.reembed_wiki_page.delay"
) as mock_reembed, app.test_request_context(
f"/api/sources/{sid}/wiki/page",
method="PUT",
@@ -505,8 +505,8 @@ class TestWikiPageEdit:
assert mock_reembed.call_args.kwargs["user"] == user
def test_team_editor_reembeds_as_owner(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "alice-edit"
editor = "bob-edit-editor"
@@ -517,7 +517,7 @@ class TestWikiPageEdit:
_grant_team_access(pg_conn, owner, editor, sid, "editor")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reembed_wiki_page.delay"
"docsgpt.api.user.sources.routes.reembed_wiki_page.delay"
) as mock_reembed, app.test_request_context(
f"/api/sources/{sid}/wiki/page",
method="PUT",
@@ -532,9 +532,9 @@ class TestWikiPageEdit:
assert mock_reembed.call_args.kwargs["user"] == owner
def test_stale_version_returns_409(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from application.storage.db.repositories.wiki_pages import WikiPagesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.wiki_pages import WikiPagesRepository
user = "u-edit-conflict"
src = SourcesRepository(pg_conn).create(
@@ -546,7 +546,7 @@ class TestWikiPageEdit:
WikiPagesRepository(pg_conn).upsert(sid, "/c.md", "v2", updated_by=user)
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reembed_wiki_page.delay"
"docsgpt.api.user.sources.routes.reembed_wiki_page.delay"
) as mock_reembed, app.test_request_context(
f"/api/sources/{sid}/wiki/page",
method="PUT",
@@ -560,8 +560,8 @@ class TestWikiPageEdit:
mock_reembed.assert_not_called()
def test_traversal_path_returns_400(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
user = "u-edit-traversal"
src = SourcesRepository(pg_conn).create(
@@ -570,7 +570,7 @@ class TestWikiPageEdit:
sid = str(src["id"])
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reembed_wiki_page.delay"
"docsgpt.api.user.sources.routes.reembed_wiki_page.delay"
), app.test_request_context(
f"/api/sources/{sid}/wiki/page",
method="PUT",
@@ -582,8 +582,8 @@ class TestWikiPageEdit:
assert response.status_code == 400
def test_viewer_rejected_403(self, app, pg_conn):
from application.api.user.sources.routes import WikiPage
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.api.user.sources.routes import WikiPage
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "alice-edit-viewer"
viewer = "bob-edit-viewer"
@@ -594,7 +594,7 @@ class TestWikiPageEdit:
_grant_team_access(pg_conn, owner, viewer, sid, "viewer")
with _patch_db(pg_conn), patch(
"application.api.user.sources.routes.reembed_wiki_page.delay"
"docsgpt.api.user.sources.routes.reembed_wiki_page.delay"
) as mock_reembed, app.test_request_context(
f"/api/sources/{sid}/wiki/page",
method="PUT",