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

+36 -36
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/agents/folders.py.
"""Tests for docsgpt/api/user/agents/folders.py.
Uses the ephemeral ``pg_conn`` fixture to exercise real PG repository code.
"""
@@ -22,22 +22,22 @@ def _patch_folders_db(conn):
yield conn
with patch(
"application.api.user.agents.folders.db_session", _yield
"docsgpt.api.user.agents.folders.db_session", _yield
), patch(
"application.api.user.agents.folders.db_readonly", _yield
"docsgpt.api.user.agents.folders.db_readonly", _yield
):
yield
def _seed_folder(pg_conn, user, name="F", parent_id=None):
from application.storage.db.repositories.agent_folders import (
from docsgpt.storage.db.repositories.agent_folders import (
AgentFoldersRepository,
)
return AgentFoldersRepository(pg_conn).create(user, name, parent_id=parent_id)
def _seed_agent(pg_conn, user, folder_id=None):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
repo = AgentsRepository(pg_conn)
agent = repo.create(user, "test-agent", "published", description="x")
if folder_id:
@@ -47,7 +47,7 @@ def _seed_agent(pg_conn, user, folder_id=None):
class TestAgentFoldersGet:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with app.test_request_context("/api/agents/folders/"):
from flask import request
@@ -56,7 +56,7 @@ class TestAgentFoldersGet:
assert response.status_code == 401
def test_returns_folders_list(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
user = "u-folders-list"
_seed_folder(pg_conn, user, name="A")
@@ -75,7 +75,7 @@ class TestAgentFoldersGet:
class TestAgentFoldersPost:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with app.test_request_context(
"/api/agents/folders/", method="POST", json={"name": "F"}
@@ -86,7 +86,7 @@ class TestAgentFoldersPost:
assert response.status_code == 401
def test_returns_400_missing_name(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with app.test_request_context(
"/api/agents/folders/", method="POST", json={}
@@ -97,7 +97,7 @@ class TestAgentFoldersPost:
assert response.status_code == 400
def test_creates_folder_at_root(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/", method="POST",
@@ -111,7 +111,7 @@ class TestAgentFoldersPost:
assert response.json["parent_id"] is None
def test_creates_nested_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
user = "u-nested"
parent = _seed_folder(pg_conn, user, name="parent")
@@ -127,7 +127,7 @@ class TestAgentFoldersPost:
assert response.json["parent_id"] == str(parent["id"])
def test_returns_404_for_missing_parent(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/", method="POST",
@@ -144,7 +144,7 @@ class TestAgentFoldersPost:
class TestAgentFolderGet:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with app.test_request_context("/api/agents/folders/abc"):
from flask import request
@@ -153,7 +153,7 @@ class TestAgentFolderGet:
assert response.status_code == 401
def test_returns_404_missing_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/00000000-0000-0000-0000-000000000000"
@@ -166,7 +166,7 @@ class TestAgentFolderGet:
assert response.status_code == 404
def test_returns_folder_with_agents_and_subfolders(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
user = "u-folder-detail"
parent = _seed_folder(pg_conn, user, name="parent")
@@ -188,7 +188,7 @@ class TestAgentFolderGet:
class TestAgentFolderPut:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with app.test_request_context(
"/api/agents/folders/abc", method="PUT", json={"name": "new"}
@@ -199,7 +199,7 @@ class TestAgentFolderPut:
assert response.status_code == 401
def test_returns_404_missing_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/00000000-0000-0000-0000-000000000000",
@@ -214,7 +214,7 @@ class TestAgentFolderPut:
assert response.status_code == 404
def test_renames_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
user = "u-rename"
folder = _seed_folder(pg_conn, user, name="old")
@@ -230,7 +230,7 @@ class TestAgentFolderPut:
assert response.status_code == 200
def test_prevents_setting_self_as_parent(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
user = "u-self-parent"
folder = _seed_folder(pg_conn, user, name="f1")
@@ -246,7 +246,7 @@ class TestAgentFolderPut:
assert response.status_code == 400
def test_sets_parent_to_other_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
user = "u-moveparent"
f1 = _seed_folder(pg_conn, user, name="f1")
@@ -263,7 +263,7 @@ class TestAgentFolderPut:
assert response.status_code == 200
def test_returns_404_for_missing_parent(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
user = "u-missingparent"
folder = _seed_folder(pg_conn, user, name="f")
@@ -279,7 +279,7 @@ class TestAgentFolderPut:
assert response.status_code == 404
def test_clears_parent_id_when_null(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
user = "u-clear"
parent = _seed_folder(pg_conn, user, name="p")
@@ -300,7 +300,7 @@ class TestAgentFolderPut:
class TestAgentFolderDelete:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with app.test_request_context(
"/api/agents/folders/abc", method="DELETE"
@@ -311,7 +311,7 @@ class TestAgentFolderDelete:
assert response.status_code == 401
def test_returns_404_missing_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/00000000-0000-0000-0000-000000000000",
@@ -325,7 +325,7 @@ class TestAgentFolderDelete:
assert response.status_code == 404
def test_deletes_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
user = "u-del"
folder = _seed_folder(pg_conn, user, name="tbd")
@@ -341,7 +341,7 @@ class TestAgentFolderDelete:
class TestMoveAgentToFolder:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
with app.test_request_context(
"/api/agents/folders/move_agent",
@@ -354,7 +354,7 @@ class TestMoveAgentToFolder:
assert response.status_code == 401
def test_returns_400_missing_agent_id(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
with app.test_request_context(
"/api/agents/folders/move_agent",
@@ -367,7 +367,7 @@ class TestMoveAgentToFolder:
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/move_agent",
@@ -380,7 +380,7 @@ class TestMoveAgentToFolder:
assert response.status_code == 404
def test_moves_agent_into_folder(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
user = "u-move"
folder = _seed_folder(pg_conn, user, name="target")
@@ -400,7 +400,7 @@ class TestMoveAgentToFolder:
assert response.status_code == 200
def test_returns_404_folder_not_found(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
user = "u-move-nofolder"
agent = _seed_agent(pg_conn, user)
@@ -419,7 +419,7 @@ class TestMoveAgentToFolder:
assert response.status_code == 404
def test_removes_agent_from_folder(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
user = "u-remove"
folder = _seed_folder(pg_conn, user, name="target")
@@ -438,7 +438,7 @@ class TestMoveAgentToFolder:
class TestBulkMoveAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
with app.test_request_context(
"/api/agents/folders/bulk_move",
@@ -451,7 +451,7 @@ class TestBulkMoveAgents:
assert response.status_code == 401
def test_returns_400_missing_agent_ids(self, app):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
with app.test_request_context(
"/api/agents/folders/bulk_move",
@@ -464,7 +464,7 @@ class TestBulkMoveAgents:
assert response.status_code == 400
def test_bulk_moves_agents(self, app, pg_conn):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
user = "u-bulk"
folder = _seed_folder(pg_conn, user, name="dest")
@@ -485,7 +485,7 @@ class TestBulkMoveAgents:
assert response.status_code == 200
def test_returns_404_when_folder_not_found(self, app, pg_conn):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/bulk_move",
@@ -501,7 +501,7 @@ class TestBulkMoveAgents:
assert response.status_code == 404
def test_bulk_move_tolerates_missing_agents(self, app, pg_conn):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
user = "u-bulk-partial"
folder = _seed_folder(pg_conn, user, name="f")
+16 -16
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/agents/guardrails.py and config validation.
"""Tests for docsgpt/api/user/agents/guardrails.py and config validation.
Uses the ephemeral ``pg_conn`` fixture so the repository code is real.
"""
@@ -22,13 +22,13 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.agents.guardrails.db_readonly", _yield
"docsgpt.api.user.agents.guardrails.db_readonly", _yield
):
yield
def _seed_agent(pg_conn, user="u-gr"):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
return AgentsRepository(pg_conn).create(user, "guarded", "published")
@@ -41,7 +41,7 @@ def _seed_agent(pg_conn, user="u-gr"):
@pytest.mark.unit
class TestNormalizeAgentConfig:
def _norm(self, raw):
from application.api.user.agents.routes import normalize_agent_config
from docsgpt.api.user.agents.routes import normalize_agent_config
return normalize_agent_config(raw)
@@ -140,7 +140,7 @@ class TestNormalizeAgentConfig:
@pytest.mark.unit
class TestCatalogRoute:
def _get(self, app, decoded_token={"sub": "u-gr"}):
from application.api.user.agents.guardrails import GuardrailCatalog
from docsgpt.api.user.agents.guardrails import GuardrailCatalog
with app.test_request_context("/api/guardrails/catalog"):
from flask import request
@@ -183,7 +183,7 @@ class TestCatalogRoute:
def test_reports_the_instance_floor(self, app, monkeypatch):
import json
from application.core.settings import settings
from docsgpt.core.settings import settings
monkeypatch.setattr(
settings,
@@ -199,7 +199,7 @@ class TestCatalogRoute:
def test_floor_is_null_when_unset(self, app, monkeypatch):
import json
from application.core.settings import settings
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "GUARDRAILS_FLOOR", {})
payload = json.loads(self._get(app).get_data(as_text=True))
@@ -214,7 +214,7 @@ class TestCatalogRoute:
@pytest.mark.unit
class TestEventsRoute:
def _get(self, app, pg_conn, args="", decoded_token={"sub": "u-gr"}):
from application.api.user.agents.guardrails import GuardrailEvents
from docsgpt.api.user.agents.guardrails import GuardrailEvents
with app.test_request_context(f"/api/guardrails/events{args}"):
from flask import request
@@ -248,7 +248,7 @@ class TestEventsRoute:
def test_returns_recorded_events(self, app, pg_conn):
import json
from application.storage.db.repositories.guardrail_events import (
from docsgpt.storage.db.repositories.guardrail_events import (
GuardrailEventsRepository,
)
@@ -277,7 +277,7 @@ class TestEventsRoute:
@pytest.mark.unit
class TestSummaryRoute:
def _get(self, app, pg_conn, args="", decoded_token={"sub": "u-gr"}):
from application.api.user.agents.guardrails import GuardrailSummary
from docsgpt.api.user.agents.guardrails import GuardrailSummary
with app.test_request_context(f"/api/guardrails/summary{args}"):
from flask import request
@@ -293,7 +293,7 @@ class TestSummaryRoute:
def test_splits_blocked_from_flagged_from_unevaluated(self, app, pg_conn):
import json
from application.storage.db.repositories.guardrail_events import (
from docsgpt.storage.db.repositories.guardrail_events import (
GuardrailEventsRepository,
)
@@ -331,7 +331,7 @@ class TestSummaryAgentScoping:
"""The agent-logs panel needs per-agent aggregates, not per-user ones."""
def _get(self, app, pg_conn, args="", decoded_token={"sub": "u-gr"}):
from application.api.user.agents.guardrails import GuardrailSummary
from docsgpt.api.user.agents.guardrails import GuardrailSummary
with app.test_request_context(f"/api/guardrails/summary{args}"):
from flask import request
@@ -341,8 +341,8 @@ class TestSummaryAgentScoping:
return GuardrailSummary().get()
def _seed(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.guardrail_events import (
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.guardrail_events import (
GuardrailEventsRepository,
)
@@ -400,7 +400,7 @@ class TestConfigRejectionDoesNotLeakInternals:
"""
def test_the_static_message_is_not_derived_from_the_exception(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
INVALID_CONFIG_MESSAGE,
normalize_agent_config,
)
@@ -415,7 +415,7 @@ class TestConfigRejectionDoesNotLeakInternals:
assert INVALID_CONFIG_MESSAGE not in detail
def test_internal_validator_text_still_names_the_field(self):
from application.api.user.agents.routes import normalize_agent_config
from docsgpt.api.user.agents.routes import normalize_agent_config
with pytest.raises(ValueError, match="timeout_ms"):
normalize_agent_config({"guardrails": {"timeout_ms": 5}})
+7 -7
View File
@@ -1,4 +1,4 @@
"""Gap-coverage tests for application.api.user.agents.routes.
"""Gap-coverage tests for docsgpt.api.user.agents.routes.
No bson/ObjectId imports. The routes internally use ObjectId (patched where
needed) and Mongo collections (mocked). Repository assertions use
@@ -37,35 +37,35 @@ class TestNormalizeWorkflowReferenceGaps:
pass
def test_handles_integer_zero(self):
from application.api.user.agents.routes import normalize_workflow_reference
from docsgpt.api.user.agents.routes import normalize_workflow_reference
assert normalize_workflow_reference(0) == "0"
def test_handles_list_converts_to_str(self):
from application.api.user.agents.routes import normalize_workflow_reference
from docsgpt.api.user.agents.routes import normalize_workflow_reference
result = normalize_workflow_reference([1, 2])
assert isinstance(result, str)
def test_json_string_with_underscore_id(self):
from application.api.user.agents.routes import normalize_workflow_reference
from docsgpt.api.user.agents.routes import normalize_workflow_reference
result = normalize_workflow_reference('{"_id": "abc"}')
assert result == "abc"
def test_json_string_with_workflow_id_key(self):
from application.api.user.agents.routes import normalize_workflow_reference
from docsgpt.api.user.agents.routes import normalize_workflow_reference
result = normalize_workflow_reference('{"workflow_id": "wf99"}')
assert result == "wf99"
def test_whitespace_only_string_returns_empty(self):
from application.api.user.agents.routes import normalize_workflow_reference
from docsgpt.api.user.agents.routes import normalize_workflow_reference
assert normalize_workflow_reference(" \t ") == ""
def test_dict_missing_all_id_keys_returns_none(self):
from application.api.user.agents.routes import normalize_workflow_reference
from docsgpt.api.user.agents.routes import normalize_workflow_reference
result = normalize_workflow_reference({"other_key": "value"})
assert result is None
+122 -122
View File
@@ -1,4 +1,4 @@
"""Happy-path tests for application/api/user/agents/routes.py.
"""Happy-path tests for docsgpt/api/user/agents/routes.py.
Exercises each endpoint with the ephemeral ``pg_conn`` fixture and real
repository classes so the route bodies run end-to-end.
@@ -23,9 +23,9 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.agents.routes.db_session", _yield
"docsgpt.api.user.agents.routes.db_session", _yield
), patch(
"application.api.user.agents.routes.db_readonly", _yield
"docsgpt.api.user.agents.routes.db_readonly", _yield
):
yield
@@ -34,12 +34,12 @@ def _seed_agent(
pg_conn, user="u", *, name="A", status="published", with_source=True,
retriever="classic", **extra,
):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
repo = AgentsRepository(pg_conn)
kwargs = {"description": "d", "retriever": retriever, **extra}
if with_source:
# Sources have a UUID FK; seed one
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
src = SourcesRepository(pg_conn).create("src", user_id=user)
kwargs["source_id"] = str(src["id"])
return repo.create(user, name, status, **kwargs)
@@ -47,7 +47,7 @@ def _seed_agent(
class TestGetAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import GetAgent
from docsgpt.api.user.agents.routes import GetAgent
with app.test_request_context("/api/get_agent?id=x"):
from flask import request
@@ -59,7 +59,7 @@ class TestGetAgent:
assert status == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.routes import GetAgent
from docsgpt.api.user.agents.routes import GetAgent
with app.test_request_context("/api/get_agent"):
from flask import request
@@ -71,7 +71,7 @@ class TestGetAgent:
assert status == 400
def test_returns_404_when_missing(self, app, pg_conn):
from application.api.user.agents.routes import GetAgent
from docsgpt.api.user.agents.routes import GetAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/get_agent?id=00000000-0000-0000-0000-000000000000"
@@ -85,7 +85,7 @@ class TestGetAgent:
assert status == 404
def test_returns_agent_by_id(self, app, pg_conn):
from application.api.user.agents.routes import GetAgent
from docsgpt.api.user.agents.routes import GetAgent
user = "u-getA"
agent = _seed_agent(pg_conn, user=user, name="Al")
@@ -102,7 +102,7 @@ class TestGetAgent:
class TestGetAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import GetAgents
from docsgpt.api.user.agents.routes import GetAgents
with app.test_request_context("/api/get_agents"):
from flask import request
@@ -114,7 +114,7 @@ class TestGetAgents:
assert status == 401
def test_returns_list_for_user(self, app, pg_conn):
from application.api.user.agents.routes import GetAgents
from docsgpt.api.user.agents.routes import GetAgents
user = "u-list-agents"
_seed_agent(pg_conn, user=user, name="B1")
@@ -129,7 +129,7 @@ class TestGetAgents:
assert "B1" in names and "B2" in names
def test_db_error_returns_400(self, app):
from application.api.user.agents.routes import GetAgents
from docsgpt.api.user.agents.routes import GetAgents
@contextmanager
def _broken():
@@ -137,7 +137,7 @@ class TestGetAgents:
yield
with patch(
"application.api.user.agents.routes.db_session", _broken
"docsgpt.api.user.agents.routes.db_session", _broken
), app.test_request_context("/api/get_agents"):
from flask import request
request.decoded_token = {"sub": "u"}
@@ -147,7 +147,7 @@ class TestGetAgents:
class TestDeleteAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import DeleteAgent
from docsgpt.api.user.agents.routes import DeleteAgent
with app.test_request_context(
"/api/delete_agent?id=x", method="DELETE"
@@ -158,7 +158,7 @@ class TestDeleteAgent:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.routes import DeleteAgent
from docsgpt.api.user.agents.routes import DeleteAgent
with app.test_request_context(
"/api/delete_agent", method="DELETE"
@@ -169,7 +169,7 @@ class TestDeleteAgent:
assert response.status_code == 400
def test_returns_404_missing_agent(self, app, pg_conn):
from application.api.user.agents.routes import DeleteAgent
from docsgpt.api.user.agents.routes import DeleteAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/delete_agent?id=00000000-0000-0000-0000-000000000000",
@@ -181,8 +181,8 @@ class TestDeleteAgent:
assert response.status_code == 404
def test_deletes_agent(self, app, pg_conn):
from application.api.user.agents.routes import DeleteAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import DeleteAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-delagent"
agent = _seed_agent(pg_conn, user=user)
@@ -199,7 +199,7 @@ class TestDeleteAgent:
class TestPinnedAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import PinnedAgents
from docsgpt.api.user.agents.routes import PinnedAgents
with app.test_request_context("/api/pinned_agents"):
from flask import request
@@ -208,7 +208,7 @@ class TestPinnedAgents:
assert response.status_code == 401
def test_returns_empty_list_for_new_user(self, app, pg_conn):
from application.api.user.agents.routes import PinnedAgents
from docsgpt.api.user.agents.routes import PinnedAgents
with _patch_db(pg_conn), app.test_request_context("/api/pinned_agents"):
from flask import request
@@ -220,7 +220,7 @@ class TestPinnedAgents:
class TestPinAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import PinAgent
from docsgpt.api.user.agents.routes import PinAgent
with app.test_request_context("/api/pin_agent?id=x", method="POST"):
from flask import request
@@ -229,7 +229,7 @@ class TestPinAgent:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.routes import PinAgent
from docsgpt.api.user.agents.routes import PinAgent
with app.test_request_context("/api/pin_agent", method="POST"):
from flask import request
@@ -238,7 +238,7 @@ class TestPinAgent:
assert response.status_code == 400
def test_pins_agent(self, app, pg_conn):
from application.api.user.agents.routes import PinAgent
from docsgpt.api.user.agents.routes import PinAgent
user = "u-pin"
agent = _seed_agent(pg_conn, user=user)
@@ -254,7 +254,7 @@ class TestPinAgent:
class TestGetTemplateAgents:
def test_returns_empty_without_templates(self, app, pg_conn):
from application.api.user.agents.routes import GetTemplateAgents
from docsgpt.api.user.agents.routes import GetTemplateAgents
with _patch_db(pg_conn), app.test_request_context(
"/api/template_agents"
@@ -266,8 +266,8 @@ class TestGetTemplateAgents:
assert response.json == []
def test_returns_templates(self, app, pg_conn):
from application.api.user.agents.routes import GetTemplateAgents
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import GetTemplateAgents
from docsgpt.storage.db.repositories.agents import AgentsRepository
AgentsRepository(pg_conn).create(
"__system__", "Template One", "template",
@@ -286,7 +286,7 @@ class TestGetTemplateAgents:
class TestAdoptAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import AdoptAgent
from docsgpt.api.user.agents.routes import AdoptAgent
with app.test_request_context(
"/api/adopt_agent?id=x", method="POST"
@@ -297,7 +297,7 @@ class TestAdoptAgent:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.routes import AdoptAgent
from docsgpt.api.user.agents.routes import AdoptAgent
with app.test_request_context(
"/api/adopt_agent", method="POST"
@@ -310,7 +310,7 @@ class TestAdoptAgent:
class TestRemoveSharedAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import RemoveSharedAgent
from docsgpt.api.user.agents.routes import RemoveSharedAgent
with app.test_request_context(
"/api/remove_shared_agent?id=x", method="POST"
@@ -321,7 +321,7 @@ class TestRemoveSharedAgent:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.routes import RemoveSharedAgent
from docsgpt.api.user.agents.routes import RemoveSharedAgent
with app.test_request_context(
"/api/remove_shared_agent", method="POST"
@@ -332,9 +332,9 @@ class TestRemoveSharedAgent:
assert response.status_code == 400
def test_removes_shared_agent(self, app, pg_conn):
from application.api.user.agents.routes import RemoveSharedAgent
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.users import UsersRepository
from docsgpt.api.user.agents.routes import RemoveSharedAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.users import UsersRepository
owner = "owner-user"
viewer = "u-rm-shared"
@@ -357,7 +357,7 @@ class TestRemoveSharedAgent:
class TestCreateAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
with app.test_request_context(
"/api/create_agent",
@@ -373,7 +373,7 @@ class TestCreateAgent:
assert status == 401
def test_returns_400_missing_required_draft(self, app):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
with app.test_request_context(
"/api/create_agent",
@@ -386,8 +386,8 @@ class TestCreateAgent:
assert response.status_code == 400
def test_creates_draft_classic_agent(self, app, pg_conn):
from application.api.user.agents.routes import CreateAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import CreateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-create-A"
@@ -417,7 +417,7 @@ class TestCreateAgent:
class TestUpdateAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
with app.test_request_context(
"/api/update_agent/abc", method="PUT",
@@ -429,7 +429,7 @@ class TestUpdateAgent:
assert response.status_code == 401
def test_returns_404_agent_not_found(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/update_agent/00000000-0000-0000-0000-000000000000",
@@ -444,8 +444,8 @@ class TestUpdateAgent:
assert response.status_code == 404
def test_updates_simple_fields(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import UpdateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-upd-simple"
agent = _seed_agent(pg_conn, user=user, name="orig")
@@ -466,8 +466,8 @@ class TestUpdateAgent:
assert got["name"] == "new name"
def test_ignores_client_supplied_image_path(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import UpdateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-upd-image-path"
agent = _seed_agent(
@@ -488,7 +488,7 @@ class TestUpdateAgent:
assert got["image"] is None
def test_invalid_status_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-upd-status"
agent = _seed_agent(pg_conn, user=user)
@@ -503,7 +503,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_invalid_source_uuid_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-upd-src"
agent = _seed_agent(pg_conn, user=user)
@@ -521,7 +521,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_source_default_clears(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-src-default"
agent = _seed_agent(pg_conn, user=user)
@@ -539,7 +539,7 @@ class TestUpdateAgent:
assert response.status_code == 200
def test_invalid_sources_item_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-sources"
agent = _seed_agent(pg_conn, user=user)
@@ -583,7 +583,7 @@ class TestUpdateAgent:
"""
import logging
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = f"u-log-{field}"
agent = _seed_agent(pg_conn, user=user)
@@ -607,7 +607,7 @@ class TestUpdateAgent:
), f"no WARN naming field={field!r} and user={user!r}; got {warnings!r}"
def test_invalid_chunks_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-upd-chunks"
agent = _seed_agent(pg_conn, user=user)
@@ -625,7 +625,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_negative_chunks_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-neg-chunks"
agent = _seed_agent(pg_conn, user=user)
@@ -643,7 +643,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_tools_must_be_list(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-tools"
agent = _seed_agent(pg_conn, user=user)
@@ -661,7 +661,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_limited_token_mode_requires_limit(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-limit"
agent = _seed_agent(pg_conn, user=user)
@@ -679,7 +679,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_limited_request_mode_requires_limit(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-req-limit"
agent = _seed_agent(pg_conn, user=user)
@@ -697,7 +697,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_token_limit_without_mode_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-token-mode"
agent = _seed_agent(pg_conn, user=user)
@@ -715,7 +715,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_invalid_prompt_id_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-upd-prompt"
agent = _seed_agent(pg_conn, user=user)
@@ -733,7 +733,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_prompt_id_default_clears(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-pid-default"
agent = _seed_agent(pg_conn, user=user)
@@ -751,7 +751,7 @@ class TestUpdateAgent:
assert response.status_code == 200
def test_empty_name_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-empty-name"
agent = _seed_agent(pg_conn, user=user)
@@ -766,7 +766,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_publish_classic_missing_fields_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-publish-missing"
agent = _seed_agent(pg_conn, user=user, with_source=False)
@@ -789,9 +789,9 @@ class TestUpdateAgent:
``Missing or invalid required fields: Source``. The retriever
carries the runtime identity, so the gate now accepts it.
"""
from application.api.user.agents.routes import UpdateAgent
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.prompts import PromptsRepository
from docsgpt.api.user.agents.routes import UpdateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.prompts import PromptsRepository
user = "u-publish-default"
# Draft agent with retriever='classic', no source_id, prompt + chunks set.
@@ -832,9 +832,9 @@ class TestUpdateAgent:
):
"""If neither a source nor a retriever is configured, the gate
still trips — the agent has no way to retrieve anything."""
from application.api.user.agents.routes import UpdateAgent
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.prompts import PromptsRepository
from docsgpt.api.user.agents.routes import UpdateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.prompts import PromptsRepository
user = "u-publish-no-retriever"
agent = _seed_agent(
@@ -868,15 +868,15 @@ class TestUpdateAgent:
assert "Source or retriever" in response.json.get("message", "")
def test_publishing_generates_api_key(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import UpdateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-publish-key"
agent = _seed_agent(
pg_conn, user=user, status="draft", retriever="classic",
)
# Seed a prompt so published path validates
from application.storage.db.repositories.prompts import PromptsRepository
from docsgpt.storage.db.repositories.prompts import PromptsRepository
prompt = PromptsRepository(pg_conn).create(user, "p", "c")
AgentsRepository(pg_conn).update(
str(agent["id"]), user,
@@ -901,7 +901,7 @@ class TestUpdateAgent:
assert "key" in response.json
def test_invalid_json_in_form_field_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-bad-json"
agent = _seed_agent(pg_conn, user=user)
@@ -922,7 +922,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_empty_update_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-empty-upd"
agent = _seed_agent(pg_conn, user=user)
@@ -937,7 +937,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_db_error_returns_500(self, app):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
@contextmanager
def _broken():
@@ -945,7 +945,7 @@ class TestUpdateAgent:
yield
with patch(
"application.api.user.agents.routes.db_session", _broken
"docsgpt.api.user.agents.routes.db_session", _broken
), app.test_request_context(
"/api/update_agent/abc", method="PUT",
json={"name": "n", "description": "d", "status": "draft"},
@@ -956,7 +956,7 @@ class TestUpdateAgent:
assert response.status_code == 500
def test_allow_system_prompt_override(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-override"
agent = _seed_agent(pg_conn, user=user)
@@ -974,7 +974,7 @@ class TestUpdateAgent:
assert response.status_code == 200
def test_folder_id_null_clears(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-folder-null"
agent = _seed_agent(pg_conn, user=user)
@@ -992,7 +992,7 @@ class TestUpdateAgent:
assert response.status_code == 200
def test_publish_workflow_without_workflow_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-wf-publish"
agent = _seed_agent(pg_conn, user=user, with_source=False)
@@ -1012,7 +1012,7 @@ class TestUpdateAgent:
assert response.status_code == 400
def test_invalid_json_schema_returns_400(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-bad-schema"
agent = _seed_agent(pg_conn, user=user)
@@ -1033,7 +1033,7 @@ class TestUpdateAgent:
assert response.status_code in (200, 400)
def test_json_schema_empty_becomes_none(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from docsgpt.api.user.agents.routes import UpdateAgent
user = "u-schema-empty"
agent = _seed_agent(pg_conn, user=user)
@@ -1061,7 +1061,7 @@ class TestUpdateAgent:
class TestCreateAgentMore:
def test_invalid_status_returns_400(self, app):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
with app.test_request_context(
"/api/create_agent", method="POST",
@@ -1076,7 +1076,7 @@ class TestCreateAgentMore:
assert status == 400
def test_publish_classic_without_source_returns_400(self, app):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
with app.test_request_context(
"/api/create_agent", method="POST",
@@ -1096,7 +1096,7 @@ class TestCreateAgentMore:
assert status == 400
def test_unknown_agent_type_falls_back_classic(self, app, pg_conn):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
user = "u-unknown-type"
with _patch_db(pg_conn), app.test_request_context(
@@ -1117,7 +1117,7 @@ class TestCreateAgentMore:
assert status == 201
def test_create_form_invalid_json_fields(self, app, pg_conn):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
# Invalid JSON strings in form fields are coerced to []/None (no 400)
user = "u-form-json"
@@ -1145,7 +1145,7 @@ class TestCreateAgentMore:
def test_create_published_workflow_without_workflow_returns_400(
self, app, pg_conn,
):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
user = "u-wf-no-wf"
with _patch_db(pg_conn), app.test_request_context(
@@ -1166,8 +1166,8 @@ class TestCreateAgentMore:
assert status == 400
def test_create_with_sources_list(self, app, pg_conn):
from application.api.user.agents.routes import CreateAgent
from application.storage.db.repositories.sources import (
from docsgpt.api.user.agents.routes import CreateAgent
from docsgpt.storage.db.repositories.sources import (
SourcesRepository,
)
@@ -1194,7 +1194,7 @@ class TestCreateAgentMore:
assert status == 201
def test_db_error_returns_400(self, app):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
@contextmanager
def _broken():
@@ -1202,7 +1202,7 @@ class TestCreateAgentMore:
yield
with patch(
"application.api.user.agents.routes.db_session", _broken
"docsgpt.api.user.agents.routes.db_session", _broken
), app.test_request_context(
"/api/create_agent", method="POST",
json={
@@ -1226,8 +1226,8 @@ class TestCreateAgentMore:
class TestAdoptAgentMore:
def test_adopts_template_agent(self, app, pg_conn):
from application.api.user.agents.routes import AdoptAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import AdoptAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
repo = AgentsRepository(pg_conn)
template = repo.create("__system__", "Template X", "template")
@@ -1247,10 +1247,10 @@ class TestAdoptAgentMore:
import io
from unittest.mock import MagicMock
from application.api.user.agents.routes import AdoptAgent
from application.core.settings import settings
from application.storage.db.repositories.agents import AgentsRepository
from application.utils import (
from docsgpt.api.user.agents.routes import AdoptAgent
from docsgpt.core.settings import settings
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.utils import (
is_safe_agent_image_path,
safe_user_storage_component,
)
@@ -1268,7 +1268,7 @@ class TestAdoptAgentMore:
storage.save_file.side_effect = lambda data, path, **kw: {"path": path}
with _patch_db(pg_conn), patch(
"application.api.user.agents.routes.storage", storage
"docsgpt.api.user.agents.routes.storage", storage
), app.test_request_context(
f"/api/adopt_agent?id={template['id']}", method="POST"
):
@@ -1292,8 +1292,8 @@ class TestAdoptAgentMore:
def test_adopt_drops_image_when_copy_fails(self, app, pg_conn):
from unittest.mock import MagicMock
from application.api.user.agents.routes import AdoptAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import AdoptAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
repo = AgentsRepository(pg_conn)
template = repo.create(
@@ -1307,7 +1307,7 @@ class TestAdoptAgentMore:
storage.get_file.side_effect = FileNotFoundError("missing")
with _patch_db(pg_conn), patch(
"application.api.user.agents.routes.storage", storage
"docsgpt.api.user.agents.routes.storage", storage
), app.test_request_context(
f"/api/adopt_agent?id={template['id']}", method="POST"
):
@@ -1326,8 +1326,8 @@ class TestAdoptAgentMore:
def test_adopt_keeps_external_image_url(self, app, pg_conn):
from unittest.mock import MagicMock
from application.api.user.agents.routes import AdoptAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import AdoptAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
repo = AgentsRepository(pg_conn)
template = repo.create(
@@ -1340,7 +1340,7 @@ class TestAdoptAgentMore:
storage = MagicMock()
with _patch_db(pg_conn), patch(
"application.api.user.agents.routes.storage", storage
"docsgpt.api.user.agents.routes.storage", storage
), app.test_request_context(
f"/api/adopt_agent?id={template['id']}", method="POST"
):
@@ -1358,7 +1358,7 @@ class TestAdoptAgentMore:
storage.get_file.assert_not_called()
def test_adopt_template_missing_returns_404(self, app, pg_conn):
from application.api.user.agents.routes import AdoptAgent
from docsgpt.api.user.agents.routes import AdoptAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/adopt_agent?id=00000000-0000-0000-0000-000000000000",
@@ -1370,7 +1370,7 @@ class TestAdoptAgentMore:
assert response.status_code == 404
def test_db_error_returns_400(self, app):
from application.api.user.agents.routes import AdoptAgent
from docsgpt.api.user.agents.routes import AdoptAgent
@contextmanager
def _broken():
@@ -1378,7 +1378,7 @@ class TestAdoptAgentMore:
yield
with patch(
"application.api.user.agents.routes.db_session", _broken
"docsgpt.api.user.agents.routes.db_session", _broken
), app.test_request_context(
"/api/adopt_agent?id=abc", method="POST"
):
@@ -1391,8 +1391,8 @@ class TestAdoptAgentMore:
class TestPinAgentMore:
def test_toggle_pin(self, app, pg_conn):
"""PinAgent is a toggle — second call unpins."""
from application.api.user.agents.routes import PinAgent
from application.storage.db.repositories.users import UsersRepository
from docsgpt.api.user.agents.routes import PinAgent
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-pin-toggle"
agent = _seed_agent(pg_conn, user=user)
@@ -1420,7 +1420,7 @@ class TestPinAgentMore:
assert response.status_code == 200
def test_pin_db_error_returns_500(self, app):
from application.api.user.agents.routes import PinAgent
from docsgpt.api.user.agents.routes import PinAgent
@contextmanager
def _broken():
@@ -1428,7 +1428,7 @@ class TestPinAgentMore:
yield
with patch(
"application.api.user.agents.routes.db_session", _broken
"docsgpt.api.user.agents.routes.db_session", _broken
), app.test_request_context(
"/api/pin_agent?id=abc", method="POST"
):
@@ -1440,14 +1440,14 @@ class TestPinAgentMore:
class TestRegenerateAgentKey:
def _seed_published_with_key(self, pg_conn, user, key):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = _seed_agent(pg_conn, user=user, status="published")
AgentsRepository(pg_conn).update(str(agent["id"]), user, {"key": key})
return agent
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.routes import RegenerateAgentKey
from docsgpt.api.user.agents.routes import RegenerateAgentKey
with app.test_request_context(
"/api/regenerate_agent_key/abc", method="POST"
@@ -1461,7 +1461,7 @@ class TestRegenerateAgentKey:
assert status == 401
def test_returns_404_when_missing(self, app, pg_conn):
from application.api.user.agents.routes import RegenerateAgentKey
from docsgpt.api.user.agents.routes import RegenerateAgentKey
with _patch_db(pg_conn), app.test_request_context(
"/api/regenerate_agent_key/00000000-0000-0000-0000-000000000000",
@@ -1476,7 +1476,7 @@ class TestRegenerateAgentKey:
def test_returns_404_for_non_owner(self, app, pg_conn):
"""Owner-only: another user cannot rotate someone else's key."""
from application.api.user.agents.routes import RegenerateAgentKey
from docsgpt.api.user.agents.routes import RegenerateAgentKey
owner = "u-owner-key"
agent = self._seed_published_with_key(pg_conn, owner, "owner-old-key")
@@ -1490,7 +1490,7 @@ class TestRegenerateAgentKey:
assert response.status_code == 404
def test_returns_400_for_draft_without_key(self, app, pg_conn):
from application.api.user.agents.routes import RegenerateAgentKey
from docsgpt.api.user.agents.routes import RegenerateAgentKey
user = "u-draft-key"
agent = _seed_agent(pg_conn, user=user, status="draft")
@@ -1504,8 +1504,8 @@ class TestRegenerateAgentKey:
assert response.status_code == 400
def test_regenerates_key_and_invalidates_old(self, app, pg_conn):
from application.api.user.agents.routes import RegenerateAgentKey
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import RegenerateAgentKey
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-regen"
old_key = "regen-old-key"
@@ -1536,17 +1536,17 @@ class TestRegenerateAgentKey:
not owner-scoped, and the conversation is created with api_key set but
agent_id NULL to prove the api_key-only path is covered.
"""
from application.api.user.agents.routes import RegenerateAgentKey
from application.storage.db.repositories.conversations import (
from docsgpt.api.user.agents.routes import RegenerateAgentKey
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
from application.storage.db.repositories.shared_conversations import (
from docsgpt.storage.db.repositories.shared_conversations import (
SharedConversationsRepository,
)
from application.storage.db.repositories.stack_logs import (
from docsgpt.storage.db.repositories.stack_logs import (
StackLogsRepository,
)
from application.storage.db.repositories.token_usage import (
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -1601,7 +1601,7 @@ class TestRegenerateAgentKey:
assert _count("shared_conversations", new_key) == 1
def test_db_error_returns_500(self, app):
from application.api.user.agents.routes import RegenerateAgentKey
from docsgpt.api.user.agents.routes import RegenerateAgentKey
@contextmanager
def _broken():
@@ -1609,7 +1609,7 @@ class TestRegenerateAgentKey:
yield
with patch(
"application.api.user.agents.routes.db_session", _broken
"docsgpt.api.user.agents.routes.db_session", _broken
), app.test_request_context(
"/api/regenerate_agent_key/abc", method="POST"
):
@@ -1621,7 +1621,7 @@ class TestRegenerateAgentKey:
class TestPinnedAgentsListing:
def test_returns_pinned_after_pinning(self, app, pg_conn):
from application.api.user.agents.routes import PinAgent, PinnedAgents
from docsgpt.api.user.agents.routes import PinAgent, PinnedAgents
user = "u-pinned-list"
agent = _seed_agent(pg_conn, user=user, retriever="classic")
@@ -1646,7 +1646,7 @@ class TestPinnedAgentsListing:
assert response.json[0]["pinned"] is True
def test_pinned_db_error_returns_400(self, app):
from application.api.user.agents.routes import PinnedAgents
from docsgpt.api.user.agents.routes import PinnedAgents
@contextmanager
def _broken():
@@ -1654,7 +1654,7 @@ class TestPinnedAgentsListing:
yield
with patch(
"application.api.user.agents.routes.db_session", _broken
"docsgpt.api.user.agents.routes.db_session", _broken
), app.test_request_context("/api/pinned_agents"):
from flask import request
request.decoded_token = {"sub": "u"}
+30 -30
View File
@@ -1,4 +1,4 @@
"""Tests for helper functions in application/api/user/agents/routes.py."""
"""Tests for helper functions in docsgpt/api/user/agents/routes.py."""
import json
from contextlib import contextmanager
@@ -20,28 +20,28 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.agents.routes.db_session", _yield
"docsgpt.api.user.agents.routes.db_session", _yield
), patch(
"application.api.user.agents.routes.db_readonly", _yield
"docsgpt.api.user.agents.routes.db_readonly", _yield
):
yield
class TestNormalizeWorkflowReference:
def test_none_returns_none(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference(None) is None
def test_dict_returns_id_field(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference({"id": "w1"}) == "w1"
def test_dict_returns_workflow_id_field(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert (
@@ -49,25 +49,25 @@ class TestNormalizeWorkflowReference:
)
def test_dict_with_underscore_id(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference({"_id": "wf-2"}) == "wf-2"
def test_empty_string_returns_empty(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference("") == ""
def test_plain_string_returned(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference("wf-123") == "wf-123"
def test_json_string_dict(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert (
@@ -75,19 +75,19 @@ class TestNormalizeWorkflowReference:
)
def test_json_string_as_string(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference('"wf-str"') == "wf-str"
def test_invalid_json_returns_original(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference("not-json-wf") == "not-json-wf"
def test_non_string_non_dict_coerced(self):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
normalize_workflow_reference,
)
assert normalize_workflow_reference(42) == "42"
@@ -95,14 +95,14 @@ class TestNormalizeWorkflowReference:
class TestResolveWorkflowForUser:
def test_none_workflow_returns_none(self, pg_conn):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
_resolve_workflow_for_user,
)
pg_id, err = _resolve_workflow_for_user(pg_conn, None, "u")
assert pg_id is None and err is None
def test_not_found_returns_error(self, pg_conn, app):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
_resolve_workflow_for_user,
)
with app.app_context():
@@ -116,10 +116,10 @@ class TestResolveWorkflowForUser:
assert err.status_code == 404
def test_resolves_owned_workflow(self, pg_conn, app):
from application.api.user.agents.routes import (
from docsgpt.api.user.agents.routes import (
_resolve_workflow_for_user,
)
from application.storage.db.repositories.workflows import (
from docsgpt.storage.db.repositories.workflows import (
WorkflowsRepository,
)
@@ -135,13 +135,13 @@ class TestResolveWorkflowForUser:
class TestResolveFolderId:
def test_none_returns_none(self, pg_conn):
from application.api.user.agents.routes import _resolve_folder_id
from docsgpt.api.user.agents.routes import _resolve_folder_id
pg_id, err = _resolve_folder_id(pg_conn, None, "u")
assert pg_id is None
assert err is None
def test_not_found_returns_error(self, pg_conn, app):
from application.api.user.agents.routes import _resolve_folder_id
from docsgpt.api.user.agents.routes import _resolve_folder_id
with app.app_context():
pg_id, err = _resolve_folder_id(
@@ -151,8 +151,8 @@ class TestResolveFolderId:
assert err.status_code == 404
def test_resolves_owned_folder(self, pg_conn, app):
from application.api.user.agents.routes import _resolve_folder_id
from application.storage.db.repositories.agent_folders import (
from docsgpt.api.user.agents.routes import _resolve_folder_id
from docsgpt.storage.db.repositories.agent_folders import (
AgentFoldersRepository,
)
@@ -166,7 +166,7 @@ class TestResolveFolderId:
class TestFormatAgentOutput:
def test_basic_shape(self):
from application.api.user.agents.routes import _format_agent_output
from docsgpt.api.user.agents.routes import _format_agent_output
agent = {
"id": "agent-1",
@@ -191,21 +191,21 @@ class TestFormatAgentOutput:
assert out["key"].startswith("secr") and out["key"].endswith("long")
def test_no_key_masking(self):
from application.api.user.agents.routes import _format_agent_output
from docsgpt.api.user.agents.routes import _format_agent_output
agent = {"id": "a", "name": "n", "chunks": None}
out = _format_agent_output(agent, include_key_masked=False)
assert "key" not in out
def test_empty_key_returns_empty_string(self):
from application.api.user.agents.routes import _format_agent_output
from docsgpt.api.user.agents.routes import _format_agent_output
agent = {"id": "a", "name": "n", "key": ""}
out = _format_agent_output(agent)
assert out["key"] == ""
def test_with_folder_and_workflow(self):
from application.api.user.agents.routes import _format_agent_output
from docsgpt.api.user.agents.routes import _format_agent_output
agent = {
"id": "a", "name": "n",
@@ -218,7 +218,7 @@ class TestFormatAgentOutput:
class TestBuildCreateKwargs:
def test_classic_kwargs(self):
from application.api.user.agents.routes import _build_create_kwargs
from docsgpt.api.user.agents.routes import _build_create_kwargs
data = {
"description": "d",
@@ -233,7 +233,7 @@ class TestBuildCreateKwargs:
assert out["chunks"] == 3
def test_invalid_chunks_skipped(self, app):
from application.api.user.agents.routes import _build_create_kwargs
from docsgpt.api.user.agents.routes import _build_create_kwargs
data = {"chunks": "abc"}
with app.app_context():
@@ -243,7 +243,7 @@ class TestBuildCreateKwargs:
assert "chunks" not in out
def test_prompt_id_default_not_set(self):
from application.api.user.agents.routes import _build_create_kwargs
from docsgpt.api.user.agents.routes import _build_create_kwargs
data = {"prompt_id": "default"}
out = _build_create_kwargs(
@@ -252,7 +252,7 @@ class TestBuildCreateKwargs:
assert "prompt_id" not in out
def test_image_url_used_when_provided(self):
from application.api.user.agents.routes import _build_create_kwargs
from docsgpt.api.user.agents.routes import _build_create_kwargs
out = _build_create_kwargs(
{}, image_url="/upload/img.png", agent_type="classic",
@@ -260,7 +260,7 @@ class TestBuildCreateKwargs:
assert out.get("image") == "/upload/img.png"
def test_client_image_path_is_never_persisted(self):
from application.api.user.agents.routes import _build_create_kwargs
from docsgpt.api.user.agents.routes import _build_create_kwargs
out = _build_create_kwargs(
{"image": ".env"}, image_url="", agent_type="classic",
+25 -25
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/agents/sharing.py.
"""Tests for docsgpt/api/user/agents/sharing.py.
Uses the ephemeral ``pg_conn`` fixture to exercise the real PG repository
code paths (agents, users).
@@ -23,15 +23,15 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.agents.sharing.db_session", _yield
"docsgpt.api.user.agents.sharing.db_session", _yield
), patch(
"application.api.user.agents.sharing.db_readonly", _yield
"docsgpt.api.user.agents.sharing.db_readonly", _yield
):
yield
def _make_agent(pg_conn, user_id="owner", *, shared=False, shared_token=None):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
user_id,
"Agent",
@@ -49,7 +49,7 @@ def _make_agent(pg_conn, user_id="owner", *, shared=False, shared_token=None):
class TestSharedAgentGet:
def test_returns_400_missing_token(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
with app.test_request_context("/api/shared_agent"):
from flask import request
@@ -58,7 +58,7 @@ class TestSharedAgentGet:
assert response.status_code == 400
def test_returns_404_for_unknown_token(self, app, pg_conn):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agent?token=unknown"
@@ -69,7 +69,7 @@ class TestSharedAgentGet:
assert response.status_code == 404
def test_returns_agent_for_known_token(self, app, pg_conn):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
_make_agent(pg_conn, shared=True, shared_token="abc123")
@@ -85,7 +85,7 @@ class TestSharedAgentGet:
assert data["shared"] is True
def test_records_shared_with_different_user(self, app, pg_conn):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
_make_agent(pg_conn, user_id="owner", shared=True, shared_token="tk1")
@@ -98,7 +98,7 @@ class TestSharedAgentGet:
assert response.status_code == 200
def test_db_error_returns_400(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
@contextmanager
def _broken():
@@ -106,7 +106,7 @@ class TestSharedAgentGet:
yield
with patch(
"application.api.user.agents.sharing.db_readonly", _broken
"docsgpt.api.user.agents.sharing.db_readonly", _broken
), app.test_request_context("/api/shared_agent?token=x"):
from flask import request
request.decoded_token = None
@@ -116,7 +116,7 @@ class TestSharedAgentGet:
class TestSharedAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
with app.test_request_context("/api/shared_agents"):
from flask import request
@@ -125,7 +125,7 @@ class TestSharedAgents:
assert response.status_code == 401
def test_returns_empty_list_for_new_user(self, app, pg_conn):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agents"
@@ -139,7 +139,7 @@ class TestSharedAgents:
def test_returns_shared_agents_for_user(self, app, pg_conn):
"""After SharedAgent adds an agent to the user's shared_with_me, it
should appear in SharedAgents."""
from application.api.user.agents.sharing import SharedAgent, SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgent, SharedAgents
_make_agent(pg_conn, user_id="owner", shared=True, shared_token="tk2")
@@ -162,7 +162,7 @@ class TestSharedAgents:
assert response.json[0]["shared"] is True
def test_db_error_returns_400(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
@contextmanager
def _broken():
@@ -170,7 +170,7 @@ class TestSharedAgents:
yield
with patch(
"application.api.user.agents.sharing.db_session", _broken
"docsgpt.api.user.agents.sharing.db_session", _broken
), app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = {"sub": "u"}
@@ -180,7 +180,7 @@ class TestSharedAgents:
class TestShareAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent", method="PUT", json={"id": "x", "shared": True}
@@ -191,7 +191,7 @@ class TestShareAgent:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent", method="PUT", json={"shared": True}
@@ -202,7 +202,7 @@ class TestShareAgent:
assert response.status_code == 400
def test_returns_400_missing_shared(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent", method="PUT", json={"id": "x"}
@@ -213,7 +213,7 @@ class TestShareAgent:
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app, pg_conn):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/share_agent",
@@ -226,8 +226,8 @@ class TestShareAgent:
assert response.status_code == 404
def test_shares_agent(self, app, pg_conn):
from application.api.user.agents.sharing import ShareAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.sharing import ShareAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = _make_agent(pg_conn, user_id="owner")
agent_id = str(agent["id"])
@@ -250,8 +250,8 @@ class TestShareAgent:
assert got["shared_metadata"]["shared_by"] == "alice"
def test_unshares_agent(self, app, pg_conn):
from application.api.user.agents.sharing import ShareAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.sharing import ShareAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = _make_agent(pg_conn, user_id="owner", shared=True, shared_token="tk")
agent_id = str(agent["id"])
@@ -270,7 +270,7 @@ class TestShareAgent:
assert got["shared_token"] is None
def test_db_error_returns_400(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
@contextmanager
def _broken():
@@ -278,7 +278,7 @@ class TestShareAgent:
yield
with patch(
"application.api.user.agents.sharing.db_session", _broken
"docsgpt.api.user.agents.sharing.db_session", _broken
), app.test_request_context(
"/api/share_agent",
method="PUT",
+28 -28
View File
@@ -1,4 +1,4 @@
"""Gap-coverage tests for application.api.user.agents.webhooks.
"""Gap-coverage tests for docsgpt.api.user.agents.webhooks.
These tests use only stdlib IDs (uuid / hex strings) — no bson/ObjectId.
The agent routes still read from Mongo collections internally; we mock
@@ -47,13 +47,13 @@ class TestAgentWebhookListenerGaps:
def test_post_empty_payload_still_enqueues(self, app):
"""Empty dict payload does not block task enqueue (warning only)."""
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
mock_task = Mock()
mock_task.id = "task_empty"
with patch(
"application.api.user.agents.webhooks.process_agent_webhook"
"docsgpt.api.user.agents.webhooks.process_agent_webhook"
) as mock_process:
mock_process.apply_async.return_value = mock_task
with app.test_request_context(
@@ -69,13 +69,13 @@ class TestAgentWebhookListenerGaps:
def test_get_empty_query_string(self, app):
"""GET request with no query params produces an empty payload dict."""
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
mock_task = Mock()
mock_task.id = "task_noqs"
with patch(
"application.api.user.agents.webhooks.process_agent_webhook"
"docsgpt.api.user.agents.webhooks.process_agent_webhook"
) as mock_process:
mock_process.apply_async.return_value = mock_task
with app.test_request_context(
@@ -96,13 +96,13 @@ class TestAgentWebhookListenerGaps:
def test_enqueue_returns_task_id_in_response(self, app):
"""Success response body includes task_id from the Celery task."""
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
mock_task = Mock()
mock_task.id = "celery-task-99"
with patch(
"application.api.user.agents.webhooks.process_agent_webhook"
"docsgpt.api.user.agents.webhooks.process_agent_webhook"
) as mock_process:
mock_process.apply_async.return_value = mock_task
with app.test_request_context(
@@ -118,10 +118,10 @@ class TestAgentWebhookListenerGaps:
def test_enqueue_error_returns_500_with_message(self, app):
"""Queue failure returns 500 with a human-readable message."""
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
with patch(
"application.api.user.agents.webhooks.process_agent_webhook"
"docsgpt.api.user.agents.webhooks.process_agent_webhook"
) as mock_process:
mock_process.apply_async.side_effect = RuntimeError("celery is down")
with app.test_request_context(
@@ -149,9 +149,9 @@ def _patch_webhooks_db(conn):
yield conn
with patch(
"application.api.user.agents.webhooks.db_session", _yield
"docsgpt.api.user.agents.webhooks.db_session", _yield
), patch(
"application.api.user.agents.webhooks.db_readonly", _yield
"docsgpt.api.user.agents.webhooks.db_readonly", _yield
):
yield
@@ -163,16 +163,16 @@ def _patch_base_db(conn):
yield conn
with patch(
"application.api.user.base.db_readonly", _yield
"docsgpt.api.user.base.db_readonly", _yield
), patch(
"application.api.user.base.db_session", _yield
"docsgpt.api.user.base.db_session", _yield
):
yield
class TestAgentWebhookGet:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.webhooks import AgentWebhook
from docsgpt.api.user.agents.webhooks import AgentWebhook
with app.test_request_context("/api/agent_webhook?id=x"):
from flask import request
@@ -181,7 +181,7 @@ class TestAgentWebhookGet:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.webhooks import AgentWebhook
from docsgpt.api.user.agents.webhooks import AgentWebhook
with app.test_request_context("/api/agent_webhook"):
from flask import request
@@ -190,7 +190,7 @@ class TestAgentWebhookGet:
assert response.status_code == 400
def test_returns_404_missing_agent(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhook
from docsgpt.api.user.agents.webhooks import AgentWebhook
with _patch_webhooks_db(pg_conn), app.test_request_context(
"/api/agent_webhook?id=00000000-0000-0000-0000-000000000000"
@@ -201,14 +201,14 @@ class TestAgentWebhookGet:
assert response.status_code == 404
def test_generates_webhook_url(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhook
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.webhooks import AgentWebhook
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-wh"
agent = AgentsRepository(pg_conn).create(user, "a", "published")
with _patch_webhooks_db(pg_conn), patch(
"application.api.user.agents.webhooks.settings.API_URL",
"docsgpt.api.user.agents.webhooks.settings.API_URL",
"https://api.test",
), app.test_request_context(
f"/api/agent_webhook?id={agent['id']}"
@@ -222,8 +222,8 @@ class TestAgentWebhookGet:
)
def test_reuses_existing_webhook_token(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhook
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.webhooks import AgentWebhook
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-wh-reuse"
agent = AgentsRepository(pg_conn).create(
@@ -231,7 +231,7 @@ class TestAgentWebhookGet:
)
with _patch_webhooks_db(pg_conn), patch(
"application.api.user.agents.webhooks.settings.API_URL",
"docsgpt.api.user.agents.webhooks.settings.API_URL",
"https://api.test",
), app.test_request_context(
f"/api/agent_webhook?id={agent['id']}"
@@ -244,8 +244,8 @@ class TestAgentWebhookGet:
class TestAgentWebhookListener:
def test_post_valid_enqueues_task(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhookListener
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.storage.db.repositories.agents import AgentsRepository
from unittest.mock import MagicMock
user = "u-wh-enq"
@@ -255,7 +255,7 @@ class TestAgentWebhookListener:
fake_task = MagicMock(id="task-post")
with patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/webhooks/agents/tk-enq", method="POST",
@@ -272,8 +272,8 @@ class TestAgentWebhookListener:
assert response.json["task_id"] == "task-post"
def test_get_collects_query_params(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhookListener
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.storage.db.repositories.agents import AgentsRepository
from unittest.mock import MagicMock
user = "u-wh-get"
@@ -283,7 +283,7 @@ class TestAgentWebhookListener:
fake_task = MagicMock(id="task-get")
with patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
return_value=fake_task,
), app.test_request_context(
"/api/webhooks/agents/tk-get?foo=bar&baz=42"
@@ -19,15 +19,15 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.agents.webhooks.db_session", _yield
"docsgpt.api.user.agents.webhooks.db_session", _yield
), patch(
"application.api.user.agents.webhooks.db_readonly", _yield
"docsgpt.api.user.agents.webhooks.db_readonly", _yield
):
yield
def _seed_agent(pg_conn, user="u", token="tk", **kw):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
return AgentsRepository(pg_conn).create(
user, "a", "published", incoming_webhook_token=token, **kw,
)
@@ -42,13 +42,13 @@ def _apply_async_mock():
class TestWebhookIdempotency:
def test_no_header_enqueues_normally(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent = _seed_agent(pg_conn, user="u-noh", token="tk-noh")
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-noh", method="POST",
@@ -66,13 +66,13 @@ class TestWebhookIdempotency:
def test_header_first_post_records_row(self, app, pg_conn):
from sqlalchemy import text
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent = _seed_agent(pg_conn, user="u-first", token="tk-first")
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-first", method="POST",
@@ -105,13 +105,13 @@ class TestWebhookIdempotency:
"""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.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent = _seed_agent(pg_conn, user="u-fwd", token="tk-fwd")
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-fwd", method="POST",
@@ -133,13 +133,13 @@ class TestWebhookIdempotency:
)
def test_same_header_second_post_returns_cached(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent = _seed_agent(pg_conn, user="u-rep", token="tk-rep")
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
):
with app.test_request_context(
@@ -179,8 +179,8 @@ class TestWebhookIdempotency:
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
from application.api.user.agents.webhooks import AgentWebhookListener
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.storage.db.repositories.agents import AgentsRepository
with pg_engine.begin() as conn:
agent = AgentsRepository(conn).create(
@@ -218,13 +218,13 @@ class TestWebhookIdempotency:
# module-attribute patches once before fanning out so every
# thread sees the mock instead of racing on save/restore.
with patch(
"application.api.user.agents.webhooks.db_session",
"docsgpt.api.user.agents.webhooks.db_session",
_engine_session,
), patch(
"application.api.user.agents.webhooks.db_readonly",
"docsgpt.api.user.agents.webhooks.db_readonly",
_engine_readonly,
), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), ThreadPoolExecutor(max_workers=8) as ex:
responses = list(ex.map(fire, range(8)))
@@ -243,14 +243,14 @@ class TestWebhookIdempotency:
"""
from sqlalchemy import text as sql_text
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent_a = _seed_agent(pg_conn, user="u-a", token="tk-a")
agent_b = _seed_agent(pg_conn, user="u-b", token="tk-b")
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
):
with app.test_request_context(
@@ -296,13 +296,13 @@ class TestWebhookIdempotency:
def test_empty_header_treated_as_absent(self, app, pg_conn):
from sqlalchemy import text
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent = _seed_agent(pg_conn, user="u-empty", token="tk-empty")
apply_mock = _apply_async_mock()
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-empty", method="POST",
@@ -323,13 +323,13 @@ class TestWebhookIdempotency:
assert count == 0
def test_oversized_header_rejected_with_400(self, app, pg_conn):
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent = _seed_agent(pg_conn, user="u-big", token="tk-big")
oversized = "x" * 257
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
) as mock_apply, app.test_request_context(
"/api/webhooks/agents/tk-big", method="POST",
json={"event": "x"},
@@ -351,14 +351,14 @@ class TestWebhookIdempotency:
"""
from sqlalchemy import text
from application.api.user.agents.webhooks import AgentWebhookListener
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
agent = _seed_agent(pg_conn, user="u-stale", token="tk-stale")
apply_mock = _apply_async_mock()
# First POST creates a dedup row.
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-stale", method="POST",
@@ -388,7 +388,7 @@ class TestWebhookIdempotency:
# Second POST with the same key must enqueue again, not silently dedup.
with _patch_db(pg_conn), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
"docsgpt.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-stale", method="POST",
+34 -34
View File
@@ -26,22 +26,22 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.agents.routes.db_session", _yield
"docsgpt.api.user.agents.routes.db_session", _yield
), patch(
"application.api.user.agents.routes.db_readonly", _yield
"docsgpt.api.user.agents.routes.db_readonly", _yield
):
yield
def _seed_workflow(pg_conn, user, *, name="WF"):
"""Create a start → agent → end workflow; returns (workflow, nodes, edges)."""
from application.storage.db.repositories.workflow_edges import (
from docsgpt.storage.db.repositories.workflow_edges import (
WorkflowEdgesRepository,
)
from application.storage.db.repositories.workflow_nodes import (
from docsgpt.storage.db.repositories.workflow_nodes import (
WorkflowNodesRepository,
)
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
wf = WorkflowsRepository(pg_conn).create(user, name, description="d")
wf_id = str(wf["id"])
@@ -80,10 +80,10 @@ def _seed_workflow(pg_conn, user, *, name="WF"):
def _graph(pg_conn, workflow_id, version=1):
from application.storage.db.repositories.workflow_edges import (
from docsgpt.storage.db.repositories.workflow_edges import (
WorkflowEdgesRepository,
)
from application.storage.db.repositories.workflow_nodes import (
from docsgpt.storage.db.repositories.workflow_nodes import (
WorkflowNodesRepository,
)
@@ -94,7 +94,7 @@ def _graph(pg_conn, workflow_id, version=1):
class TestCloneToUser:
def test_clones_graph_for_new_owner(self, pg_conn):
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
wf, _, _ = _seed_workflow(pg_conn, "owner-a", name="Source WF")
clone = WorkflowsRepository(pg_conn).clone_to_user(
@@ -122,7 +122,7 @@ class TestCloneToUser:
assert len(src_nodes) == 3 and len(src_edges) == 2
def test_wrong_owner_returns_none(self, pg_conn):
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
wf, _, _ = _seed_workflow(pg_conn, "owner-a")
assert (
@@ -133,7 +133,7 @@ class TestCloneToUser:
)
def test_missing_workflow_returns_none(self, pg_conn):
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
assert (
WorkflowsRepository(pg_conn).clone_to_user(
@@ -145,9 +145,9 @@ class TestCloneToUser:
class TestAdoptWorkflowAgent:
def test_adopt_clones_the_graph(self, app, pg_conn):
from application.api.user.agents.routes import AdoptAgent
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.api.user.agents.routes import AdoptAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
wf, _, _ = _seed_workflow(pg_conn, "__system__", name="Template WF")
repo = AgentsRepository(pg_conn)
@@ -187,8 +187,8 @@ class TestAdoptWorkflowAgent:
Publishing the agent anyway would hand the adopter a workflow agent
with no graph — permanently unrunnable — so the adopt fails instead.
"""
from application.api.user.agents.routes import AdoptAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import AdoptAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
wf, _, _ = _seed_workflow(pg_conn, "someone-else")
repo = AgentsRepository(pg_conn)
@@ -219,16 +219,16 @@ class TestAdoptWorkflowAgent:
def test_adopt_strips_foreign_node_refs(self, app, pg_conn):
"""Cloned agent nodes keep builtin tool ids but shed the owner's refs."""
from application.agents.default_tools import default_tool_id
from application.api.user.agents.routes import AdoptAgent
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.user_tools import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.agents.routes import AdoptAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
from application.storage.db.repositories.workflow_nodes import (
from docsgpt.storage.db.repositories.workflow_nodes import (
WorkflowNodesRepository,
)
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
owner_tool = UserToolsRepository(pg_conn).create(
"__system__", "brave", config={}, display_name="Brave"
@@ -291,9 +291,9 @@ class TestAdoptWorkflowAgent:
class TestDeleteWorkflowAgent:
def test_delete_removes_owned_workflow(self, app, pg_conn):
from application.api.user.agents.routes import DeleteAgent
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.api.user.agents.routes import DeleteAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
user = "u-wf-del"
wf, _, _ = _seed_workflow(pg_conn, user)
@@ -319,9 +319,9 @@ class TestDeleteWorkflowAgent:
"""Regression: deleting an agent whose ``workflow_id`` points at another
user's workflow (the pre-clone adopted shape) must not touch that graph.
The old explicit node/edge cleanup was not owner-scoped and gutted it."""
from application.api.user.agents.routes import DeleteAgent
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.workflows import WorkflowsRepository
from docsgpt.api.user.agents.routes import DeleteAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.workflows import WorkflowsRepository
wf, _, _ = _seed_workflow(pg_conn, "__system__", name="Shared Template WF")
repo = AgentsRepository(pg_conn)
@@ -350,8 +350,8 @@ class TestDeleteWorkflowAgent:
class TestCreateWorkflowAgent:
def test_create_published_with_owned_workflow(self, app, pg_conn):
from application.api.user.agents.routes import CreateAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import CreateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-wf-create"
wf, _, _ = _seed_workflow(pg_conn, user)
@@ -381,7 +381,7 @@ class TestCreateWorkflowAgent:
assert agent["status"] == "published"
def test_create_published_with_unowned_workflow_returns_404(self, app, pg_conn):
from application.api.user.agents.routes import CreateAgent
from docsgpt.api.user.agents.routes import CreateAgent
wf, _, _ = _seed_workflow(pg_conn, "someone-else")
@@ -405,8 +405,8 @@ class TestCreateWorkflowAgent:
class TestUpdateWorkflowAgent:
def test_update_sets_and_clears_workflow(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import UpdateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-wf-update"
wf, _, _ = _seed_workflow(pg_conn, user)
@@ -440,8 +440,8 @@ class TestUpdateWorkflowAgent:
assert repo.get(agent_id, user)["workflow_id"] is None
def test_publish_without_workflow_is_rejected(self, app, pg_conn):
from application.api.user.agents.routes import UpdateAgent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.agents.routes import UpdateAgent
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-wf-update2"
agent = AgentsRepository(pg_conn).create(
File diff suppressed because it is too large. Load diff
+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",
+79 -79
View File
@@ -1,4 +1,4 @@
"""Tests for application.api.user.agents.sharing module."""
"""Tests for docsgpt.api.user.agents.sharing module."""
import uuid
from unittest.mock import Mock, patch
@@ -27,27 +27,27 @@ def app():
class TestSharedAgent:
def test_returns_400_missing_token(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
with app.test_request_context("/api/shared_agent"):
response = SharedAgent().get()
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
mock_col = Mock()
mock_col.find_one.return_value = None
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context("/api/shared_agent?token=abc123"):
response = SharedAgent().get()
assert response.status_code == 404
def test_returns_shared_agent_data(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
agent_id = uuid.uuid4().hex
mock_agents_col = Mock()
@@ -69,11 +69,11 @@ class TestSharedAgent:
mock_db = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
"docsgpt.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=abc123"):
from flask import request
@@ -88,7 +88,7 @@ class TestSharedAgent:
assert data["shared"] is True
def test_adds_to_shared_with_me_for_different_user(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
agent_id = uuid.uuid4().hex
mock_agents_col = Mock()
@@ -106,15 +106,15 @@ class TestSharedAgent:
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
"docsgpt.api.user.agents.sharing.db", mock_db
), patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
"docsgpt.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
"docsgpt.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agent?token=abc123"):
from flask import request
@@ -126,7 +126,7 @@ class TestSharedAgent:
mock_users_col.update_one.assert_called_once()
def test_does_not_add_to_shared_for_owner(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
agent_id = uuid.uuid4().hex
mock_agents_col = Mock()
@@ -144,15 +144,15 @@ class TestSharedAgent:
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
"docsgpt.api.user.agents.sharing.db", mock_db
), patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
"docsgpt.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
"docsgpt.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agent?token=abc123"):
from flask import request
@@ -164,7 +164,7 @@ class TestSharedAgent:
mock_users_col.update_one.assert_not_called()
def test_enriches_tool_names(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
agent_id = uuid.uuid4().hex
tool_id = str(uuid.uuid4().hex)
@@ -186,13 +186,13 @@ class TestSharedAgent:
mock_db = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.user_tools_collection", mock_tools_col
"docsgpt.api.user.agents.sharing.user_tools_collection", mock_tools_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
"docsgpt.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
@@ -203,7 +203,7 @@ class TestSharedAgent:
assert response.json["tools"] == ["calculator"]
def test_handles_source_dbref(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
agent_id = uuid.uuid4().hex
source_id = uuid.uuid4().hex
@@ -223,11 +223,11 @@ class TestSharedAgent:
mock_db.dereference.return_value = {"_id": source_id}
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
"docsgpt.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
@@ -238,20 +238,20 @@ class TestSharedAgent:
assert response.json["source"] == str(source_id)
def test_returns_400_on_exception(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
mock_col = Mock()
mock_col.find_one.side_effect = Exception("DB error")
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context("/api/shared_agent?token=tok"):
response = SharedAgent().get()
assert response.status_code == 400
def test_tool_enrichment_handles_missing_tool(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
agent_id = uuid.uuid4().hex
tool_id = str(uuid.uuid4().hex)
@@ -270,13 +270,13 @@ class TestSharedAgent:
mock_db = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.user_tools_collection", mock_tools_col
"docsgpt.api.user.agents.sharing.user_tools_collection", mock_tools_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
"docsgpt.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
@@ -288,7 +288,7 @@ class TestSharedAgent:
assert response.json["tools"] == []
def test_image_url_generated_when_present(self, app):
from application.api.user.agents.sharing import SharedAgent
from docsgpt.api.user.agents.sharing import SharedAgent
agent_id = uuid.uuid4().hex
mock_agents_col = Mock()
@@ -306,13 +306,13 @@ class TestSharedAgent:
mock_generate = Mock(return_value="http://example.com/img.png")
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
"docsgpt.api.user.agents.sharing.db", mock_db
), patch(
"application.api.user.agents.sharing.generate_image_url", mock_generate
"docsgpt.api.user.agents.sharing.generate_image_url", mock_generate
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
@@ -333,7 +333,7 @@ class TestSharedAgent:
class TestSharedAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
with app.test_request_context("/api/shared_agents"):
from flask import request
@@ -343,7 +343,7 @@ class TestSharedAgents:
assert response.status_code == 401
def test_returns_shared_agents_list(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
agent_id = uuid.uuid4().hex
mock_ensure = Mock(
@@ -372,13 +372,13 @@ class TestSharedAgents:
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
"docsgpt.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
"docsgpt.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agents"):
from flask import request
@@ -392,7 +392,7 @@ class TestSharedAgents:
assert data[0]["pinned"] is True
def test_removes_stale_shared_ids(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
stale_id = str(uuid.uuid4().hex)
mock_ensure = Mock(
@@ -409,11 +409,11 @@ class TestSharedAgents:
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
"docsgpt.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
"docsgpt.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agents"):
from flask import request
@@ -428,7 +428,7 @@ class TestSharedAgents:
]
def test_returns_empty_when_no_shared_ids(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
mock_ensure = Mock(
return_value={
@@ -440,9 +440,9 @@ class TestSharedAgents:
mock_agents_col.find.return_value = []
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
"docsgpt.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
):
with app.test_request_context("/api/shared_agents"):
from flask import request
@@ -453,12 +453,12 @@ class TestSharedAgents:
assert response.json == []
def test_returns_400_on_exception(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
mock_ensure = Mock(side_effect=Exception("DB error"))
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
"docsgpt.api.user.agents.sharing.ensure_user_doc", mock_ensure
):
with app.test_request_context("/api/shared_agents"):
from flask import request
@@ -468,7 +468,7 @@ class TestSharedAgents:
assert response.status_code == 400
def test_image_url_generated(self, app):
from application.api.user.agents.sharing import SharedAgents
from docsgpt.api.user.agents.sharing import SharedAgents
agent_id = uuid.uuid4().hex
mock_ensure = Mock(
@@ -494,15 +494,15 @@ class TestSharedAgents:
mock_generate = Mock(return_value="http://example.com/path.png")
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
"docsgpt.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
"docsgpt.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.generate_image_url", mock_generate
"docsgpt.api.user.agents.sharing.generate_image_url", mock_generate
), patch(
"application.api.user.agents.sharing.users_collection", Mock()
"docsgpt.api.user.agents.sharing.users_collection", Mock()
):
with app.test_request_context("/api/shared_agents"):
from flask import request
@@ -522,7 +522,7 @@ class TestSharedAgents:
class TestShareAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
@@ -536,7 +536,7 @@ class TestShareAgent:
assert response.status_code == 401
def test_returns_400_missing_json_body(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
@@ -553,7 +553,7 @@ class TestShareAgent:
assert response.json["success"] is False
def test_returns_400_missing_id(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
@@ -567,7 +567,7 @@ class TestShareAgent:
assert response.status_code == 400
def test_returns_400_missing_shared_param(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
@@ -581,7 +581,7 @@ class TestShareAgent:
assert response.status_code == 400
def test_returns_400_invalid_agent_id(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
@@ -595,14 +595,14 @@ class TestShareAgent:
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
mock_col = Mock()
mock_col.find_one.return_value = None
agent_id = str(uuid.uuid4().hex)
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
@@ -616,7 +616,7 @@ class TestShareAgent:
assert response.status_code == 404
def test_shares_agent_success(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
agent_id = uuid.uuid4().hex
mock_col = Mock()
@@ -627,7 +627,7 @@ class TestShareAgent:
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
@@ -649,7 +649,7 @@ class TestShareAgent:
mock_col.update_one.assert_called_once()
def test_unshares_agent_success(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
agent_id = uuid.uuid4().hex
mock_col = Mock()
@@ -660,7 +660,7 @@ class TestShareAgent:
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
@@ -680,7 +680,7 @@ class TestShareAgent:
assert data["shared_token"] is None
def test_returns_400_on_db_exception(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
agent_id = uuid.uuid4().hex
mock_col = Mock()
@@ -691,7 +691,7 @@ class TestShareAgent:
mock_col.update_one.side_effect = Exception("DB error")
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
@@ -708,7 +708,7 @@ class TestShareAgent:
assert response.status_code == 400
def test_share_with_username(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
agent_id = uuid.uuid4().hex
mock_col = Mock()
@@ -719,7 +719,7 @@ class TestShareAgent:
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
@@ -742,7 +742,7 @@ class TestShareAgent:
assert "shared_token" in update_call
def test_shared_false_explicitly(self, app):
from application.api.user.agents.sharing import ShareAgent
from docsgpt.api.user.agents.sharing import ShareAgent
agent_id = uuid.uuid4().hex
mock_col = Mock()
@@ -753,7 +753,7 @@ class TestShareAgent:
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
"docsgpt.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
+81 -81
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/analytics/routes.py.
"""Tests for docsgpt/api/user/analytics/routes.py.
Uses the ephemeral ``pg_conn`` fixture so analytics SQL runs against a real
(in-memory) Postgres schema.
@@ -24,7 +24,7 @@ def _patch_analytics_db(conn):
yield conn
with patch(
"application.api.user.analytics.routes.db_readonly", _yield_conn
"docsgpt.api.user.analytics.routes.db_readonly", _yield_conn
):
yield
@@ -33,7 +33,7 @@ def _seed_conversation_with_messages(
pg_conn, user_id, *, count=3, api_key=None, feedback_text=None,
agent_id=None,
):
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
repo = ConversationsRepository(pg_conn)
@@ -57,7 +57,7 @@ def _seed_conversation_with_messages(
class TestRangeForFilter:
def test_returns_none_for_invalid_filter(self):
from application.api.user.analytics.routes import _range_for_filter
from docsgpt.api.user.analytics.routes import _range_for_filter
assert _range_for_filter("bogus") is None
@@ -66,7 +66,7 @@ class TestRangeForFilter:
["last_hour", "last_24_hour", "last_7_days", "last_15_days", "last_30_days"],
)
def test_returns_start_end_for_supported(self, option):
from application.api.user.analytics.routes import _range_for_filter
from docsgpt.api.user.analytics.routes import _range_for_filter
got = _range_for_filter(option)
assert got is not None
@@ -77,14 +77,14 @@ class TestRangeForFilter:
class TestResolveAgent:
def test_no_agent_when_no_id(self, pg_conn):
from application.api.user.analytics.routes import _resolve_agent
from docsgpt.api.user.analytics.routes import _resolve_agent
assert _resolve_agent(pg_conn, None, "u") == (None, None, None)
assert _resolve_agent(pg_conn, "", "u") == (None, None, None)
def test_returns_key_and_id_for_owned_agent(self, pg_conn):
from application.api.user.analytics.routes import _resolve_agent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.analytics.routes import _resolve_agent
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "test-agent", "published", key="secret-api-key",
@@ -97,8 +97,8 @@ class TestResolveAgent:
assert agent_pg_id == str(agent["id"])
def test_keyless_agent_yields_none_key(self, pg_conn):
from application.api.user.analytics.routes import _resolve_agent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.analytics.routes import _resolve_agent
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "test-agent", "draft", key="",
@@ -114,8 +114,8 @@ class TestResolveAgent:
assert agent_pg_id == str(agent["id"])
def test_no_match_for_other_users_agent(self, pg_conn):
from application.api.user.analytics.routes import _resolve_agent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.analytics.routes import _resolve_agent
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "test-agent", "published", key="secret"
@@ -129,7 +129,7 @@ class TestResolveAgent:
class TestGetMessageAnalytics:
def test_returns_401_unauthenticated(self, app):
from application.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
with app.test_request_context(
"/api/get_message_analytics", method="POST", json={}
@@ -140,7 +140,7 @@ class TestGetMessageAnalytics:
assert response.status_code == 401
def test_invalid_filter_returns_400(self, app):
from application.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
with app.test_request_context(
"/api/get_message_analytics",
@@ -153,7 +153,7 @@ class TestGetMessageAnalytics:
assert response.status_code == 400
def test_returns_bucketed_counts(self, app, pg_conn):
from application.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
user = "u-msg"
_seed_conversation_with_messages(pg_conn, user, count=3)
@@ -173,8 +173,8 @@ class TestGetMessageAnalytics:
assert sum(messages.values()) == 3
def test_filters_by_api_key(self, app, pg_conn):
from application.api.user.analytics.routes import GetMessageAnalytics
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.storage.db.repositories.agents import AgentsRepository
user = "u-msg-key"
agent = AgentsRepository(pg_conn).create(
@@ -195,7 +195,7 @@ class TestGetMessageAnalytics:
assert sum(response.json["messages"].values()) == 4
def test_db_error_returns_400(self, app):
from application.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
@contextmanager
def _broken():
@@ -203,7 +203,7 @@ class TestGetMessageAnalytics:
yield
with patch(
"application.api.user.analytics.routes.db_readonly", _broken
"docsgpt.api.user.analytics.routes.db_readonly", _broken
), app.test_request_context(
"/api/get_message_analytics", method="POST", json={}
):
@@ -215,7 +215,7 @@ class TestGetMessageAnalytics:
class TestGetTokenAnalytics:
def test_returns_401_unauthenticated(self, app):
from application.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
with app.test_request_context(
"/api/get_token_analytics", method="POST", json={}
@@ -226,7 +226,7 @@ class TestGetTokenAnalytics:
assert response.status_code == 401
def test_invalid_filter_returns_400(self, app):
from application.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
with app.test_request_context(
"/api/get_token_analytics",
@@ -239,7 +239,7 @@ class TestGetTokenAnalytics:
assert response.status_code == 400
def test_returns_token_usage_shape(self, app, pg_conn):
from application.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
with _patch_analytics_db(pg_conn), app.test_request_context(
"/api/get_token_analytics",
@@ -254,7 +254,7 @@ class TestGetTokenAnalytics:
assert isinstance(response.json["token_usage"], dict)
def test_db_error_returns_400(self, app):
from application.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
@contextmanager
def _broken():
@@ -262,7 +262,7 @@ class TestGetTokenAnalytics:
yield
with patch(
"application.api.user.analytics.routes.db_readonly", _broken
"docsgpt.api.user.analytics.routes.db_readonly", _broken
), app.test_request_context(
"/api/get_token_analytics", method="POST", json={}
):
@@ -274,7 +274,7 @@ class TestGetTokenAnalytics:
class TestGetFeedbackAnalytics:
def test_returns_401_unauthenticated(self, app):
from application.api.user.analytics.routes import GetFeedbackAnalytics
from docsgpt.api.user.analytics.routes import GetFeedbackAnalytics
with app.test_request_context(
"/api/get_feedback_analytics", method="POST", json={}
@@ -285,7 +285,7 @@ class TestGetFeedbackAnalytics:
assert response.status_code == 401
def test_invalid_filter_returns_400(self, app):
from application.api.user.analytics.routes import GetFeedbackAnalytics
from docsgpt.api.user.analytics.routes import GetFeedbackAnalytics
with app.test_request_context(
"/api/get_feedback_analytics",
@@ -298,7 +298,7 @@ class TestGetFeedbackAnalytics:
assert response.status_code == 400
def test_returns_positive_and_negative_counts(self, app, pg_conn):
from application.api.user.analytics.routes import GetFeedbackAnalytics
from docsgpt.api.user.analytics.routes import GetFeedbackAnalytics
user = "u-fb"
_seed_conversation_with_messages(
@@ -321,7 +321,7 @@ class TestGetFeedbackAnalytics:
assert total_neg == 2
def test_db_error_returns_400(self, app):
from application.api.user.analytics.routes import GetFeedbackAnalytics
from docsgpt.api.user.analytics.routes import GetFeedbackAnalytics
@contextmanager
def _broken():
@@ -329,7 +329,7 @@ class TestGetFeedbackAnalytics:
yield
with patch(
"application.api.user.analytics.routes.db_readonly", _broken
"docsgpt.api.user.analytics.routes.db_readonly", _broken
), app.test_request_context(
"/api/get_feedback_analytics", method="POST", json={}
):
@@ -341,7 +341,7 @@ class TestGetFeedbackAnalytics:
class TestGetUserLogs:
def test_returns_401_unauthenticated(self, app):
from application.api.user.analytics.routes import GetUserLogs
from docsgpt.api.user.analytics.routes import GetUserLogs
with app.test_request_context(
"/api/get_user_logs", method="POST", json={}
@@ -352,8 +352,8 @@ class TestGetUserLogs:
assert response.status_code == 401
def test_returns_logs_list_paginated(self, app, pg_conn):
from application.api.user.analytics.routes import GetUserLogs
from application.storage.db.repositories.user_logs import (
from docsgpt.api.user.analytics.routes import GetUserLogs
from docsgpt.storage.db.repositories.user_logs import (
UserLogsRepository,
)
@@ -382,9 +382,9 @@ class TestGetUserLogs:
assert data["page_size"] == 10
def test_filters_logs_by_api_key(self, app, pg_conn):
from application.api.user.analytics.routes import GetUserLogs
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.user_logs import (
from docsgpt.api.user.analytics.routes import GetUserLogs
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.user_logs import (
UserLogsRepository,
)
@@ -419,9 +419,9 @@ class TestGetUserLogs:
"""A webhook activity-log row stamped with the agent's id but an
*old* api_key (the state after a key rotation) must still surface on
the agent's timeline via the stable agent_id join."""
from application.api.user.analytics.routes import GetUserLogs
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.stack_logs import (
from docsgpt.api.user.analytics.routes import GetUserLogs
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.stack_logs import (
StackLogsRepository,
)
@@ -463,7 +463,7 @@ class TestGetUserLogs:
assert logs[0]["id"] # the rotated-away row surfaced via agent_id
def test_db_error_returns_400(self, app):
from application.api.user.analytics.routes import GetUserLogs
from docsgpt.api.user.analytics.routes import GetUserLogs
@contextmanager
def _broken():
@@ -471,7 +471,7 @@ class TestGetUserLogs:
yield
with patch(
"application.api.user.analytics.routes.db_readonly", _broken
"docsgpt.api.user.analytics.routes.db_readonly", _broken
), app.test_request_context(
"/api/get_user_logs", method="POST", json={}
):
@@ -487,7 +487,7 @@ def _seed_stack_log(
):
import uuid as _uuid
from application.storage.db.repositories.stack_logs import (
from docsgpt.storage.db.repositories.stack_logs import (
StackLogsRepository,
)
@@ -503,7 +503,7 @@ def _seed_stack_log(
def _post_logs(app, pg_conn, user, body):
from application.api.user.analytics.routes import GetUserLogs
from docsgpt.api.user.analytics.routes import GetUserLogs
with _patch_analytics_db(pg_conn), app.test_request_context(
"/api/get_user_logs", method="POST", json=body
@@ -538,7 +538,7 @@ class TestCrossTenantIsolation:
assert response.json["has_more"] is False
def test_own_draft_agent_leaks_nothing(self, app, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
_seed_stack_log(pg_conn, user_id="victim", api_key="")
# Draft agents legitimately store key='' — filtering by one must
@@ -557,9 +557,9 @@ class TestCrossTenantIsolation:
assert response.json["logs"] == []
def test_other_users_agent_id_returns_empty_tokens(self, app, pg_conn):
from application.api.user.analytics.routes import GetTokenAnalytics
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.token_usage import (
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -589,7 +589,7 @@ class TestCrossTenantIsolation:
class TestGetToolAnalytics:
def _post(self, app, pg_conn, user, body):
from application.api.user.analytics.routes import GetToolAnalytics
from docsgpt.api.user.analytics.routes import GetToolAnalytics
with _patch_analytics_db(pg_conn), app.test_request_context(
"/api/get_tool_analytics", method="POST", json=body
@@ -599,7 +599,7 @@ class TestGetToolAnalytics:
return GetToolAnalytics().post()
def test_counts_terminal_attempts_only(self, app, pg_conn):
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -631,8 +631,8 @@ class TestGetToolAnalytics:
assert response.json["tools"] == []
def test_filters_by_agent_stamp(self, app, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.tool_call_attempts import (
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.tool_call_attempts import (
ToolCallAttemptsRepository,
)
@@ -660,10 +660,10 @@ class TestGetScheduleAnalytics:
def _seed_run(self, pg_conn, user, status, *, agent_id=None):
import datetime as _dt
from application.storage.db.repositories.schedule_runs import (
from docsgpt.storage.db.repositories.schedule_runs import (
ScheduleRunsRepository,
)
from application.storage.db.repositories.schedules import (
from docsgpt.storage.db.repositories.schedules import (
SchedulesRepository,
)
@@ -683,7 +683,7 @@ class TestGetScheduleAnalytics:
return schedule
def _post(self, app, pg_conn, user, body):
from application.api.user.analytics.routes import GetScheduleAnalytics
from docsgpt.api.user.analytics.routes import GetScheduleAnalytics
with _patch_analytics_db(pg_conn), app.test_request_context(
"/api/get_schedule_analytics", method="POST", json=body
@@ -765,7 +765,7 @@ class TestUnifiedLogsBranches:
assert response.json["logs"][0]["level"] == "info"
def test_level_filter_applies_per_branch(self, app, pg_conn):
from application.storage.db.repositories.user_logs import (
from docsgpt.storage.db.repositories.user_logs import (
UserLogsRepository,
)
@@ -830,8 +830,8 @@ class TestUnifiedLogsBranches:
class TestTokenAnalyticsParamCoercion:
def test_string_false_disables_side_channel(self, app, pg_conn):
from application.api.user.analytics.routes import GetTokenAnalytics
from application.storage.db.repositories.token_usage import (
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -874,7 +874,7 @@ class TestUnknownAgentShortCircuits:
def test_message_analytics_returns_zeroes(self, app, pg_conn):
import uuid as _uuid
from application.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
_seed_conversation_with_messages(pg_conn, "u-msg", count=2)
response = _post_resource(
@@ -887,7 +887,7 @@ class TestUnknownAgentShortCircuits:
def test_feedback_analytics_returns_zeroes(self, app, pg_conn):
import uuid as _uuid
from application.api.user.analytics.routes import GetFeedbackAnalytics
from docsgpt.api.user.analytics.routes import GetFeedbackAnalytics
_seed_conversation_with_messages(
pg_conn, "u-fb0", count=2, feedback_text="like"
@@ -906,7 +906,7 @@ class TestUnknownAgentShortCircuits:
class TestMessageAnalyticsBuckets:
@pytest.mark.parametrize("option", ["last_hour", "last_24_hour"])
def test_minute_and_hour_buckets(self, app, pg_conn, option):
from application.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
_seed_conversation_with_messages(pg_conn, "u-bkt", count=2)
response = _post_resource(
@@ -919,8 +919,8 @@ class TestMessageAnalyticsBuckets:
class TestFeedbackAnalyticsAgentFilter:
def test_filters_by_agent_key_or_id(self, app, pg_conn):
from application.api.user.analytics.routes import GetFeedbackAnalytics
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.analytics.routes import GetFeedbackAnalytics
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"u-fb", "fb-agent", "published", key="fb-key",
@@ -948,8 +948,8 @@ class TestFeedbackAnalyticsAgentFilter:
class TestTokenAnalyticsGrouping:
def test_group_by_model_returns_series(self, app, pg_conn):
from application.api.user.analytics.routes import GetTokenAnalytics
from application.storage.db.repositories.token_usage import (
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -974,9 +974,9 @@ class TestTokenAnalyticsGrouping:
assert sum(series["unknown"].values()) == 3
def test_filters_by_owned_agent_key_or_id(self, app, pg_conn):
from application.api.user.analytics.routes import GetTokenAnalytics
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.token_usage import (
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -1007,7 +1007,7 @@ class TestTokenAnalyticsGrouping:
class TestScheduleAnalyticsAgentFilter:
def test_filters_by_owned_agent(self, app, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
helper = TestGetScheduleAnalytics()
agent = AgentsRepository(pg_conn).create(
@@ -1029,7 +1029,7 @@ class TestScheduleAnalyticsAgentFilter:
class TestUnifiedLogsFilters:
def test_search_matches_summary(self, app, pg_conn):
from application.storage.db.repositories.user_logs import (
from docsgpt.storage.db.repositories.user_logs import (
UserLogsRepository,
)
@@ -1050,7 +1050,7 @@ class TestUnifiedLogsFilters:
assert "whales" in logs[0]["question"]
def test_search_escapes_like_wildcards(self, app, pg_conn):
from application.storage.db.repositories.user_logs import (
from docsgpt.storage.db.repositories.user_logs import (
UserLogsRepository,
)
@@ -1080,7 +1080,7 @@ class TestNewEndpointGuards:
],
)
def test_returns_401_unauthenticated(self, app, resource_name, path):
import application.api.user.analytics.routes as routes
import docsgpt.api.user.analytics.routes as routes
resource_cls = getattr(routes, resource_name)
with app.test_request_context(path, method="POST", json={}):
@@ -1097,7 +1097,7 @@ class TestNewEndpointGuards:
],
)
def test_invalid_filter_returns_400(self, app, resource_name, path):
import application.api.user.analytics.routes as routes
import docsgpt.api.user.analytics.routes as routes
resource_cls = getattr(routes, resource_name)
with app.test_request_context(
@@ -1111,9 +1111,9 @@ class TestNewEndpointGuards:
class TestTokenAnalyticsGroupByAgent:
def test_group_by_agent_resolves_names(self, app, pg_conn):
from application.api.user.analytics.routes import GetTokenAnalytics
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.token_usage import (
from docsgpt.api.user.analytics.routes import GetTokenAnalytics
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.token_usage import (
TokenUsageRepository,
)
@@ -1146,7 +1146,7 @@ class TestNewEndpointDbErrors:
],
)
def test_db_error_returns_400(self, app, resource_name, path):
import application.api.user.analytics.routes as routes
import docsgpt.api.user.analytics.routes as routes
resource_cls = getattr(routes, resource_name)
@@ -1156,7 +1156,7 @@ class TestNewEndpointDbErrors:
yield
with patch(
"application.api.user.analytics.routes.db_readonly", _broken
"docsgpt.api.user.analytics.routes.db_readonly", _broken
), app.test_request_context(path, method="POST", json={}):
from flask import request
request.decoded_token = {"sub": "u"}
@@ -1168,10 +1168,10 @@ class TestUnifiedLogsWorkflowBranch:
def test_workflow_runs_appear_with_payload(self, app, pg_conn):
import datetime as _dt
from application.storage.db.repositories.workflow_runs import (
from docsgpt.storage.db.repositories.workflow_runs import (
WorkflowRunsRepository,
)
from application.storage.db.repositories.workflows import (
from docsgpt.storage.db.repositories.workflows import (
WorkflowsRepository,
)
@@ -1208,14 +1208,14 @@ class TestSharedAgentVisibility:
populations)."""
def _owned_agent(self, pg_conn, owner):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
return AgentsRepository(pg_conn).create(
owner, "shared-agent", "published", key="shared-key",
)
def test_message_analytics_includes_shared_caller(self, app, pg_conn):
from application.api.user.analytics.routes import GetMessageAnalytics
from docsgpt.api.user.analytics.routes import GetMessageAnalytics
agent = self._owned_agent(pg_conn, "owner-a")
# Caller B chats with A's shared agent: conversation.user_id = B,
@@ -1231,7 +1231,7 @@ class TestSharedAgentVisibility:
assert sum(response.json["messages"].values()) == 3
def test_feedback_analytics_includes_shared_caller(self, app, pg_conn):
from application.api.user.analytics.routes import GetFeedbackAnalytics
from docsgpt.api.user.analytics.routes import GetFeedbackAnalytics
agent = self._owned_agent(pg_conn, "owner-a")
_seed_conversation_with_messages(
+77 -77
View File
@@ -10,14 +10,14 @@ import pytest
from flask import request
from sqlalchemy import text
from application.api.user.artifacts import authz
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.artifacts import ArtifactsRepository
from application.storage.db.repositories.conversations import ConversationsRepository
from application.storage.db.repositories.shared_conversations import (
from docsgpt.api.user.artifacts import authz
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
from docsgpt.storage.db.repositories.conversations import ConversationsRepository
from docsgpt.storage.db.repositories.shared_conversations import (
SharedConversationsRepository,
)
from application.storage.db.repositories.workflow_runs import WorkflowRunsRepository
from docsgpt.storage.db.repositories.workflow_runs import WorkflowRunsRepository
OWNER = "artifact_owner"
@@ -33,8 +33,8 @@ def _patch_db(pg_conn, monkeypatch):
def _use_conn():
yield pg_conn
monkeypatch.setattr("application.api.user.artifacts.routes.db_readonly", _use_conn)
monkeypatch.setattr("application.api.user.artifacts.routes.db_session", _use_conn)
monkeypatch.setattr("docsgpt.api.user.artifacts.routes.db_readonly", _use_conn)
monkeypatch.setattr("docsgpt.api.user.artifacts.routes.db_session", _use_conn)
return pg_conn
@@ -110,7 +110,7 @@ class TestListArtifacts:
def test_owner_lists_conversation_artifacts(
self, _patch_db, flask_app, token_owner
):
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
conv = _make_conversation(_patch_db)
conv_id = str(conv["id"])
@@ -127,7 +127,7 @@ class TestListArtifacts:
def test_stranger_denied_conversation_list(
self, _patch_db, flask_app
):
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
conv = _make_conversation(_patch_db)
_make_artifact(_patch_db, conversation_id=str(conv["id"]))
@@ -140,7 +140,7 @@ class TestListArtifacts:
assert resp.status_code == 403
def test_no_filter_scopes_to_user(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
owner_conv = _make_conversation(_patch_db)
owned = _make_artifact(
@@ -160,13 +160,13 @@ class TestListArtifacts:
assert str(stranger["id"]) not in returned_ids
def test_unauthenticated_401(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
resp = _call(flask_app, ListArtifacts, token=None)
assert resp.status_code == 401
def test_non_uuid_conversation_id_400(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
# A malformed id must be rejected before reaching CAST(:id AS uuid),
# which would otherwise raise a DataError and poison the transaction.
@@ -177,7 +177,7 @@ class TestListArtifacts:
assert resp.status_code == 400
def test_non_uuid_workflow_run_id_400(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
resp = _call(
flask_app, ListArtifacts, token=token_owner,
@@ -194,7 +194,7 @@ class TestGetArtifact:
def test_owner_gets_artifact_with_versions(
self, _patch_db, flask_app, token_owner
):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
conv = _make_conversation(_patch_db)
art = _make_artifact(
@@ -209,7 +209,7 @@ class TestGetArtifact:
assert resp.json["artifact"]["spec"] == {"body": "v2"}
def test_stranger_denied_403(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
conv = _make_conversation(_patch_db)
art = _make_artifact(_patch_db, conversation_id=str(conv["id"]))
@@ -218,7 +218,7 @@ class TestGetArtifact:
assert resp.status_code == 403
def test_missing_parent_fails_closed(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
from sqlalchemy import text
conv = _make_conversation(_patch_db)
@@ -233,13 +233,13 @@ class TestGetArtifact:
assert resp.status_code == 403
def test_unknown_artifact_404(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
resp = _call(flask_app, GetArtifact, str(uuid.uuid4()), token=token_owner)
assert resp.status_code == 404
def test_workflow_run_owner_access(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
wf_id = _make_workflow(_patch_db, OWNER)
run = WorkflowRunsRepository(_patch_db).create(wf_id, OWNER, "completed")
@@ -257,7 +257,7 @@ class TestGetArtifact:
@pytest.mark.unit
class TestGetArtifactVersion:
def test_version_returns_spec(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import GetArtifactVersion
from docsgpt.api.user.artifacts.routes import GetArtifactVersion
conv = _make_conversation(_patch_db)
art = _make_artifact(
@@ -268,7 +268,7 @@ class TestGetArtifactVersion:
assert resp.json["version"]["spec"] == {"body": "one"}
def test_missing_version_404(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import GetArtifactVersion
from docsgpt.api.user.artifacts.routes import GetArtifactVersion
conv = _make_conversation(_patch_db)
art = _make_artifact(_patch_db, conversation_id=str(conv["id"]))
@@ -282,7 +282,7 @@ class TestGetArtifactVersion:
@pytest.mark.unit
class TestSharedAccess:
def test_shared_with_user_can_get(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
conv = _make_conversation(_patch_db)
ConversationsRepository(_patch_db).add_shared_user(str(conv["id"]), SHARED_USER)
@@ -292,7 +292,7 @@ class TestSharedAccess:
assert resp.status_code == 200
def test_share_token_holder_can_download(self, _patch_db, flask_app, monkeypatch):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
conv = _make_conversation(_patch_db)
# Attach to the first message so it falls inside the first_n_queries snapshot.
@@ -311,11 +311,11 @@ class TestSharedAccess:
storage = _FakeStorage(b"PDFDATA")
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"backend",
raising=False,
)
@@ -365,17 +365,17 @@ class TestShareTokenSnapshotScope:
def _mock_storage(monkeypatch, data=b"BYTES"):
storage = _FakeStorage(data)
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"backend", raising=False,
)
return storage
def test_share_token_list_only_snapshot(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
conv_id, in_art, out_art, null_art, token = self._seed(_patch_db)
resp = _call(
@@ -389,7 +389,7 @@ class TestShareTokenSnapshotScope:
assert str(null_art["id"]) not in ids # NULL message_id -> not in snapshot
def test_share_token_get_in_snapshot_200(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
_, in_art, _out, _null, token = self._seed(_patch_db)
resp = _call(
@@ -399,7 +399,7 @@ class TestShareTokenSnapshotScope:
assert resp.status_code == 200
def test_share_token_get_out_of_snapshot_403(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
_, _in, out_art, _null, token = self._seed(_patch_db)
resp = _call(
@@ -409,7 +409,7 @@ class TestShareTokenSnapshotScope:
assert resp.status_code == 403
def test_share_token_null_message_id_denied(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
_, _in, _out, null_art, token = self._seed(_patch_db)
resp = _call(
@@ -421,7 +421,7 @@ class TestShareTokenSnapshotScope:
def test_share_token_download_in_snapshot_200(
self, _patch_db, flask_app, monkeypatch
):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
_, in_art, _out, _null, token = self._seed(_patch_db)
self._mock_storage(monkeypatch, b"INDATA")
@@ -435,7 +435,7 @@ class TestShareTokenSnapshotScope:
def test_share_token_download_out_of_snapshot_403(
self, _patch_db, flask_app, monkeypatch
):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
_, _in, out_art, _null, token = self._seed(_patch_db)
self._mock_storage(monkeypatch, b"OUTDATA")
@@ -448,7 +448,7 @@ class TestShareTokenSnapshotScope:
def test_owner_sees_all_artifacts(self, _patch_db, flask_app, token_owner):
# Snapshot scoping is share-token-only: the owner lists every artifact and
# can fetch one attached to a message outside the first_n_queries snapshot.
from application.api.user.artifacts.routes import GetArtifact, ListArtifacts
from docsgpt.api.user.artifacts.routes import GetArtifact, ListArtifacts
conv_id, in_art, out_art, null_art, _token = self._seed(_patch_db)
listed = _call(
@@ -464,7 +464,7 @@ class TestShareTokenSnapshotScope:
def test_shared_with_collaborator_sees_all_artifacts(self, _patch_db, flask_app):
# A read-only shared_with collaborator (JWT) is not snapshot-scoped either.
from application.api.user.artifacts.routes import GetArtifact, ListArtifacts
from docsgpt.api.user.artifacts.routes import GetArtifact, ListArtifacts
conv_id, in_art, out_art, null_art, _token = self._seed(_patch_db)
ConversationsRepository(_patch_db).add_shared_user(conv_id, SHARED_USER)
@@ -518,16 +518,16 @@ class TestDownloadArtifact:
def test_local_streams_bytes_with_content_disposition(
self, _patch_db, flask_app, token_owner, monkeypatch
):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
art = self._seed(_patch_db)
storage = _FakeStorage(b"BINARY")
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"backend",
raising=False,
)
@@ -543,16 +543,16 @@ class TestDownloadArtifact:
):
# The response body must be a stream (generator), not the whole object
# buffered into memory via make_response(file_obj.read()).
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
art = self._seed(_patch_db)
storage = _FakeStorage(b"Z" * 200_000)
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"backend", raising=False,
)
resp = _call(flask_app, DownloadArtifact, art["id"], token=token_owner)
@@ -562,16 +562,16 @@ class TestDownloadArtifact:
def test_s3_strategy_redirects_to_presigned(
self, _patch_db, flask_app, token_owner, monkeypatch
):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
art = self._seed(_patch_db)
storage = _FakeStorage(b"unused")
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"s3",
raising=False,
)
@@ -585,16 +585,16 @@ class TestDownloadArtifact:
):
# ?disposition=url opts into a JSON envelope (for a top-level browser
# navigation) instead of the CORS-blocked cross-origin 302.
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
art = self._seed(_patch_db)
storage = _FakeStorage(b"unused")
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"s3",
raising=False,
)
@@ -614,7 +614,7 @@ class TestDownloadArtifact:
assert resp.mimetype == "application/vnd.docsgpt.artifact-url+json"
def test_stranger_denied(self, _patch_db, flask_app, monkeypatch):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
art = self._seed(_patch_db)
resp = _call(flask_app, DownloadArtifact, art["id"], token={"sub": STRANGER})
@@ -623,7 +623,7 @@ class TestDownloadArtifact:
def test_s3_strategy_misconfigured_backend_500(
self, _patch_db, flask_app, token_owner, monkeypatch
):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
art = self._seed(_patch_db)
@@ -635,11 +635,11 @@ class TestDownloadArtifact:
raise NotImplementedError("backend cannot mint presigned URLs")
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: _NoPresignStorage(),
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"s3",
raising=False,
)
@@ -650,16 +650,16 @@ class TestDownloadArtifact:
def test_crlf_filename_sanitized_in_header(
self, _patch_db, flask_app, token_owner, monkeypatch
):
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
art = self._seed(_patch_db, filename='a"\r\nInjected: x.txt')
storage = _FakeStorage(b"X")
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"backend",
raising=False,
)
@@ -675,7 +675,7 @@ class TestDownloadArtifact:
@pytest.mark.unit
class TestRestoreArtifact:
def test_restore_appends_new_version(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import RestoreArtifact
from docsgpt.api.user.artifacts.routes import RestoreArtifact
conv = _make_conversation(_patch_db)
art = _make_artifact(
@@ -695,7 +695,7 @@ class TestRestoreArtifact:
def test_restore_missing_version_field_400(
self, _patch_db, flask_app, token_owner
):
from application.api.user.artifacts.routes import RestoreArtifact
from docsgpt.api.user.artifacts.routes import RestoreArtifact
conv = _make_conversation(_patch_db)
art = _make_artifact(_patch_db, conversation_id=str(conv["id"]))
@@ -706,7 +706,7 @@ class TestRestoreArtifact:
assert resp.status_code == 400
def test_restore_stranger_denied(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import RestoreArtifact
from docsgpt.api.user.artifacts.routes import RestoreArtifact
conv = _make_conversation(_patch_db)
art = _make_artifact(_patch_db, conversation_id=str(conv["id"]))
@@ -721,7 +721,7 @@ class TestRestoreArtifact:
):
# A share link inherits read/download access only; restore is a WRITE and
# an anonymous link holder must NOT be able to mutate the artifact.
from application.api.user.artifacts.routes import RestoreArtifact
from docsgpt.api.user.artifacts.routes import RestoreArtifact
conv = _make_conversation(_patch_db)
art = _make_artifact(
@@ -741,7 +741,7 @@ class TestRestoreArtifact:
def test_restore_shared_with_collaborator_denied(self, _patch_db, flask_app):
# A read-only ``shared_with`` collaborator can GET but not restore.
from application.api.user.artifacts.routes import RestoreArtifact
from docsgpt.api.user.artifacts.routes import RestoreArtifact
conv = _make_conversation(_patch_db)
ConversationsRepository(_patch_db).add_shared_user(str(conv["id"]), SHARED_USER)
@@ -770,12 +770,12 @@ class TestDeleteArtifact:
def test_owner_deletes_and_reaps_bytes(
self, _patch_db, flask_app, token_owner, monkeypatch
):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
art = self._seed(_patch_db)
storage = _FakeStorage()
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
resp = _call(flask_app, GetArtifact, art["id"], token=token_owner, method="delete")
@@ -784,7 +784,7 @@ class TestDeleteArtifact:
assert "inputs/owner/artifacts/x/v1/f.bin" in storage.deleted
def test_stranger_denied(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
art = self._seed(_patch_db)
resp = _call(
@@ -794,7 +794,7 @@ class TestDeleteArtifact:
assert ArtifactsRepository(_patch_db).get_artifact(art["id"]) is not None
def test_share_token_holder_denied(self, _patch_db, flask_app):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
conv = _make_conversation(_patch_db)
art = _make_artifact(_patch_db, conversation_id=str(conv["id"]))
@@ -807,7 +807,7 @@ class TestDeleteArtifact:
assert ArtifactsRepository(_patch_db).get_artifact(art["id"]) is not None
def test_unknown_artifact_404(self, _patch_db, flask_app, token_owner):
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
resp = _call(
flask_app, GetArtifact, str(uuid.uuid4()), token=token_owner, method="delete"
@@ -827,7 +827,7 @@ class TestConversationDeleteReapsArtifacts:
)
storage = _FakeStorage()
monkeypatch.setattr(
"application.storage.storage_creator.StorageCreator.get_storage",
"docsgpt.storage.storage_creator.StorageCreator.get_storage",
lambda: storage,
)
@@ -858,7 +858,7 @@ class TestMalformedArtifactId:
self, _patch_db, flask_app, token_owner, resource_name,
extra_args, method, json_body,
):
from application.api.user.artifacts import routes as routes_mod
from docsgpt.api.user.artifacts import routes as routes_mod
resource_cls = getattr(routes_mod, resource_name)
resp = _call(
@@ -878,7 +878,7 @@ class TestApiKeyPrincipal:
):
# The public widget key reads an artifact ONLY when the request also
# carries the parent conversation_id (the per-visitor bearer capability).
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
agent = _make_agent(_patch_db)
conv = _make_agent_conversation(_patch_db, agent["id"])
@@ -897,7 +897,7 @@ class TestApiKeyPrincipal:
):
# Regression (critical IDOR): the public widget key alone -- without the
# unguessable per-visitor conversation_id -- cannot fetch a known artifact.
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
agent = _make_agent(_patch_db)
conv = _make_agent_conversation(_patch_db, agent["id"])
@@ -917,7 +917,7 @@ class TestApiKeyPrincipal:
# but must NOT reach an artifact from the owner's OTHER (non-agent)
# conversation -- even when it supplies that conversation_id, the agent
# scope gate still blocks it.
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
_make_agent(_patch_db) # the widget agent (key=secret-key)
other_conv = _make_conversation(_patch_db, user_id=OWNER) # no agent_id
@@ -935,7 +935,7 @@ class TestApiKeyPrincipal:
):
# Download follows the same bearer-capability rule as get: the key alone is
# denied; the key + matching conversation_id is served.
from application.api.user.artifacts.routes import DownloadArtifact
from docsgpt.api.user.artifacts.routes import DownloadArtifact
agent = _make_agent(_patch_db)
conv = _make_agent_conversation(_patch_db, agent["id"])
@@ -946,11 +946,11 @@ class TestApiKeyPrincipal:
_wire_api_key(monkeypatch, _patch_db)
storage = _FakeStorage(b"BYTES")
monkeypatch.setattr(
"application.api.user.artifacts.routes.StorageCreator.get_storage",
"docsgpt.api.user.artifacts.routes.StorageCreator.get_storage",
lambda: storage,
)
monkeypatch.setattr(
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
"docsgpt.api.user.artifacts.routes.settings.URL_STRATEGY",
"backend", raising=False,
)
@@ -972,7 +972,7 @@ class TestApiKeyPrincipal:
):
# Regression (critical IDOR): the public widget key cannot enumerate the
# agent's artifacts -- a list with no conversation_id is refused outright.
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
agent = _make_agent(_patch_db)
conv = _make_agent_conversation(_patch_db, agent["id"])
@@ -987,7 +987,7 @@ class TestApiKeyPrincipal:
):
# With the per-visitor conversation_id the list returns only that
# conversation's artifacts -- not the agent's other conversations.
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
agent = _make_agent(_patch_db)
conv_a = _make_agent_conversation(_patch_db, agent["id"])
@@ -1010,7 +1010,7 @@ class TestApiKeyPrincipal:
):
# A conversation_id belonging to a DIFFERENT agent leaks nothing: the JOIN
# filters it out, yielding an empty 200 rather than a cross-visitor leak.
from application.api.user.artifacts.routes import ListArtifacts
from docsgpt.api.user.artifacts.routes import ListArtifacts
agent = _make_agent(_patch_db)
agent_conv = _make_agent_conversation(_patch_db, agent["id"])
@@ -1032,7 +1032,7 @@ class TestApiKeyPrincipal:
):
# Mutations require a JWT owner; a low-trust agent key may never delete,
# even an artifact inside its own agent scope.
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
agent = _make_agent(_patch_db)
conv = _make_agent_conversation(_patch_db, agent["id"])
@@ -1050,7 +1050,7 @@ class TestApiKeyPrincipal:
self, _patch_db, flask_app, monkeypatch
):
# A key owned by a different user cannot reach this owner's artifact.
from application.api.user.artifacts.routes import GetArtifact
from docsgpt.api.user.artifacts.routes import GetArtifact
owner_conv = _make_conversation(_patch_db, user_id=OWNER)
art = _make_artifact(_patch_db, conversation_id=str(owner_conv["id"]))
+38 -38
View File
@@ -15,9 +15,9 @@ def _patch_base_db(conn):
yield conn
with patch(
"application.api.user.base.db_session", _yield
"docsgpt.api.user.base.db_session", _yield
), patch(
"application.api.user.base.db_readonly", _yield
"docsgpt.api.user.base.db_readonly", _yield
):
yield
@@ -27,7 +27,7 @@ class TestTimeRangeGenerators:
pass
def test_generate_minute_range(self):
from application.api.user.base import generate_minute_range
from docsgpt.api.user.base import generate_minute_range
start = datetime.datetime(2024, 1, 1, 10, 0, 0)
end = datetime.datetime(2024, 1, 1, 10, 5, 0)
@@ -40,7 +40,7 @@ class TestTimeRangeGenerators:
assert all(val == 0 for val in result.values())
def test_generate_hourly_range(self):
from application.api.user.base import generate_hourly_range
from docsgpt.api.user.base import generate_hourly_range
start = datetime.datetime(2024, 1, 1, 10, 0, 0)
end = datetime.datetime(2024, 1, 1, 15, 0, 0)
@@ -53,7 +53,7 @@ class TestTimeRangeGenerators:
assert all(val == 0 for val in result.values())
def test_generate_date_range(self):
from application.api.user.base import generate_date_range
from docsgpt.api.user.base import generate_date_range
start = datetime.date(2024, 1, 1)
end = datetime.date(2024, 1, 5)
@@ -66,7 +66,7 @@ class TestTimeRangeGenerators:
assert all(val == 0 for val in result.values())
def test_single_minute_range(self):
from application.api.user.base import generate_minute_range
from docsgpt.api.user.base import generate_minute_range
time = datetime.datetime(2024, 1, 1, 10, 30, 0)
result = generate_minute_range(time, time)
@@ -84,7 +84,7 @@ class TestResolveToolDetails:
pass
def test_empty_tool_ids_list(self, mock_mongo_db):
from application.api.user.base import resolve_tool_details
from docsgpt.api.user.base import resolve_tool_details
result = resolve_tool_details([])
@@ -95,9 +95,9 @@ class TestResolveToolDetails:
class TestGetVectorStore:
pass
@patch("application.api.user.base.VectorCreator.create_vectorstore")
@patch("docsgpt.api.user.base.VectorCreator.create_vectorstore")
def test_creates_vector_store(self, mock_create):
from application.api.user.base import get_vector_store
from docsgpt.api.user.base import get_vector_store
mock_store = Mock()
mock_create.return_value = mock_store
@@ -128,7 +128,7 @@ class TestHandleImageUpload:
return cls._image_bytes()
def test_returns_existing_url_when_no_file(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.test_request_context():
mock_request = Mock()
@@ -144,7 +144,7 @@ class TestHandleImageUpload:
assert error is None
def test_uploads_new_image(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.test_request_context():
mock_file = FileStorage(
@@ -166,7 +166,7 @@ class TestHandleImageUpload:
mock_storage.save_file.assert_called_once()
def test_accepts_multi_picture_jpeg(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
# Pillow reports multi-picture JPEGs (e.g. iPhone portrait photos) as
# MPO; they must still be accepted under a .jpg extension.
@@ -189,7 +189,7 @@ class TestHandleImageUpload:
mock_storage.save_file.assert_called_once()
def test_uploads_image_with_non_ascii_basename(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.test_request_context():
mock_file = FileStorage(
@@ -206,7 +206,7 @@ class TestHandleImageUpload:
mock_storage.save_file.assert_called_once()
def test_ignores_empty_filename(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.test_request_context():
mock_file = Mock()
@@ -225,7 +225,7 @@ class TestHandleImageUpload:
mock_storage.save_file.assert_not_called()
def test_handles_upload_error(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.app_context():
mock_file = FileStorage(
@@ -245,7 +245,7 @@ class TestHandleImageUpload:
assert error.status_code == 400
def test_rejects_non_image_content(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.app_context():
mock_file = FileStorage(
@@ -263,11 +263,11 @@ class TestHandleImageUpload:
mock_storage.save_file.assert_not_called()
def test_accepts_image_at_encoded_byte_limit(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
payload = self._png_bytes()
with patch(
"application.api.user.base.settings.AGENT_IMAGE_MAX_BYTES",
"docsgpt.api.user.base.settings.AGENT_IMAGE_MAX_BYTES",
len(payload),
), flask_app.app_context():
mock_file = FileStorage(
@@ -284,11 +284,11 @@ class TestHandleImageUpload:
mock_storage.save_file.assert_called_once()
def test_rejects_image_over_encoded_byte_limit(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
payload = self._png_bytes()
with patch(
"application.api.user.base.settings.AGENT_IMAGE_MAX_BYTES",
"docsgpt.api.user.base.settings.AGENT_IMAGE_MAX_BYTES",
len(payload) - 1,
), flask_app.app_context():
mock_file = FileStorage(
@@ -311,11 +311,11 @@ class TestHandleImageUpload:
def test_enforces_decoded_pixel_limit(
self, flask_app, width, height, expected_status
):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
payload = self._image_bytes(width, height)
with patch(
"application.api.user.base.settings.AGENT_IMAGE_MAX_PIXELS", 16
"docsgpt.api.user.base.settings.AGENT_IMAGE_MAX_PIXELS", 16
), flask_app.app_context():
mock_file = FileStorage(
stream=io.BytesIO(payload), filename="dimensions.png"
@@ -338,7 +338,7 @@ class TestHandleImageUpload:
def test_rejects_image_whose_content_does_not_match_extension(
self, flask_app
):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.app_context():
mock_file = FileStorage(
@@ -355,7 +355,7 @@ class TestHandleImageUpload:
mock_storage.save_file.assert_not_called()
def test_sanitizes_user_directory_component(self, flask_app):
from application.api.user.base import handle_image_upload
from docsgpt.api.user.base import handle_image_upload
with flask_app.app_context():
mock_file = FileStorage(
@@ -379,7 +379,7 @@ class TestRequireAgentDecorator:
pass
def test_returns_400_for_missing_token(self, flask_app):
from application.api.user.base import require_agent
from docsgpt.api.user.base import require_agent
with flask_app.app_context():
@@ -400,7 +400,7 @@ class TestRequireAgentDecorator:
class TestEnsureUserDocPgConn:
def test_creates_new_user_doc(self, pg_conn):
from application.api.user.base import ensure_user_doc
from docsgpt.api.user.base import ensure_user_doc
with _patch_base_db(pg_conn):
doc = ensure_user_doc("brand-new-user")
@@ -410,8 +410,8 @@ class TestEnsureUserDocPgConn:
assert prefs.get("shared_with_me") == []
def test_preserves_existing_prefs(self, pg_conn):
from application.api.user.base import ensure_user_doc
from application.storage.db.repositories.users import UsersRepository
from docsgpt.api.user.base import ensure_user_doc
from docsgpt.storage.db.repositories.users import UsersRepository
user = "existing-user"
UsersRepository(pg_conn).upsert(user)
@@ -425,18 +425,18 @@ class TestEnsureUserDocPgConn:
class TestResolveToolDetailsPgConn:
def test_empty_list_returns_empty(self, pg_conn):
from application.api.user.base import resolve_tool_details
from docsgpt.api.user.base import resolve_tool_details
with _patch_base_db(pg_conn):
assert resolve_tool_details([]) == []
def test_none_entries_filtered_out(self, pg_conn):
from application.api.user.base import resolve_tool_details
from docsgpt.api.user.base import resolve_tool_details
with _patch_base_db(pg_conn):
assert resolve_tool_details([None, ""]) == []
def test_resolves_known_uuid_ids(self, pg_conn):
from application.api.user.base import resolve_tool_details
from application.storage.db.repositories.user_tools import (
from docsgpt.api.user.base import resolve_tool_details
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -452,7 +452,7 @@ class TestResolveToolDetailsPgConn:
assert got[0]["display_name"] == "Custom"
def test_unknown_ids_skipped(self, pg_conn):
from application.api.user.base import resolve_tool_details
from docsgpt.api.user.base import resolve_tool_details
with _patch_base_db(pg_conn):
got = resolve_tool_details(
["00000000-0000-0000-0000-000000000000"]
@@ -460,8 +460,8 @@ class TestResolveToolDetailsPgConn:
assert got == []
def test_legacy_ids_lookup(self, pg_conn):
from application.api.user.base import resolve_tool_details
from application.storage.db.repositories.user_tools import (
from docsgpt.api.user.base import resolve_tool_details
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -479,7 +479,7 @@ class TestResolveToolDetailsPgConn:
class TestRequireAgentPgConn:
def test_returns_404_invalid_token(self, pg_conn, flask_app):
from application.api.user.base import require_agent
from docsgpt.api.user.base import require_agent
@require_agent
def fn(webhook_token=None, agent=None, agent_id_str=None):
@@ -490,8 +490,8 @@ class TestRequireAgentPgConn:
assert result.status_code == 404
def test_injects_agent_when_valid(self, pg_conn, flask_app):
from application.api.user.base import require_agent
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.api.user.base import require_agent
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "wh-agent", "published",
+56 -56
View File
@@ -19,16 +19,16 @@ def _patch_conversations_db(conn):
yield conn
with patch(
"application.api.user.conversations.routes.db_session", _yield_conn
"docsgpt.api.user.conversations.routes.db_session", _yield_conn
), patch(
"application.api.user.conversations.routes.db_readonly", _yield_conn
"docsgpt.api.user.conversations.routes.db_readonly", _yield_conn
):
yield
def _seed_conversation(pg_conn, user_id, name="Test Conv"):
"""Create a conversation and return its PG uuid id as str."""
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
repo = ConversationsRepository(pg_conn)
@@ -41,7 +41,7 @@ class TestDeleteConversation:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import DeleteConversation
from docsgpt.api.user.conversations.routes import DeleteConversation
with app.test_request_context("/api/delete_conversation?id=abc"):
from flask import request
@@ -52,7 +52,7 @@ class TestDeleteConversation:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.conversations.routes import DeleteConversation
from docsgpt.api.user.conversations.routes import DeleteConversation
with app.test_request_context("/api/delete_conversation"):
from flask import request
@@ -68,7 +68,7 @@ class TestDeleteAllConversations:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import DeleteAllConversations
from docsgpt.api.user.conversations.routes import DeleteAllConversations
with app.test_request_context("/api/delete_all_conversations"):
from flask import request
@@ -84,7 +84,7 @@ class TestGetConversations:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import GetConversations
from docsgpt.api.user.conversations.routes import GetConversations
with app.test_request_context("/api/get_conversations"):
from flask import request
@@ -100,7 +100,7 @@ class TestGetSingleConversation:
pass
def test_returns_400_missing_id(self, app):
from application.api.user.conversations.routes import GetSingleConversation
from docsgpt.api.user.conversations.routes import GetSingleConversation
with app.test_request_context("/api/get_single_conversation"):
from flask import request
@@ -117,7 +117,7 @@ class TestUpdateConversationName:
pass
def test_returns_400_missing_fields(self, app):
from application.api.user.conversations.routes import UpdateConversationName
from docsgpt.api.user.conversations.routes import UpdateConversationName
with app.test_request_context(
"/api/update_conversation_name",
@@ -137,7 +137,7 @@ class TestSubmitFeedback:
pass
def test_returns_400_missing_fields(self, app):
from application.api.user.conversations.routes import SubmitFeedback
from docsgpt.api.user.conversations.routes import SubmitFeedback
with app.test_request_context(
"/api/feedback",
@@ -159,8 +159,8 @@ class TestSubmitFeedback:
class TestDeleteConversationHappy:
def test_deletes_existing_conversation(self, app, pg_conn):
from application.api.user.conversations.routes import DeleteConversation
from application.storage.db.repositories.conversations import (
from docsgpt.api.user.conversations.routes import DeleteConversation
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -182,7 +182,7 @@ class TestDeleteConversationHappy:
def test_delete_nonexistent_still_returns_200(self, app, pg_conn):
"""get_any returns None, so delete is a no-op but endpoint succeeds."""
from application.api.user.conversations.routes import DeleteConversation
from docsgpt.api.user.conversations.routes import DeleteConversation
with _patch_conversations_db(pg_conn), app.test_request_context(
f"/api/delete_conversation?id={uuid.uuid4()}"
@@ -195,7 +195,7 @@ class TestDeleteConversationHappy:
assert response.status_code == 200
def test_db_error_returns_400(self, app):
from application.api.user.conversations.routes import DeleteConversation
from docsgpt.api.user.conversations.routes import DeleteConversation
@contextmanager
def _broken():
@@ -203,7 +203,7 @@ class TestDeleteConversationHappy:
yield
with patch(
"application.api.user.conversations.routes.db_session", _broken
"docsgpt.api.user.conversations.routes.db_session", _broken
), app.test_request_context("/api/delete_conversation?id=abc"):
from flask import request
@@ -215,10 +215,10 @@ class TestDeleteConversationHappy:
class TestDeleteAllConversationsHappy:
def test_deletes_all_conversations(self, app, pg_conn):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
DeleteAllConversations,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -238,7 +238,7 @@ class TestDeleteAllConversationsHappy:
assert ConversationsRepository(pg_conn).list_for_user(user) == []
def test_db_error_returns_400(self, app):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
DeleteAllConversations,
)
@@ -248,7 +248,7 @@ class TestDeleteAllConversationsHappy:
yield
with patch(
"application.api.user.conversations.routes.db_session", _broken
"docsgpt.api.user.conversations.routes.db_session", _broken
), app.test_request_context("/api/delete_all_conversations"):
from flask import request
@@ -260,7 +260,7 @@ class TestDeleteAllConversationsHappy:
class TestGetConversationsHappy:
def test_returns_list_of_conversations(self, app, pg_conn):
from application.api.user.conversations.routes import GetConversations
from docsgpt.api.user.conversations.routes import GetConversations
user = "user-list"
c1 = _seed_conversation(pg_conn, user, name="one")
@@ -284,7 +284,7 @@ class TestGetConversationsHappy:
assert "shared_token" in c
def test_db_error_returns_400(self, app):
from application.api.user.conversations.routes import GetConversations
from docsgpt.api.user.conversations.routes import GetConversations
@contextmanager
def _broken():
@@ -292,7 +292,7 @@ class TestGetConversationsHappy:
yield
with patch(
"application.api.user.conversations.routes.db_readonly", _broken
"docsgpt.api.user.conversations.routes.db_readonly", _broken
), app.test_request_context("/api/get_conversations"):
from flask import request
@@ -304,7 +304,7 @@ class TestGetConversationsHappy:
class TestGetSingleConversationHappy:
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
GetSingleConversation,
)
@@ -317,7 +317,7 @@ class TestGetSingleConversationHappy:
assert response.status_code == 401
def test_returns_404_not_found(self, app, pg_conn):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
GetSingleConversation,
)
@@ -332,10 +332,10 @@ class TestGetSingleConversationHappy:
assert response.status_code == 404
def test_returns_conversation_with_messages(self, app, pg_conn):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
GetSingleConversation,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -370,10 +370,10 @@ class TestGetSingleConversationHappy:
assert data["queries"][0]["response"] == "hello"
def test_returns_message_with_dict_feedback(self, app, pg_conn):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
GetSingleConversation,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -399,7 +399,7 @@ class TestGetSingleConversationHappy:
assert q["feedback_timestamp"] == "2024-01-01T00:00:00Z"
def test_db_error_returns_400(self, app):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
GetSingleConversation,
)
@@ -409,7 +409,7 @@ class TestGetSingleConversationHappy:
yield
with patch(
"application.api.user.conversations.routes.db_readonly", _broken
"docsgpt.api.user.conversations.routes.db_readonly", _broken
), app.test_request_context("/api/get_single_conversation?id=abc"):
from flask import request
@@ -426,7 +426,7 @@ class TestGetMessageTail:
"""
def _seed_in_flight_message(self, pg_conn, owner_user_id):
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -444,7 +444,7 @@ class TestGetMessageTail:
return conv_id, str(msg["id"])
def test_owner_can_tail(self, app, pg_conn):
from application.api.user.conversations.routes import GetMessageTail
from docsgpt.api.user.conversations.routes import GetMessageTail
owner = "user-owner"
_, msg_id = self._seed_in_flight_message(pg_conn, owner)
@@ -468,8 +468,8 @@ class TestGetMessageTail:
the tail-poll silently 404s and the in-flight bubble never
resolves on the shared user's side.
"""
from application.api.user.conversations.routes import GetMessageTail
from application.storage.db.repositories.conversations import (
from docsgpt.api.user.conversations.routes import GetMessageTail
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -490,7 +490,7 @@ class TestGetMessageTail:
assert response.json["message_id"] == msg_id
def test_non_member_gets_404(self, app, pg_conn):
from application.api.user.conversations.routes import GetMessageTail
from docsgpt.api.user.conversations.routes import GetMessageTail
owner = "user-owner-private"
intruder = "user-intruder"
@@ -508,8 +508,8 @@ class TestGetMessageTail:
def test_streaming_row_returns_partial_from_journal(self, app, pg_conn):
"""Mid-stream rows must rebuild from message_events, not return the placeholder."""
from application.api.user.conversations.routes import GetMessageTail
from application.storage.db.repositories.message_events import (
from docsgpt.api.user.conversations.routes import GetMessageTail
from docsgpt.storage.db.repositories.message_events import (
MessageEventsRepository,
)
@@ -543,7 +543,7 @@ class TestGetMessageTail:
self, app, pg_conn
):
"""Empty journal returns empty response, not the placeholder."""
from application.api.user.conversations.routes import GetMessageTail
from docsgpt.api.user.conversations.routes import GetMessageTail
owner = "user-tail-empty"
_, msg_id = self._seed_in_flight_message(pg_conn, owner)
@@ -563,7 +563,7 @@ class TestGetMessageTail:
class TestUpdateConversationNameHappy:
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
UpdateConversationName,
)
@@ -580,10 +580,10 @@ class TestUpdateConversationNameHappy:
assert response.status_code == 401
def test_renames_conversation(self, app, pg_conn):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
UpdateConversationName,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -605,7 +605,7 @@ class TestUpdateConversationNameHappy:
assert got["name"] == "new"
def test_rename_nonexistent_still_returns_200(self, app, pg_conn):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
UpdateConversationName,
)
@@ -622,7 +622,7 @@ class TestUpdateConversationNameHappy:
assert response.status_code == 200
def test_db_error_returns_400(self, app):
from application.api.user.conversations.routes import (
from docsgpt.api.user.conversations.routes import (
UpdateConversationName,
)
@@ -632,7 +632,7 @@ class TestUpdateConversationNameHappy:
yield
with patch(
"application.api.user.conversations.routes.db_session", _broken
"docsgpt.api.user.conversations.routes.db_session", _broken
), app.test_request_context(
"/api/update_conversation_name",
method="POST",
@@ -648,7 +648,7 @@ class TestUpdateConversationNameHappy:
class TestSubmitFeedbackHappy:
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import SubmitFeedback
from docsgpt.api.user.conversations.routes import SubmitFeedback
with app.test_request_context(
"/api/feedback",
@@ -667,8 +667,8 @@ class TestSubmitFeedbackHappy:
assert response.status_code == 401
def test_submits_feedback(self, app, pg_conn):
from application.api.user.conversations.routes import SubmitFeedback
from application.storage.db.repositories.conversations import (
from docsgpt.api.user.conversations.routes import SubmitFeedback
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -698,8 +698,8 @@ class TestSubmitFeedbackHappy:
assert fb and fb.get("text") == "like"
def test_none_feedback_allowed(self, app, pg_conn):
from application.api.user.conversations.routes import SubmitFeedback
from application.storage.db.repositories.conversations import (
from docsgpt.api.user.conversations.routes import SubmitFeedback
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -726,7 +726,7 @@ class TestSubmitFeedbackHappy:
assert response.status_code == 200
def test_returns_404_for_missing_conversation(self, app, pg_conn):
from application.api.user.conversations.routes import SubmitFeedback
from docsgpt.api.user.conversations.routes import SubmitFeedback
with _patch_conversations_db(pg_conn), app.test_request_context(
"/api/feedback",
@@ -745,7 +745,7 @@ class TestSubmitFeedbackHappy:
assert response.status_code == 404
def test_db_error_returns_400(self, app):
from application.api.user.conversations.routes import SubmitFeedback
from docsgpt.api.user.conversations.routes import SubmitFeedback
@contextmanager
def _broken():
@@ -753,7 +753,7 @@ class TestSubmitFeedbackHappy:
yield
with patch(
"application.api.user.conversations.routes.db_session", _broken
"docsgpt.api.user.conversations.routes.db_session", _broken
), app.test_request_context(
"/api/feedback",
method="POST",
@@ -776,12 +776,12 @@ class TestSubmitFeedbackWithApiKey:
"""api_key callers carry no JWT."""
def _seed_agent_with_key(self, pg_conn, owner, key):
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
return AgentsRepository(pg_conn).create(owner, "widget", "published", key=key)
def _post(self, app, pg_conn, payload):
from application.api.user.conversations.routes import SubmitFeedback
from docsgpt.api.user.conversations.routes import SubmitFeedback
with _patch_conversations_db(pg_conn), app.test_request_context("/api/feedback", method="POST", json=payload):
from flask import request
@@ -790,7 +790,7 @@ class TestSubmitFeedbackWithApiKey:
return SubmitFeedback().post()
def test_valid_key_rates_its_own_conversation(self, app, pg_conn):
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -816,7 +816,7 @@ class TestSubmitFeedbackWithApiKey:
assert fb and fb.get("text") == "like"
def test_key_cannot_rate_owner_conversation_it_did_not_create(self, app, pg_conn):
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
+48 -48
View File
@@ -21,7 +21,7 @@ def app():
class TestAgentFoldersGet:
def test_returns_folders(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
now = datetime.datetime(2024, 6, 15, tzinfo=datetime.timezone.utc)
folder_id = uuid.uuid4().hex
@@ -37,7 +37,7 @@ class TestAgentFoldersGet:
]
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context("/api/agents/folders/", method="GET"):
@@ -53,7 +53,7 @@ class TestAgentFoldersGet:
assert folders[0]["name"] == "My Folder"
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with app.test_request_context("/api/agents/folders/", method="GET"):
from flask import request
@@ -68,14 +68,14 @@ class TestAgentFoldersGet:
class TestAgentFoldersCreate:
def test_creates_folder(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
inserted_id = uuid.uuid4().hex
mock_collection = Mock()
mock_collection.insert_one.return_value = Mock(inserted_id=inserted_id)
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
@@ -93,7 +93,7 @@ class TestAgentFoldersCreate:
assert response.json["name"] == "New Folder"
def test_returns_400_missing_name(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with app.test_request_context(
"/api/agents/folders/",
@@ -108,13 +108,13 @@ class TestAgentFoldersCreate:
assert response.status_code == 400
def test_validates_parent_folder_exists(self, app):
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
@@ -134,7 +134,7 @@ class TestAgentFoldersCreate:
class TestAgentFolderGet:
def test_returns_folder_with_agents_and_subfolders(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
folder_id = uuid.uuid4().hex
agent_id = uuid.uuid4().hex
@@ -154,10 +154,10 @@ class TestAgentFolderGet:
]
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
), patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
@@ -174,13 +174,13 @@ class TestAgentFolderGet:
assert len(response.json["subfolders"]) == 1
def test_returns_404_not_found(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
@@ -198,14 +198,14 @@ class TestAgentFolderGet:
class TestAgentFolderUpdate:
def test_updates_folder_name(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
folder_id = uuid.uuid4().hex
mock_collection = Mock()
mock_collection.update_one.return_value = Mock(matched_count=1)
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
@@ -222,7 +222,7 @@ class TestAgentFolderUpdate:
assert response.json["success"] is True
def test_prevents_self_parent(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
folder_id = str(uuid.uuid4().hex)
@@ -240,13 +240,13 @@ class TestAgentFolderUpdate:
assert "own parent" in response.json["message"]
def test_returns_404_when_not_found(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
mock_collection = Mock()
mock_collection.update_one.return_value = Mock(matched_count=0)
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
@@ -266,7 +266,7 @@ class TestAgentFolderUpdate:
class TestAgentFolderDelete:
def test_deletes_folder_and_unsets_references(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
folder_id = str(uuid.uuid4().hex)
mock_folders = Mock()
@@ -274,10 +274,10 @@ class TestAgentFolderDelete:
mock_agents = Mock()
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
), patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
@@ -294,17 +294,17 @@ class TestAgentFolderDelete:
mock_folders.delete_one.assert_called_once()
def test_returns_404_not_found(self, app):
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
mock_folders = Mock()
mock_folders.delete_one.return_value = Mock(deleted_count=0)
mock_agents = Mock()
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
), patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
@@ -322,7 +322,7 @@ class TestAgentFolderDelete:
class TestMoveAgentToFolder:
def test_moves_agent_to_folder(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
agent_id = uuid.uuid4().hex
folder_id = uuid.uuid4().hex
@@ -332,10 +332,10 @@ class TestMoveAgentToFolder:
mock_folders.find_one.return_value = {"_id": folder_id}
with patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
), patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
@@ -355,14 +355,14 @@ class TestMoveAgentToFolder:
mock_agents.update_one.assert_called_once()
def test_removes_agent_from_folder(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
agent_id = uuid.uuid4().hex
mock_agents = Mock()
mock_agents.find_one.return_value = {"_id": agent_id, "user": "user1"}
with patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
@@ -380,13 +380,13 @@ class TestMoveAgentToFolder:
assert "$unset" in call_args[0][1]
def test_returns_404_agent_not_found(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
mock_agents = Mock()
mock_agents.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
@@ -402,7 +402,7 @@ class TestMoveAgentToFolder:
assert response.status_code == 404
def test_returns_400_missing_agent_id(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
from docsgpt.api.user.agents.folders import MoveAgentToFolder
with app.test_request_context(
"/api/agents/folders/move_agent",
@@ -421,7 +421,7 @@ class TestMoveAgentToFolder:
class TestBulkMoveAgents:
def test_bulk_moves_to_folder(self, app):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
folder_id = uuid.uuid4().hex
agent_ids = [str(uuid.uuid4().hex), str(uuid.uuid4().hex)]
@@ -430,10 +430,10 @@ class TestBulkMoveAgents:
mock_folders.find_one.return_value = {"_id": folder_id}
with patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
), patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
@@ -450,13 +450,13 @@ class TestBulkMoveAgents:
mock_agents.update_many.assert_called_once()
def test_bulk_removes_from_folders(self, app):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
agent_ids = [str(uuid.uuid4().hex)]
mock_agents = Mock()
with patch(
"application.api.user.agents.folders.agents_collection",
"docsgpt.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
@@ -474,7 +474,7 @@ class TestBulkMoveAgents:
assert "$unset" in call_args[0][1]
def test_returns_400_missing_agent_ids(self, app):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
with app.test_request_context(
"/api/agents/folders/bulk_move",
@@ -489,13 +489,13 @@ class TestBulkMoveAgents:
assert response.status_code == 400
def test_returns_404_folder_not_found(self, app):
from application.api.user.agents.folders import BulkMoveAgents
from docsgpt.api.user.agents.folders import BulkMoveAgents
mock_folders = Mock()
mock_folders.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
@@ -524,7 +524,7 @@ class TestAgentFoldersGaps:
def test_create_folder_no_auth(self, app):
"""Cover line 64: post returns 401 when no decoded_token."""
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
with app.test_request_context(
"/api/agents/folders/",
@@ -539,14 +539,14 @@ class TestAgentFoldersGaps:
def test_create_folder_exception(self, app):
"""Cover lines 90-91: exception during insert_one returns 400."""
from application.api.user.agents.folders import AgentFolders
from docsgpt.api.user.agents.folders import AgentFolders
mock_folders = Mock()
mock_folders.find_one.return_value = None
mock_folders.insert_one.side_effect = Exception("db error")
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
@@ -562,7 +562,7 @@ class TestAgentFoldersGaps:
def test_get_folder_no_auth(self, app):
"""Cover line 100: get specific folder returns 401 when no auth."""
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with app.test_request_context(
"/api/agents/folders/abc",
@@ -576,13 +576,13 @@ class TestAgentFoldersGaps:
def test_get_folder_exception(self, app):
"""Cover lines 125-126: exception during find returns 400."""
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
mock_folders = Mock()
mock_folders.find_one.side_effect = Exception("db error")
with patch(
"application.api.user.agents.folders.agent_folders_collection",
"docsgpt.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
@@ -597,7 +597,7 @@ class TestAgentFoldersGaps:
def test_update_folder_no_auth(self, app):
"""Cover line 132: put returns 401 when no decoded_token."""
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with app.test_request_context(
"/api/agents/folders/abc",
@@ -612,7 +612,7 @@ class TestAgentFoldersGaps:
def test_update_folder_no_data(self, app):
"""Cover line 136: put with no data returns 400."""
from application.api.user.agents.folders import AgentFolder
from docsgpt.api.user.agents.folders import AgentFolder
with app.test_request_context(
"/api/agents/folders/abc",
+31 -31
View File
@@ -16,9 +16,9 @@ def _patch_decorator_db(conn):
yield conn
with patch(
"application.api.user.idempotency.db_session", _yield
"docsgpt.api.user.idempotency.db_session", _yield
), patch(
"application.api.user.idempotency.db_readonly", _yield
"docsgpt.api.user.idempotency.db_readonly", _yield
):
yield
@@ -43,7 +43,7 @@ def _row_for(conn, key):
@pytest.mark.unit
class TestNoKey:
def test_pass_through_no_db_hit(self, pg_conn):
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
calls = []
@@ -53,9 +53,9 @@ class TestNoKey:
return {"x": x}
with patch(
"application.api.user.idempotency.db_session"
"docsgpt.api.user.idempotency.db_session"
) as mock_session, patch(
"application.api.user.idempotency.db_readonly"
"docsgpt.api.user.idempotency.db_readonly"
) as mock_readonly:
result = task(_fake_celery_self(), 7)
@@ -65,7 +65,7 @@ class TestNoKey:
assert mock_readonly.call_count == 0
def test_empty_string_key_treated_as_absent(self, pg_conn):
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="thing")
def task(self, idempotency_key=None):
@@ -84,7 +84,7 @@ class TestNoKey:
@pytest.mark.unit
class TestFirstRunWithKey:
def test_records_completed_row(self, pg_conn):
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="thing")
def task(self, idempotency_key=None):
@@ -105,7 +105,7 @@ class TestFirstRunWithKey:
@pytest.mark.unit
class TestSecondRunCompletedShortCircuits:
def test_returns_cached_without_invoking(self, pg_conn):
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
invocations = {"count": 0}
@@ -130,7 +130,7 @@ class TestFirstRunFails:
stays in ``pending`` (with bumped attempt_count) so the next
attempt isn't gated as already-completed.
"""
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="thing")
def task(self, idempotency_key=None):
@@ -149,7 +149,7 @@ class TestFirstRunFails:
@pytest.mark.unit
class TestPoisonLoopGuard:
def test_refuses_after_max_attempts(self, pg_conn):
from application.api.user.idempotency import (
from docsgpt.api.user.idempotency import (
MAX_TASK_ATTEMPTS, with_idempotency,
)
@@ -177,8 +177,8 @@ class TestPoisonLoopGuard:
class TestPreviousPendingReruns:
def test_pending_row_does_not_short_circuit(self, pg_conn):
"""HTTP boundary writes ``pending``; on first arrival the wrapper still runs."""
from application.api.user.idempotency import with_idempotency
from application.storage.db.repositories.idempotency import (
from docsgpt.api.user.idempotency import with_idempotency
from docsgpt.storage.db.repositories.idempotency import (
IdempotencyRepository,
)
@@ -215,8 +215,8 @@ class TestRaceWithCompletedRow:
"""A second worker finishing after the first should not clobber the completed row."""
def test_second_record_no_ops_on_completed(self, pg_conn):
from application.api.user.idempotency import with_idempotency
from application.storage.db.repositories.idempotency import (
from docsgpt.api.user.idempotency import with_idempotency
from docsgpt.storage.db.repositories.idempotency import (
IdempotencyRepository,
)
@@ -258,10 +258,10 @@ class TestLiveLeaseDefersConcurrentRun:
"""
def test_second_worker_reraises_retry_without_running(self, pg_conn):
from application.api.user.idempotency import (
from docsgpt.api.user.idempotency import (
LEASE_TTL_SECONDS, with_idempotency,
)
from application.storage.db.repositories.idempotency import (
from docsgpt.storage.db.repositories.idempotency import (
IdempotencyRepository,
)
@@ -303,8 +303,8 @@ class TestLiveLeaseDefersConcurrentRun:
"""
from sqlalchemy import text
from application.api.user.idempotency import with_idempotency
from application.storage.db.repositories.idempotency import (
from docsgpt.api.user.idempotency import with_idempotency
from docsgpt.storage.db.repositories.idempotency import (
IdempotencyRepository,
)
@@ -344,7 +344,7 @@ class TestExceptionPathReleasesLease:
def test_release_clears_lease_owner(self, pg_conn):
from sqlalchemy import text
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="thing")
def task(self, idempotency_key=None):
@@ -366,7 +366,7 @@ class TestExceptionPathReleasesLease:
def test_next_attempt_can_reclaim_after_release(self, pg_conn):
"""Sequential retries don't get blocked by the lease TTL."""
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
invocations = {"count": 0}
@@ -398,7 +398,7 @@ class TestSuccessfulRunClearsLease:
def test_completed_row_has_null_lease(self, pg_conn):
from sqlalchemy import text
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="thing")
def task(self, idempotency_key=None):
@@ -426,7 +426,7 @@ class TestSynthesizedKeyGuardsKeylessDispatch:
"""
def test_keyless_with_source_id_records_dedup_row(self, pg_conn):
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="ingest")
def task(self, idempotency_key=None, source_id=None):
@@ -445,7 +445,7 @@ class TestSynthesizedKeyGuardsKeylessDispatch:
"""Same ``source_id`` → same key → a redelivery short-circuits to
the cached result instead of re-running the body.
"""
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
runs = {"count": 0}
@@ -465,7 +465,7 @@ class TestSynthesizedKeyGuardsKeylessDispatch:
"""The core fix: a keyless OOM-looping dispatch is bounded — the
guard trips after MAX_TASK_ATTEMPTS with no explicit key.
"""
from application.api.user.idempotency import (
from docsgpt.api.user.idempotency import (
MAX_TASK_ATTEMPTS, with_idempotency,
)
@@ -491,16 +491,16 @@ class TestSynthesizedKeyGuardsKeylessDispatch:
"""No explicit key and no ``source_id`` anchor → pass through with
no DB writes, exactly as before.
"""
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="store_attachment")
def task(self, idempotency_key=None):
return {"ran": True}
with patch(
"application.api.user.idempotency.db_session"
"docsgpt.api.user.idempotency.db_session"
) as mock_session, patch(
"application.api.user.idempotency.db_readonly"
"docsgpt.api.user.idempotency.db_readonly"
) as mock_readonly:
result = task(_fake_celery_self())
@@ -510,7 +510,7 @@ class TestSynthesizedKeyGuardsKeylessDispatch:
def test_explicit_key_takes_precedence_over_source_id(self, pg_conn):
"""An explicit key wins; the synthesized ``auto:`` key is unused."""
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
@with_idempotency(task_name="ingest")
def task(self, idempotency_key=None, source_id=None):
@@ -534,7 +534,7 @@ class TestPoisonHook:
"""
def test_hook_invoked_with_bound_args_on_poison(self, pg_conn):
from application.api.user.idempotency import (
from docsgpt.api.user.idempotency import (
MAX_TASK_ATTEMPTS, with_idempotency,
)
@@ -559,7 +559,7 @@ class TestPoisonHook:
assert bound["source_id"] == "src-h"
def test_hook_not_invoked_on_success(self, pg_conn):
from application.api.user.idempotency import with_idempotency
from docsgpt.api.user.idempotency import with_idempotency
calls = []
@@ -576,7 +576,7 @@ class TestPoisonHook:
def test_hook_failure_does_not_break_poison_return(self, pg_conn):
"""A throwing hook must not change the poison-guard outcome."""
from application.api.user.idempotency import (
from docsgpt.api.user.idempotency import (
MAX_TASK_ATTEMPTS, with_idempotency,
)
+6 -6
View File
@@ -14,7 +14,7 @@ def app():
class TestModelsListResource:
def test_returns_models(self, app):
from application.api.user.models.routes import ModelsListResource
from docsgpt.api.user.models.routes import ModelsListResource
mock_model = Mock()
mock_model.to_dict.return_value = {
@@ -28,7 +28,7 @@ class TestModelsListResource:
mock_registry.default_model_id = "gpt-4"
with patch(
"application.api.user.models.routes.ModelRegistry.get_instance",
"docsgpt.api.user.models.routes.ModelRegistry.get_instance",
return_value=mock_registry,
):
with app.test_request_context("/api/models"):
@@ -40,14 +40,14 @@ class TestModelsListResource:
assert response.json["models"][0]["id"] == "gpt-4"
def test_returns_empty_models(self, app):
from application.api.user.models.routes import ModelsListResource
from docsgpt.api.user.models.routes import ModelsListResource
mock_registry = Mock()
mock_registry.get_enabled_models.return_value = []
mock_registry.default_model_id = None
with patch(
"application.api.user.models.routes.ModelRegistry.get_instance",
"docsgpt.api.user.models.routes.ModelRegistry.get_instance",
return_value=mock_registry,
):
with app.test_request_context("/api/models"):
@@ -58,10 +58,10 @@ class TestModelsListResource:
assert response.json["models"] == []
def test_returns_500_on_error(self, app):
from application.api.user.models.routes import ModelsListResource
from docsgpt.api.user.models.routes import ModelsListResource
with patch(
"application.api.user.models.routes.ModelRegistry.get_instance",
"docsgpt.api.user.models.routes.ModelRegistry.get_instance",
side_effect=Exception("Registry error"),
):
with app.test_request_context("/api/models"):
+38 -38
View File
@@ -20,9 +20,9 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.prompts.routes.db_session", _yield_conn
"docsgpt.api.user.prompts.routes.db_session", _yield_conn
), patch(
"application.api.user.prompts.routes.db_readonly", _yield_conn
"docsgpt.api.user.prompts.routes.db_readonly", _yield_conn
):
yield
@@ -32,7 +32,7 @@ class TestCreatePrompt:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.prompts.routes import CreatePrompt
from docsgpt.api.user.prompts.routes import CreatePrompt
with app.test_request_context(
"/api/create_prompt",
@@ -47,7 +47,7 @@ class TestCreatePrompt:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.prompts.routes import CreatePrompt
from docsgpt.api.user.prompts.routes import CreatePrompt
with app.test_request_context(
"/api/create_prompt",
@@ -67,7 +67,7 @@ class TestGetPrompts:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.prompts.routes import GetPrompts
from docsgpt.api.user.prompts.routes import GetPrompts
with app.test_request_context("/api/get_prompts"):
from flask import request
@@ -83,9 +83,9 @@ class TestGetSinglePrompt:
pass
def test_returns_default_prompt(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
from docsgpt.api.user.prompts.routes import GetSinglePrompt
from application.prompts.composer import compose_preset
from docsgpt.prompts.composer import compose_preset
with app.test_request_context("/api/get_single_prompt?id=default"):
from flask import request
@@ -97,9 +97,9 @@ class TestGetSinglePrompt:
assert response.json["content"] == compose_preset("default")
def test_returns_creative_prompt(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
from docsgpt.api.user.prompts.routes import GetSinglePrompt
from application.prompts.composer import compose_preset
from docsgpt.prompts.composer import compose_preset
with app.test_request_context("/api/get_single_prompt?id=creative"):
from flask import request
@@ -111,9 +111,9 @@ class TestGetSinglePrompt:
assert response.json["content"] == compose_preset("creative")
def test_returns_strict_prompt(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
from docsgpt.api.user.prompts.routes import GetSinglePrompt
from application.prompts.composer import compose_preset
from docsgpt.prompts.composer import compose_preset
with app.test_request_context("/api/get_single_prompt?id=strict"):
from flask import request
@@ -126,7 +126,7 @@ class TestGetSinglePrompt:
def test_returns_400_missing_id(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
from docsgpt.api.user.prompts.routes import GetSinglePrompt
with app.test_request_context("/api/get_single_prompt"):
from flask import request
@@ -142,7 +142,7 @@ class TestDeletePrompt:
pass
def test_returns_400_missing_id(self, app):
from application.api.user.prompts.routes import DeletePrompt
from docsgpt.api.user.prompts.routes import DeletePrompt
with app.test_request_context(
"/api/delete_prompt",
@@ -162,7 +162,7 @@ class TestUpdatePrompt:
pass
def test_returns_400_missing_fields(self, app):
from application.api.user.prompts.routes import UpdatePrompt
from docsgpt.api.user.prompts.routes import UpdatePrompt
with app.test_request_context(
"/api/update_prompt",
@@ -184,7 +184,7 @@ class TestUpdatePrompt:
class TestCreatePromptHappyPath:
def test_creates_prompt_returns_id(self, app, pg_conn):
from application.api.user.prompts.routes import CreatePrompt
from docsgpt.api.user.prompts.routes import CreatePrompt
with _patch_db(pg_conn), app.test_request_context(
"/api/create_prompt",
@@ -200,7 +200,7 @@ class TestCreatePromptHappyPath:
assert "id" in response.json
def test_create_error_returns_400(self, app, pg_conn):
from application.api.user.prompts.routes import CreatePrompt
from docsgpt.api.user.prompts.routes import CreatePrompt
# Force repository error by closing the connection first
@contextmanager
@@ -209,7 +209,7 @@ class TestCreatePromptHappyPath:
yield # unreachable
with patch(
"application.api.user.prompts.routes.db_session", _broken
"docsgpt.api.user.prompts.routes.db_session", _broken
), app.test_request_context(
"/api/create_prompt",
method="POST",
@@ -225,7 +225,7 @@ class TestCreatePromptHappyPath:
class TestGetPromptsHappyPath:
def test_returns_builtin_plus_user_prompts(self, app, pg_conn):
from application.api.user.prompts.routes import CreatePrompt, GetPrompts
from docsgpt.api.user.prompts.routes import CreatePrompt, GetPrompts
user = "user-list"
# Seed two prompts via the same endpoint
@@ -253,7 +253,7 @@ class TestGetPromptsHappyPath:
assert "alpha" in names and "beta" in names
def test_get_error_returns_400(self, app):
from application.api.user.prompts.routes import GetPrompts
from docsgpt.api.user.prompts.routes import GetPrompts
@contextmanager
def _broken():
@@ -261,7 +261,7 @@ class TestGetPromptsHappyPath:
yield
with patch(
"application.api.user.prompts.routes.db_readonly", _broken
"docsgpt.api.user.prompts.routes.db_readonly", _broken
), app.test_request_context("/api/get_prompts"):
from flask import request
@@ -273,7 +273,7 @@ class TestGetPromptsHappyPath:
class TestGetSinglePromptHappyPath:
def test_returns_private_prompt_content(self, app, pg_conn):
from application.api.user.prompts.routes import (
from docsgpt.api.user.prompts.routes import (
CreatePrompt,
GetSinglePrompt,
)
@@ -302,7 +302,7 @@ class TestGetSinglePromptHappyPath:
assert response.json["content"] == "hello world"
def test_returns_404_for_unknown_prompt(self, app, pg_conn):
from application.api.user.prompts.routes import GetSinglePrompt
from docsgpt.api.user.prompts.routes import GetSinglePrompt
bogus_id = str(uuid.uuid4())
with _patch_db(pg_conn), app.test_request_context(
@@ -316,12 +316,12 @@ class TestGetSinglePromptHappyPath:
assert response.status_code == 404
def test_file_read_exception_returns_400(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
from docsgpt.api.user.prompts.routes import GetSinglePrompt
# Presets are composed in-process now, so the failure this covers is a
# repository error on the custom-prompt path.
with patch(
"application.api.user.prompts.routes.PromptsRepository",
"docsgpt.api.user.prompts.routes.PromptsRepository",
side_effect=OSError("boom"),
), app.test_request_context("/api/get_single_prompt?id=some-custom-id"):
from flask import request
@@ -334,7 +334,7 @@ class TestGetSinglePromptHappyPath:
class TestDeletePromptHappyPath:
def test_deletes_existing_prompt(self, app, pg_conn):
from application.api.user.prompts.routes import (
from docsgpt.api.user.prompts.routes import (
CreatePrompt,
DeletePrompt,
GetSinglePrompt,
@@ -376,7 +376,7 @@ class TestDeletePromptHappyPath:
assert check.status_code == 404
def test_delete_returns_401_unauthenticated(self, app):
from application.api.user.prompts.routes import DeletePrompt
from docsgpt.api.user.prompts.routes import DeletePrompt
with app.test_request_context(
"/api/delete_prompt",
@@ -391,7 +391,7 @@ class TestDeletePromptHappyPath:
assert response.status_code == 401
def test_delete_error_returns_400(self, app):
from application.api.user.prompts.routes import DeletePrompt
from docsgpt.api.user.prompts.routes import DeletePrompt
@contextmanager
def _broken():
@@ -399,7 +399,7 @@ class TestDeletePromptHappyPath:
yield
with patch(
"application.api.user.prompts.routes.db_session", _broken
"docsgpt.api.user.prompts.routes.db_session", _broken
), app.test_request_context(
"/api/delete_prompt",
method="POST",
@@ -422,14 +422,14 @@ class TestLegacyMongoIdResolution:
LEGACY_ID = "507f1f77bcf86cd799439011"
def _seed_legacy(self, pg_conn, user: str, name: str, content: str):
from application.storage.db.repositories.prompts import PromptsRepository
from docsgpt.storage.db.repositories.prompts import PromptsRepository
return PromptsRepository(pg_conn).create(
user, name, content, legacy_mongo_id=self.LEGACY_ID,
)
def test_get_single_prompt_resolves_legacy_id(self, app, pg_conn):
from application.api.user.prompts.routes import GetSinglePrompt
from docsgpt.api.user.prompts.routes import GetSinglePrompt
user = "legacy-user"
self._seed_legacy(pg_conn, user, "orig", "legacy-body")
@@ -446,8 +446,8 @@ class TestLegacyMongoIdResolution:
assert response.json["content"] == "legacy-body"
def test_delete_prompt_resolves_legacy_id(self, app, pg_conn):
from application.api.user.prompts.routes import DeletePrompt
from application.storage.db.repositories.prompts import PromptsRepository
from docsgpt.api.user.prompts.routes import DeletePrompt
from docsgpt.storage.db.repositories.prompts import PromptsRepository
user = "legacy-user-del"
self._seed_legacy(pg_conn, user, "to-delete", "x")
@@ -469,8 +469,8 @@ class TestLegacyMongoIdResolution:
) is None
def test_update_prompt_resolves_legacy_id(self, app, pg_conn):
from application.api.user.prompts.routes import UpdatePrompt
from application.storage.db.repositories.prompts import PromptsRepository
from docsgpt.api.user.prompts.routes import UpdatePrompt
from docsgpt.storage.db.repositories.prompts import PromptsRepository
user = "legacy-user-upd"
self._seed_legacy(pg_conn, user, "old-name", "old-content")
@@ -493,7 +493,7 @@ class TestLegacyMongoIdResolution:
class TestUpdatePromptHappyPath:
def test_updates_prompt(self, app, pg_conn):
from application.api.user.prompts.routes import (
from docsgpt.api.user.prompts.routes import (
CreatePrompt,
GetSinglePrompt,
UpdatePrompt,
@@ -533,7 +533,7 @@ class TestUpdatePromptHappyPath:
assert check.json["content"] == "v2"
def test_update_returns_401_unauthenticated(self, app):
from application.api.user.prompts.routes import UpdatePrompt
from docsgpt.api.user.prompts.routes import UpdatePrompt
with app.test_request_context(
"/api/update_prompt",
@@ -548,7 +548,7 @@ class TestUpdatePromptHappyPath:
assert response.status_code == 401
def test_update_error_returns_400(self, app):
from application.api.user.prompts.routes import UpdatePrompt
from docsgpt.api.user.prompts.routes import UpdatePrompt
@contextmanager
def _broken():
@@ -556,7 +556,7 @@ class TestUpdatePromptHappyPath:
yield
with patch(
"application.api.user.prompts.routes.db_session", _broken
"docsgpt.api.user.prompts.routes.db_session", _broken
), app.test_request_context(
"/api/update_prompt",
method="POST",
+38 -38
View File
@@ -24,7 +24,7 @@ from sqlalchemy import text
def _create_conv(conn, user_id: str = "u-1") -> dict:
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -198,7 +198,7 @@ def _route_engine_to(pg_conn):
fake_engine.begin = _fake_begin
with patch(
"application.api.user.reconciliation.get_engine",
"docsgpt.api.user.reconciliation.get_engine",
return_value=fake_engine,
):
yield
@@ -212,7 +212,7 @@ def _route_engine_to(pg_conn):
class TestStuckMessages:
@pytest.mark.unit
def test_first_two_attempts_increment_only(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
msg = _seed_pending_message(pg_conn)
@@ -235,13 +235,13 @@ class TestStuckMessages:
@pytest.mark.unit
def test_third_attempt_marks_failed_and_emits_alert(self, pg_conn, caplog):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
msg = _seed_pending_message(pg_conn)
before_logs = _stack_logs_count(pg_conn, "reconciler_message_failed")
with _route_engine_to(pg_conn), caplog.at_level(
logging.ERROR, logger="application.api.user.reconciliation",
logging.ERROR, logger="docsgpt.api.user.reconciliation",
):
run_reconciliation()
run_reconciliation()
@@ -274,7 +274,7 @@ class TestStuckMessages:
@pytest.mark.unit
def test_streaming_status_also_eligible(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
msg = _seed_pending_message(pg_conn, status="streaming")
with _route_engine_to(pg_conn):
@@ -293,7 +293,7 @@ class TestStuckMessages:
@pytest.mark.unit
def test_skipped_when_active_resuming_state(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
msg = _seed_pending_message(pg_conn)
# Active resume started 60 seconds ago — within 10-min grace.
@@ -316,7 +316,7 @@ class TestStuckMessages:
@pytest.mark.unit
def test_stale_resuming_does_not_skip(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
msg = _seed_pending_message(pg_conn)
# 11 minutes ago — past the 10-minute grace window.
@@ -339,7 +339,7 @@ class TestStuckMessages:
@pytest.mark.unit
def test_fresh_message_left_alone(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
# 1 minute old — well under the 5-minute threshold.
msg = _seed_pending_message(pg_conn, age_minutes=1)
@@ -364,7 +364,7 @@ class TestStuckMessages:
while the user is still considering tool approval. The PT row's
own ``expires_at`` TTL is the abandonment signal.
"""
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
msg = _seed_pending_message(pg_conn)
_seed_pending_state(
@@ -398,7 +398,7 @@ class TestStuckMessages:
hasn't reaped it yet, the message becomes eligible immediately
— we don't wait an extra ~60s for janitor cadence to align.
"""
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
msg = _seed_pending_message(pg_conn)
_seed_pending_state(
@@ -427,13 +427,13 @@ class TestStuckMessages:
class TestStuckProposedToolCalls:
@pytest.mark.unit
def test_marks_proposed_failed_with_alert(self, pg_conn, caplog):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
_seed_tool_call(pg_conn, call_id="cp-1", status="proposed", age_minutes=6)
before = _stack_logs_count(pg_conn, "reconciler_tool_call_failed_proposed")
with _route_engine_to(pg_conn), caplog.at_level(
logging.ERROR, logger="application.api.user.reconciliation",
logging.ERROR, logger="docsgpt.api.user.reconciliation",
):
r = run_reconciliation()
@@ -459,7 +459,7 @@ class TestStuckProposedToolCalls:
@pytest.mark.unit
def test_fresh_proposed_left_alone(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
_seed_tool_call(pg_conn, call_id="cp-2", status="proposed", age_minutes=2)
@@ -482,7 +482,7 @@ class TestStuckProposedToolCalls:
class TestStuckExecutedToolCalls:
@pytest.mark.unit
def test_executed_past_ttl_marked_failed_with_alert(self, pg_conn, caplog):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
_seed_tool_call(
pg_conn, call_id="ce-1", status="executed",
@@ -491,7 +491,7 @@ class TestStuckExecutedToolCalls:
before = _stack_logs_count(pg_conn, "reconciler_tool_call_failed_executed")
with _route_engine_to(pg_conn), caplog.at_level(
logging.ERROR, logger="application.api.user.reconciliation",
logging.ERROR, logger="docsgpt.api.user.reconciliation",
):
r = run_reconciliation()
@@ -517,7 +517,7 @@ class TestStuckExecutedToolCalls:
@pytest.mark.unit
def test_fresh_executed_left_alone(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
_seed_tool_call(
pg_conn, call_id="ce-2", status="executed",
@@ -615,21 +615,21 @@ def _capture_published(pg_conn):
return "1-0"
return patch(
"application.events.publisher.publish_user_event", _fake,
"docsgpt.events.publisher.publish_user_event", _fake,
), captured
class TestStalledIngests:
@pytest.mark.unit
def test_stalled_ingest_escalated_with_alert(self, pg_conn, caplog):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
sid = "1a000000-0000-0000-0000-0000000000a1"
_seed_ingest_progress(pg_conn, source_id=sid, embedded=9, total=907)
before = _stack_logs_count(pg_conn, "reconciler_ingest_stalled")
with _route_engine_to(pg_conn), caplog.at_level(
logging.ERROR, logger="application.api.user.reconciliation",
logging.ERROR, logger="docsgpt.api.user.reconciliation",
):
r = run_reconciliation()
@@ -652,7 +652,7 @@ class TestStalledIngests:
"""The escalate-to-'stalled' write ends the re-alert loop: a
second tick neither re-counts nor re-logs the same dead ingest.
"""
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
sid = "1a000000-0000-0000-0000-0000000000a2"
_seed_ingest_progress(pg_conn, source_id=sid, embedded=1, total=95)
@@ -672,7 +672,7 @@ class TestStalledIngests:
@pytest.mark.unit
def test_fresh_ingest_left_alone(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
sid = "1a000000-0000-0000-0000-0000000000a3"
# 2 minutes old — well under the 30-minute staleness threshold.
@@ -691,7 +691,7 @@ class TestStalledIngests:
"""A stale checkpoint that finished embedding (embedded == total)
is not a stall and must not be flagged.
"""
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
sid = "1a000000-0000-0000-0000-0000000000a4"
_seed_ingest_progress(pg_conn, source_id=sid, embedded=50, total=50)
@@ -747,7 +747,7 @@ class TestStuckIdempotencyPending:
already hit the poison-loop threshold gets escalated to failed
so a same-key retry can re-claim instead of waiting 24 h.
"""
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
_seed_stuck_idempotency_row(
pg_conn, key="abandoned", attempt_count=5, lease_secs_ago=120,
@@ -757,7 +757,7 @@ class TestStuckIdempotencyPending:
)
with _route_engine_to(pg_conn), caplog.at_level(
logging.ERROR, logger="application.api.user.reconciliation",
logging.ERROR, logger="docsgpt.api.user.reconciliation",
):
r = run_reconciliation()
@@ -789,7 +789,7 @@ class TestStuckIdempotencyPending:
"""Attempt count below the threshold means the wrapper might
still re-claim cleanly — leave the row alone.
"""
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
_seed_stuck_idempotency_row(
pg_conn, key="recoverable", attempt_count=2, lease_secs_ago=120,
@@ -810,7 +810,7 @@ class TestStuckIdempotencyPending:
"""A lease that just expired (10 s ago) might be in the
heartbeat-tick window; the 60 s grace keeps the sweep quiet.
"""
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
_seed_stuck_idempotency_row(
pg_conn, key="just-expired", attempt_count=5, lease_secs_ago=10,
@@ -834,7 +834,7 @@ class TestApprovalClearedEvents:
publishes ``tool.approval.cleared`` so the approval toast doesn't
linger after reconnect.
"""
from application.api.user import reconciliation as recon
from docsgpt.api.user import reconciliation as recon
msg = _seed_pending_message(pg_conn)
# Expired PT row: doesn't shield the message (past TTL) but is the
@@ -883,7 +883,7 @@ class TestApprovalClearedEvents:
"""A plain stuck message (no resumable state) must not emit a
spurious clearing event.
"""
from application.api.user import reconciliation as recon
from docsgpt.api.user import reconciliation as recon
_seed_pending_message(pg_conn)
@@ -899,7 +899,7 @@ class TestApprovalClearedEvents:
class TestStalledIngestEvent:
@pytest.mark.unit
def test_stalled_ingest_emits_source_failed_event(self, pg_conn):
from application.api.user import reconciliation as recon
from docsgpt.api.user import reconciliation as recon
sid = "1a000000-0000-0000-0000-0000000000b1"
_seed_source(pg_conn, source_id=sid, user_id="u-ingest", name="report.pdf")
@@ -923,7 +923,7 @@ class TestStalledIngestEvent:
"""An ingest row with no matching ``sources`` row (deleted source)
still escalates to 'stalled' but emits no user event.
"""
from application.api.user import reconciliation as recon
from docsgpt.api.user import reconciliation as recon
sid = "1a000000-0000-0000-0000-0000000000b2"
_seed_ingest_progress(pg_conn, source_id=sid, embedded=1, total=20)
@@ -945,11 +945,11 @@ class TestStalledIngestEvent:
class TestPostgresUriMissing:
@pytest.mark.unit
def test_returns_skip_dict(self, monkeypatch):
from application.api.user.reconciliation import (
from docsgpt.api.user.reconciliation import (
run_reconciliation,
zero_summary,
)
from application.core.settings import settings
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "POSTGRES_URI", None, raising=False)
@@ -977,7 +977,7 @@ class TestEventsSurviveAFailedSweep:
"""
def test_queued_events_are_published_when_a_later_sweep_raises(self, pg_conn):
from application.api.user.reconciliation import run_reconciliation
from docsgpt.api.user.reconciliation import run_reconciliation
# A stalled ingest: escalated (and its event queued) by the sweep that
# runs before the one we blow up.
@@ -1005,10 +1005,10 @@ class TestEventsSurviveAFailedSweep:
fake_engine.begin = _fake_begin
with patch(
"application.api.user.reconciliation.get_engine",
"docsgpt.api.user.reconciliation.get_engine",
return_value=fake_engine,
), patch(
"application.events.publisher.publish_user_event", _capture
"docsgpt.events.publisher.publish_user_event", _capture
):
with pytest.raises(RuntimeError):
run_reconciliation()
@@ -1053,7 +1053,7 @@ class TestPublishAfterCommit:
the user's upload toast to a terminal failure and leaving the next tick
to re-find the same rows and emit a duplicate.
"""
from application.api.user import reconciliation as rec
from docsgpt.api.user import reconciliation as rec
# ``get_engine`` caches a process-wide global, so pin it to this
# test's ephemeral DB rather than whichever one was created first.
@@ -1101,7 +1101,7 @@ class TestPublishAfterCommit:
"""The property the ``finally`` buys: an earlier sweep's events still
publish when a LATER sweep raises, because that sweep already committed.
"""
from application.api.user import reconciliation as rec
from docsgpt.api.user import reconciliation as rec
# ``get_engine`` caches a process-wide global, so pin it to this
# test's ephemeral DB rather than whichever one was created first.
+17 -17
View File
@@ -7,11 +7,11 @@ from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import text
from application.api.user.scheduler_dispatcher import dispatch_due_runs
from application.storage.db.repositories.schedule_runs import (
from docsgpt.api.user.scheduler_dispatcher import dispatch_due_runs
from docsgpt.storage.db.repositories.schedule_runs import (
ScheduleRunsRepository,
)
from application.storage.db.repositories.schedules import SchedulesRepository
from docsgpt.storage.db.repositories.schedules import SchedulesRepository
def _now() -> datetime:
@@ -32,7 +32,7 @@ def _make_agent(conn, user_id: str = "u1") -> str:
@pytest.fixture
def patched_engine(pg_engine, monkeypatch):
monkeypatch.setattr(
"application.api.user.scheduler_dispatcher.get_engine",
"docsgpt.api.user.scheduler_dispatcher.get_engine",
lambda: pg_engine,
)
yield pg_engine
@@ -50,7 +50,7 @@ def stub_enqueue(monkeypatch):
enqueued.append(args[0])
monkeypatch.setattr(
"application.api.user.tasks.execute_scheduled_run", _Task
"docsgpt.api.user.tasks.execute_scheduled_run", _Task
)
return enqueued
@@ -62,7 +62,7 @@ def _create_schedule(engine, **kwargs):
def _set_postgres_uri(monkeypatch, pg_engine):
monkeypatch.setattr(
"application.api.user.scheduler_dispatcher.settings",
"docsgpt.api.user.scheduler_dispatcher.settings",
type("S", (), {
"POSTGRES_URI": str(pg_engine.url),
"SCHEDULE_MISFIRE_GRACE": 60,
@@ -144,7 +144,7 @@ class TestMisfireGrace:
):
_set_postgres_uri(monkeypatch, pg_engine)
monkeypatch.setattr(
"application.api.user.scheduler_dispatcher.settings",
"docsgpt.api.user.scheduler_dispatcher.settings",
type("S", (), {
"POSTGRES_URI": str(pg_engine.url),
"SCHEDULE_MISFIRE_GRACE": 30,
@@ -264,24 +264,24 @@ class TestAgentlessRoundTrip:
):
from unittest.mock import patch
from application.api.user.scheduler_worker import (
from docsgpt.api.user.scheduler_worker import (
execute_scheduled_run_body,
)
_set_postgres_uri(monkeypatch, pg_engine)
monkeypatch.setattr(
"application.api.user.scheduler_worker.get_engine",
"docsgpt.api.user.scheduler_worker.get_engine",
lambda: pg_engine,
)
monkeypatch.setattr(
"application.api.user.scheduler_worker.settings",
"docsgpt.api.user.scheduler_worker.settings",
type("S", (), {
"POSTGRES_URI": str(pg_engine.url),
"SCHEDULE_AUTOPAUSE_FAILURES": 3,
})(),
)
monkeypatch.setattr(
"application.api.user.scheduler_worker.publish_user_event",
"docsgpt.api.user.scheduler_worker.publish_user_event",
lambda *a, **k: "1-0",
)
@@ -307,7 +307,7 @@ class TestAgentlessRoundTrip:
run_id = stub_enqueue[0]
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "agentless e2e done",
"tool_calls": [], "sources": [], "thought": "",
@@ -345,24 +345,24 @@ class TestOnceRoundTrip:
):
from unittest.mock import patch
from application.api.user.scheduler_worker import (
from docsgpt.api.user.scheduler_worker import (
execute_scheduled_run_body,
)
_set_postgres_uri(monkeypatch, pg_engine)
monkeypatch.setattr(
"application.api.user.scheduler_worker.get_engine",
"docsgpt.api.user.scheduler_worker.get_engine",
lambda: pg_engine,
)
monkeypatch.setattr(
"application.api.user.scheduler_worker.settings",
"docsgpt.api.user.scheduler_worker.settings",
type("S", (), {
"POSTGRES_URI": str(pg_engine.url),
"SCHEDULE_AUTOPAUSE_FAILURES": 3,
})(),
)
monkeypatch.setattr(
"application.api.user.scheduler_worker.publish_user_event",
"docsgpt.api.user.scheduler_worker.publish_user_event",
lambda *a, **k: "1-0",
)
with pg_engine.begin() as conn:
@@ -382,7 +382,7 @@ class TestOnceRoundTrip:
assert sched["status"] == "active"
assert sched["next_run_at"] is None
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "done",
"tool_calls": [], "sources": [], "thought": "",
+10 -10
View File
@@ -7,11 +7,11 @@ from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy import text
from application.api.user.reconciliation import run_reconciliation
from application.storage.db.repositories.schedule_runs import (
from docsgpt.api.user.reconciliation import run_reconciliation
from docsgpt.storage.db.repositories.schedule_runs import (
ScheduleRunsRepository,
)
from application.storage.db.repositories.schedules import SchedulesRepository
from docsgpt.storage.db.repositories.schedules import SchedulesRepository
def _now() -> datetime:
@@ -65,11 +65,11 @@ def _make_once_pending_run(conn, *, user_id="u1"):
@pytest.fixture
def patched_engine(pg_engine, monkeypatch):
monkeypatch.setattr(
"application.api.user.reconciliation.get_engine",
"docsgpt.api.user.reconciliation.get_engine",
lambda: pg_engine,
)
monkeypatch.setattr(
"application.api.user.reconciliation.settings",
"docsgpt.api.user.reconciliation.settings",
type("S", (), {
"POSTGRES_URI": str(pg_engine.url),
"SCHEDULE_RUN_TIMEOUT": 60,
@@ -233,24 +233,24 @@ class TestReconciler:
class TestCleanup:
def test_cleanup_schedule_runs_trims_old_rows(self, pg_engine, monkeypatch):
from application.api.user.tasks import cleanup_schedule_runs as _task
from docsgpt.api.user.tasks import cleanup_schedule_runs as _task
monkeypatch.setattr(
"application.storage.db.engine.get_engine",
"docsgpt.storage.db.engine.get_engine",
lambda: pg_engine,
)
class S:
POSTGRES_URI = str(pg_engine.url)
SCHEDULE_RUN_OUTPUT_RETENTION_DAYS = 30
monkeypatch.setattr("application.api.user.tasks.settings", S, raising=False)
monkeypatch.setattr("docsgpt.api.user.tasks.settings", S, raising=False)
monkeypatch.setattr(
"application.core.settings.settings.POSTGRES_URI",
"docsgpt.core.settings.settings.POSTGRES_URI",
str(pg_engine.url),
raising=False,
)
monkeypatch.setattr(
"application.core.settings.settings.SCHEDULE_RUN_OUTPUT_RETENTION_DAYS",
"docsgpt.core.settings.settings.SCHEDULE_RUN_OUTPUT_RETENTION_DAYS",
30,
raising=False,
)
+20 -20
View File
@@ -8,11 +8,11 @@ from unittest.mock import patch
import pytest
from sqlalchemy import text
from application.api.user.scheduler_worker import execute_scheduled_run_body
from application.storage.db.repositories.schedule_runs import (
from docsgpt.api.user.scheduler_worker import execute_scheduled_run_body
from docsgpt.storage.db.repositories.schedule_runs import (
ScheduleRunsRepository,
)
from application.storage.db.repositories.schedules import SchedulesRepository
from docsgpt.storage.db.repositories.schedules import SchedulesRepository
def _now() -> datetime:
@@ -49,11 +49,11 @@ def _make_pending_run(conn, *, user_id="u1"):
@pytest.fixture
def patched_engine(pg_engine, monkeypatch):
monkeypatch.setattr(
"application.api.user.scheduler_worker.get_engine",
"docsgpt.api.user.scheduler_worker.get_engine",
lambda: pg_engine,
)
monkeypatch.setattr(
"application.api.user.scheduler_worker.settings",
"docsgpt.api.user.scheduler_worker.settings",
type("S", (), {
"POSTGRES_URI": str(pg_engine.url),
"SCHEDULE_AUTOPAUSE_FAILURES": 2,
@@ -71,7 +71,7 @@ def stub_events(monkeypatch):
return "1-0"
monkeypatch.setattr(
"application.api.user.scheduler_worker.publish_user_event",
"docsgpt.api.user.scheduler_worker.publish_user_event",
_fake_publish,
)
return captured
@@ -82,7 +82,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "all done",
"tool_calls": [],
@@ -113,7 +113,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
side_effect=RuntimeError("boom"),
):
result = execute_scheduled_run_body(str(run["id"]), "celery-2")
@@ -139,7 +139,7 @@ class TestExecuteScheduledRunBody:
_now() + timedelta(seconds=1),
)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
side_effect=RuntimeError("boom"),
):
execute_scheduled_run_body(str(another_run["id"]), "celery-3")
@@ -154,7 +154,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "",
"tool_calls": [],
@@ -185,7 +185,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "",
"tool_calls": [],
@@ -223,7 +223,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "",
"tool_calls": [],
@@ -255,7 +255,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "",
"tool_calls": [{"tool_name": "telegram_send", "result": "ok"}],
@@ -290,7 +290,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "",
"tool_calls": [],
@@ -318,7 +318,7 @@ class TestExecuteScheduledRunBody:
with pg_engine.begin() as conn:
schedule, run, _ = _make_pending_run(conn)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "",
"tool_calls": [],
@@ -382,7 +382,7 @@ class TestExecuteScheduledRunBody:
"denied": [], "error_type": None, "model_id": "fake",
}
with patch(
"application.api.user.scheduler_worker.run_agent_headless", _fake_run,
"docsgpt.api.user.scheduler_worker.run_agent_headless", _fake_run,
):
execute_scheduled_run_body(str(run["id"]), "celery-h")
assert len(captured.get("chat_history", [])) == 1
@@ -423,7 +423,7 @@ class TestExecuteScheduledRunBody:
}
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
_fake_run,
):
result = execute_scheduled_run_body(str(run["id"]), "celery-agentless")
@@ -484,7 +484,7 @@ class TestExecuteScheduledRunBody:
}
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
_fake_run,
):
execute_scheduled_run_body(str(run["id"]), "celery-no-snap")
@@ -516,7 +516,7 @@ class TestExecuteScheduledRunBody:
str(schedule["id"]), "u1", None, _now(),
)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "yes",
"tool_calls": [], "sources": [], "thought": "",
@@ -562,7 +562,7 @@ class TestExecuteScheduledRunBody:
str(schedule["id"]), "u1", agent_id, _now(),
)
with patch(
"application.api.user.scheduler_worker.run_agent_headless",
"docsgpt.api.user.scheduler_worker.run_agent_headless",
return_value={
"answer": "scheduled answer",
"tool_calls": [],
+26 -26
View File
@@ -10,7 +10,7 @@ import pytest
from flask import Flask
from sqlalchemy import text
from application.storage.db.repositories.schedules import SchedulesRepository
from docsgpt.storage.db.repositories.schedules import SchedulesRepository
@pytest.fixture
@@ -25,9 +25,9 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.schedules.routes.db_session", _yield,
"docsgpt.api.user.schedules.routes.db_session", _yield,
), patch(
"application.api.user.schedules.routes.db_readonly", _yield,
"docsgpt.api.user.schedules.routes.db_readonly", _yield,
):
yield
@@ -49,7 +49,7 @@ def _make_agent(conn, user_id: str = "u1") -> str:
class TestCreateRecurring:
def test_unauthorized(self, app):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
with app.test_request_context(
"/api/agents/x/schedules", method="POST", json={},
@@ -60,7 +60,7 @@ class TestCreateRecurring:
assert resp.status_code == 401
def test_agent_not_found(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
with _patch_db(pg_conn), app.test_request_context(
"/api/agents/00000000-0000-0000-0000-000000000000/schedules",
@@ -74,7 +74,7 @@ class TestCreateRecurring:
assert resp.status_code == 404
def test_invalid_cron(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
with _patch_db(pg_conn), app.test_request_context(
@@ -88,7 +88,7 @@ class TestCreateRecurring:
assert resp.status_code == 400
def test_create_success(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
with _patch_db(pg_conn), app.test_request_context(
@@ -112,7 +112,7 @@ class TestCreateRecurring:
class TestCreateOnce:
def test_creates_once_with_run_at(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
run_at = (_now() + timedelta(hours=2)).isoformat().replace(
@@ -138,7 +138,7 @@ class TestCreateOnce:
assert body["schedule"]["run_at"] is not None
def test_once_requires_run_at(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
with _patch_db(pg_conn), app.test_request_context(
@@ -155,7 +155,7 @@ class TestCreateOnce:
assert resp.status_code == 400
def test_once_rejects_past_run_at(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
past = (_now() - timedelta(hours=1)).isoformat().replace(
@@ -177,7 +177,7 @@ class TestCreateOnce:
def test_recurring_default_when_trigger_type_omitted(self, app, pg_conn):
"""Backwards compat: a payload with cron but no trigger_type still works."""
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
with _patch_db(pg_conn), app.test_request_context(
@@ -198,7 +198,7 @@ class TestCreateOnce:
class TestListForAgent:
def test_list(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
SchedulesRepository(pg_conn).create(
@@ -221,7 +221,7 @@ class TestGetEditPatchDelete:
return SchedulesRepository(conn).create(**kwargs)
def test_get_owner_scoped(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = self._make(
@@ -239,7 +239,7 @@ class TestGetEditPatchDelete:
assert resp.status_code == 404
def test_pause_then_resume(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = self._make(
@@ -272,7 +272,7 @@ class TestGetEditPatchDelete:
assert body["schedule"]["next_run_at"] is not None
def test_delete_owner_scoped(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = self._make(
@@ -297,7 +297,7 @@ class TestGetEditPatchDelete:
assert resp.status_code == 200
def test_put_invalid_cron(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = self._make(
@@ -318,7 +318,7 @@ class TestGetEditPatchDelete:
class TestRunNow:
def test_runs_returns_202(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleRunNow
from docsgpt.api.user.schedules.routes import ScheduleRunNow
agent_id = _make_agent(pg_conn)
s = SchedulesRepository(pg_conn).create(
@@ -327,7 +327,7 @@ class TestRunNow:
next_run_at=_now() + timedelta(hours=1),
)
with _patch_db(pg_conn), patch(
"application.api.user.tasks.execute_scheduled_run",
"docsgpt.api.user.tasks.execute_scheduled_run",
type("T", (), {"apply_async": staticmethod(lambda **k: None)}),
), app.test_request_context(
f"/api/schedules/{s['id']}/run", method="POST",
@@ -339,7 +339,7 @@ class TestRunNow:
def test_second_run_blocked_by_active(self, app, pg_conn):
"""Run-Now serializes via FOR UPDATE + has_active_run; second 409s."""
from application.api.user.schedules.routes import ScheduleRunNow
from docsgpt.api.user.schedules.routes import ScheduleRunNow
agent_id = _make_agent(pg_conn)
s = SchedulesRepository(pg_conn).create(
@@ -348,7 +348,7 @@ class TestRunNow:
next_run_at=_now() + timedelta(hours=1),
)
with _patch_db(pg_conn), patch(
"application.api.user.tasks.execute_scheduled_run",
"docsgpt.api.user.tasks.execute_scheduled_run",
type("T", (), {"apply_async": staticmethod(lambda **k: None)}),
), app.test_request_context(
f"/api/schedules/{s['id']}/run", method="POST",
@@ -363,7 +363,7 @@ class TestRunNow:
class TestMinInterval:
def test_create_rejects_below_min_interval(self, app, pg_conn):
from application.api.user.schedules.routes import AgentSchedules
from docsgpt.api.user.schedules.routes import AgentSchedules
agent_id = _make_agent(pg_conn)
with _patch_db(pg_conn), app.test_request_context(
@@ -378,7 +378,7 @@ class TestMinInterval:
assert "minimum interval" in resp.get_json()["message"]
def test_put_rejects_below_min_interval(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = SchedulesRepository(pg_conn).create(
@@ -399,7 +399,7 @@ class TestMinInterval:
class TestResumeOnceStale:
def test_stale_run_at_returns_clear_409(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = SchedulesRepository(pg_conn).create(
@@ -426,7 +426,7 @@ class TestResumeOnceStale:
assert "elapsed" in resp.get_json()["message"]
def test_resume_accepts_new_run_at(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = SchedulesRepository(pg_conn).create(
@@ -457,7 +457,7 @@ class TestResumeOnceStale:
class TestRunList:
def test_list_owner_scoped(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleRunList
from docsgpt.api.user.schedules.routes import ScheduleRunList
agent_id = _make_agent(pg_conn)
s = SchedulesRepository(pg_conn).create(
@@ -478,7 +478,7 @@ class TestUnexpectedExceptionMasked:
"""Unexpected exceptions log full trace + return generic 500 (no leak)."""
def test_unexpected_repo_error_returns_generic_500(self, app, pg_conn):
from application.api.user.schedules.routes import ScheduleResource
from docsgpt.api.user.schedules.routes import ScheduleResource
agent_id = _make_agent(pg_conn)
s = SchedulesRepository(pg_conn).create(
+34 -34
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/sharing/routes.py.
"""Tests for docsgpt/api/user/sharing/routes.py.
Post-PG cutover: routes use the PG repositories (ConversationsRepository,
SharedConversationsRepository, AgentsRepository, AttachmentsRepository) and
@@ -25,15 +25,15 @@ def _patch_sharing_db(conn):
yield conn
with patch(
"application.api.user.sharing.routes.db_session", _yield_conn
"docsgpt.api.user.sharing.routes.db_session", _yield_conn
), patch(
"application.api.user.sharing.routes.db_readonly", _yield_conn
"docsgpt.api.user.sharing.routes.db_readonly", _yield_conn
):
yield
def _seed_conversation(pg_conn, user_id, name="Test Conv", message_count=0):
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
repo = ConversationsRepository(pg_conn)
@@ -52,7 +52,7 @@ def _seed_conversation(pg_conn, user_id, name="Test Conv", message_count=0):
@pytest.mark.unit
class TestShareConversation:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
with app.test_request_context(
"/api/share?isPromptable=false",
@@ -67,7 +67,7 @@ class TestShareConversation:
assert response.status_code == 401
def test_returns_400_missing_conversation_id(self, app):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
with app.test_request_context(
"/api/share?isPromptable=false",
@@ -82,7 +82,7 @@ class TestShareConversation:
assert response.status_code == 400
def test_returns_400_missing_isPromptable(self, app):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
with app.test_request_context(
"/api/share",
@@ -97,7 +97,7 @@ class TestShareConversation:
assert response.status_code == 400
def test_returns_404_for_missing_conversation(self, app, pg_conn):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
with _patch_sharing_db(pg_conn), app.test_request_context(
"/api/share?isPromptable=false",
@@ -112,7 +112,7 @@ class TestShareConversation:
assert response.status_code == 404
def test_creates_non_promptable_share(self, app, pg_conn):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
user = "user-npshare"
conv_id = _seed_conversation(pg_conn, user, message_count=3)
@@ -134,7 +134,7 @@ class TestShareConversation:
def test_reuse_non_promptable_share_returns_same_identifier(
self, app, pg_conn,
):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
user = "user-reuse-np"
conv_id = _seed_conversation(pg_conn, user, message_count=1)
@@ -154,7 +154,7 @@ class TestShareConversation:
assert ids[0] == ids[1]
def test_creates_promptable_share(self, app, pg_conn):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
user = "user-pshare"
conv_id = _seed_conversation(pg_conn, user, message_count=2)
@@ -173,7 +173,7 @@ class TestShareConversation:
assert response.json["success"] is True
def test_reuse_promptable_share_returns_200(self, app, pg_conn):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
user = "user-reuse-p"
conv_id = _seed_conversation(pg_conn, user, message_count=1)
@@ -202,7 +202,7 @@ class TestShareConversation:
assert second.status_code == 200
def test_promptable_with_invalid_chunks_coerces_none(self, app, pg_conn):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
user = "user-bad-chunks"
conv_id = _seed_conversation(pg_conn, user, message_count=1)
@@ -220,7 +220,7 @@ class TestShareConversation:
assert response.status_code == 201
def test_db_error_returns_400(self, app):
from application.api.user.sharing.routes import ShareConversation
from docsgpt.api.user.sharing.routes import ShareConversation
@contextmanager
def _broken():
@@ -228,7 +228,7 @@ class TestShareConversation:
yield
with patch(
"application.api.user.sharing.routes.db_session", _broken
"docsgpt.api.user.sharing.routes.db_session", _broken
), app.test_request_context(
"/api/share?isPromptable=false",
method="POST",
@@ -250,7 +250,7 @@ class TestShareConversation:
@pytest.mark.unit
class TestGetPubliclySharedConversations:
def test_returns_404_for_missing_identifier(self, app, pg_conn):
from application.api.user.sharing.routes import (
from docsgpt.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
@@ -264,7 +264,7 @@ class TestGetPubliclySharedConversations:
assert response.status_code == 404
def test_returns_shared_conversation(self, app, pg_conn):
from application.api.user.sharing.routes import (
from docsgpt.api.user.sharing.routes import (
GetPubliclySharedConversations,
ShareConversation,
)
@@ -298,7 +298,7 @@ class TestGetPubliclySharedConversations:
assert "api_key" not in data
def test_returns_api_key_for_promptable_share(self, app, pg_conn):
from application.api.user.sharing.routes import (
from docsgpt.api.user.sharing.routes import (
GetPubliclySharedConversations,
ShareConversation,
)
@@ -327,7 +327,7 @@ class TestGetPubliclySharedConversations:
assert response.json["api_key"]
def test_db_error_returns_400(self, app):
from application.api.user.sharing.routes import (
from docsgpt.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
@@ -337,7 +337,7 @@ class TestGetPubliclySharedConversations:
yield
with patch(
"application.api.user.sharing.routes.db_readonly", _broken
"docsgpt.api.user.sharing.routes.db_readonly", _broken
), app.test_request_context("/api/shared_conversation/abc"):
response = GetPubliclySharedConversations().get("abc")
@@ -352,15 +352,15 @@ class TestGetPubliclySharedConversations:
@pytest.mark.unit
class TestResolvePromptPgId:
def test_returns_none_for_default(self, pg_conn):
from application.api.user.sharing.routes import _resolve_prompt_pg_id
from docsgpt.api.user.sharing.routes import _resolve_prompt_pg_id
assert _resolve_prompt_pg_id(pg_conn, "default", "u") is None
assert _resolve_prompt_pg_id(pg_conn, "", "u") is None
assert _resolve_prompt_pg_id(pg_conn, None, "u") is None
def test_resolves_uuid_by_ownership(self, pg_conn):
from application.api.user.sharing.routes import _resolve_prompt_pg_id
from application.storage.db.repositories.prompts import PromptsRepository
from docsgpt.api.user.sharing.routes import _resolve_prompt_pg_id
from docsgpt.storage.db.repositories.prompts import PromptsRepository
prompt = PromptsRepository(pg_conn).create("owner", "p", "c")
pid = str(prompt["id"])
@@ -369,7 +369,7 @@ class TestResolvePromptPgId:
assert _resolve_prompt_pg_id(pg_conn, pid, "someone-else") is None
def test_returns_none_for_unknown_legacy(self, pg_conn):
from application.api.user.sharing.routes import _resolve_prompt_pg_id
from docsgpt.api.user.sharing.routes import _resolve_prompt_pg_id
assert _resolve_prompt_pg_id(pg_conn, "507f1f77bcf86cd799439011", "u") is None
@@ -377,13 +377,13 @@ class TestResolvePromptPgId:
@pytest.mark.unit
class TestResolveSourcePgId:
def test_returns_none_for_falsy(self, pg_conn):
from application.api.user.sharing.routes import _resolve_source_pg_id
from docsgpt.api.user.sharing.routes import _resolve_source_pg_id
assert _resolve_source_pg_id(pg_conn, None, "u") is None
assert _resolve_source_pg_id(pg_conn, "", "u") is None
def test_returns_none_for_unknown_uuid(self, pg_conn):
from application.api.user.sharing.routes import _resolve_source_pg_id
from docsgpt.api.user.sharing.routes import _resolve_source_pg_id
assert (
_resolve_source_pg_id(
@@ -393,7 +393,7 @@ class TestResolveSourcePgId:
)
def test_returns_none_for_unknown_legacy(self, pg_conn):
from application.api.user.sharing.routes import _resolve_source_pg_id
from docsgpt.api.user.sharing.routes import _resolve_source_pg_id
assert _resolve_source_pg_id(pg_conn, "507f1f77bcf86cd799439011", "u") is None
@@ -409,9 +409,9 @@ class TestShareSourceAuthorization:
def test_unauthorized_source_is_refused(self, monkeypatch, pg_conn):
"""A resolvable id the caller cannot read must not reach the agent."""
import application.api.user.team_sharing as ts
import docsgpt.api.user.team_sharing as ts
from application.api.user.sharing.routes import _resolve_source_pg_id
from docsgpt.api.user.sharing.routes import _resolve_source_pg_id
monkeypatch.setattr(ts, "can_access", lambda *a, **k: False)
assert (
@@ -422,20 +422,20 @@ class TestShareSourceAuthorization:
)
def test_missing_principal_resolves_nothing(self, pg_conn):
from application.api.user.sharing.routes import _resolve_source_pg_id
from docsgpt.api.user.sharing.routes import _resolve_source_pg_id
assert _resolve_source_pg_id(pg_conn, "any-id", None) is None
def test_authorized_source_passes_through(self, monkeypatch):
import application.api.user.team_sharing as ts
from application.api.user.sharing.routes import _authorized_source
import docsgpt.api.user.team_sharing as ts
from docsgpt.api.user.sharing.routes import _authorized_source
monkeypatch.setattr(ts, "can_access", lambda *a, **k: True)
assert _authorized_source(None, ("src-1",), "owner") == "src-1"
def test_denied_source_returns_none(self, monkeypatch):
import application.api.user.team_sharing as ts
from application.api.user.sharing.routes import _authorized_source
import docsgpt.api.user.team_sharing as ts
from docsgpt.api.user.sharing.routes import _authorized_source
monkeypatch.setattr(ts, "can_access", lambda *a, **k: False)
assert _authorized_source(None, ("src-1",), "stranger") is None
+85 -85
View File
@@ -14,18 +14,18 @@ def _patch_decorator_db(conn):
yield conn
with patch(
"application.api.user.idempotency.db_session", _yield
"docsgpt.api.user.idempotency.db_session", _yield
), patch(
"application.api.user.idempotency.db_readonly", _yield
"docsgpt.api.user.idempotency.db_readonly", _yield
):
yield
class TestIngestTask:
@pytest.mark.unit
@patch("application.api.user.tasks.ingest_worker")
@patch("docsgpt.api.user.tasks.ingest_worker")
def test_calls_ingest_worker(self, mock_worker):
from application.api.user.tasks import ingest
from docsgpt.api.user.tasks import ingest
mock_worker.return_value = {"status": "ok"}
@@ -38,9 +38,9 @@ class TestIngestTask:
assert result == {"status": "ok"}
@pytest.mark.unit
@patch("application.api.user.tasks.ingest_worker")
@patch("docsgpt.api.user.tasks.ingest_worker")
def test_passes_file_name_map(self, mock_worker):
from application.api.user.tasks import ingest
from docsgpt.api.user.tasks import ingest
mock_worker.return_value = {"status": "ok"}
name_map = {"a.pdf": "b.pdf"}
@@ -57,9 +57,9 @@ class TestIngestTask:
class TestIngestRemoteTask:
@pytest.mark.unit
@patch("application.api.user.tasks.remote_worker")
@patch("docsgpt.api.user.tasks.remote_worker")
def test_calls_remote_worker(self, mock_worker):
from application.api.user.tasks import ingest_remote
from docsgpt.api.user.tasks import ingest_remote
mock_worker.return_value = {"status": "ok"}
@@ -74,9 +74,9 @@ class TestIngestRemoteTask:
class TestReingestSourceTask:
@pytest.mark.unit
@patch("application.worker.reingest_source_worker")
@patch("docsgpt.worker.reingest_source_worker")
def test_calls_reingest_worker(self, mock_worker):
from application.api.user.tasks import reingest_source_task
from docsgpt.api.user.tasks import reingest_source_task
mock_worker.return_value = {"status": "ok"}
@@ -88,9 +88,9 @@ class TestReingestSourceTask:
class TestConvertSourceToWikiTask:
@pytest.mark.unit
@patch("application.worker.convert_source_to_wiki_worker")
@patch("docsgpt.worker.convert_source_to_wiki_worker")
def test_calls_convert_worker(self, mock_worker):
from application.api.user.tasks import convert_source_to_wiki
from docsgpt.api.user.tasks import convert_source_to_wiki
mock_worker.return_value = {"status": "converted"}
@@ -102,9 +102,9 @@ class TestConvertSourceToWikiTask:
class TestExtractGraphTask:
@pytest.mark.unit
@patch("application.worker.extract_graph_worker")
@patch("docsgpt.worker.extract_graph_worker")
def test_calls_extract_graph_worker(self, mock_worker):
from application.api.user.tasks import extract_graph
from docsgpt.api.user.tasks import extract_graph
mock_worker.return_value = {"nodes": 2, "edges": 1}
@@ -115,7 +115,7 @@ class TestExtractGraphTask:
@pytest.mark.unit
def test_repeat_with_same_key_short_circuits(self, pg_conn):
from application.api.user import tasks
from docsgpt.api.user import tasks
calls: list[str] = []
@@ -124,7 +124,7 @@ class TestExtractGraphTask:
return {"nodes": 1, "edges": 0}
with _patch_decorator_db(pg_conn), patch(
"application.worker.extract_graph_worker", _fake_worker
"docsgpt.worker.extract_graph_worker", _fake_worker
):
first = tasks.extract_graph(
"src-g", "user1", idempotency_key="extract-graph:src-g",
@@ -139,9 +139,9 @@ class TestExtractGraphTask:
class TestScheduleSyncsTask:
@pytest.mark.unit
@patch("application.api.user.tasks.sync_worker")
@patch("docsgpt.api.user.tasks.sync_worker")
def test_calls_sync_worker(self, mock_worker):
from application.api.user.tasks import schedule_syncs
from docsgpt.api.user.tasks import schedule_syncs
mock_worker.return_value = {"status": "ok"}
@@ -153,9 +153,9 @@ class TestScheduleSyncsTask:
class TestSyncSourceTask:
@pytest.mark.unit
@patch("application.api.user.tasks.sync")
@patch("docsgpt.api.user.tasks.sync")
def test_calls_sync(self, mock_sync):
from application.api.user.tasks import sync_source
from docsgpt.api.user.tasks import sync_source
mock_sync.return_value = {"status": "ok"}
@@ -171,9 +171,9 @@ class TestSyncSourceTask:
class TestStoreAttachmentTask:
@pytest.mark.unit
@patch("application.api.user.tasks.attachment_worker")
@patch("docsgpt.api.user.tasks.attachment_worker")
def test_calls_attachment_worker(self, mock_worker):
from application.api.user.tasks import store_attachment
from docsgpt.api.user.tasks import store_attachment
mock_worker.return_value = {"status": "ok"}
@@ -188,16 +188,16 @@ class TestStoreAttachmentTask:
# times just multiplies log noise for the same terminal failure.
from sqlalchemy.exc import DataError
from application.api.user.tasks import store_attachment
from docsgpt.api.user.tasks import store_attachment
assert DataError in getattr(store_attachment, "dont_autoretry_for", ())
class TestProcessAgentWebhookTask:
@pytest.mark.unit
@patch("application.api.user.tasks.agent_webhook_worker")
@patch("docsgpt.api.user.tasks.agent_webhook_worker")
def test_calls_agent_webhook_worker(self, mock_worker):
from application.api.user.tasks import process_agent_webhook
from docsgpt.api.user.tasks import process_agent_webhook
mock_worker.return_value = {"status": "ok"}
@@ -209,9 +209,9 @@ class TestProcessAgentWebhookTask:
class TestIngestConnectorTask:
@pytest.mark.unit
@patch("application.worker.ingest_connector")
@patch("docsgpt.worker.ingest_connector")
def test_calls_ingest_connector_defaults(self, mock_worker):
from application.api.user.tasks import ingest_connector_task
from docsgpt.api.user.tasks import ingest_connector_task
mock_worker.return_value = {"status": "ok"}
@@ -237,9 +237,9 @@ class TestIngestConnectorTask:
assert result == {"status": "ok"}
@pytest.mark.unit
@patch("application.worker.ingest_connector")
@patch("docsgpt.worker.ingest_connector")
def test_calls_ingest_connector_custom(self, mock_worker):
from application.api.user.tasks import ingest_connector_task
from docsgpt.api.user.tasks import ingest_connector_task
mock_worker.return_value = {"status": "ok"}
@@ -280,7 +280,7 @@ class TestIngestConnectorTask:
class TestSetupPeriodicTasks:
@pytest.mark.unit
def test_registers_periodic_tasks(self):
from application.api.user.tasks import setup_periodic_tasks
from docsgpt.api.user.tasks import setup_periodic_tasks
sender = MagicMock()
@@ -331,9 +331,9 @@ class TestSetupPeriodicTasks:
class TestMcpOauthTask:
@pytest.mark.unit
@patch("application.api.user.tasks.mcp_oauth")
@patch("docsgpt.api.user.tasks.mcp_oauth")
def test_calls_mcp_oauth(self, mock_worker):
from application.api.user.tasks import mcp_oauth_task
from docsgpt.api.user.tasks import mcp_oauth_task
mock_worker.return_value = {"url": "http://auth"}
@@ -347,9 +347,9 @@ class TestParseDocumentTask:
"""parse_document runs on the parsing queue under a bounded time limit."""
@pytest.mark.unit
@patch("application.api.user.tasks.parse_document_worker")
@patch("docsgpt.api.user.tasks.parse_document_worker")
def test_calls_parse_document_worker(self, mock_worker):
from application.api.user.tasks import parse_document
from docsgpt.api.user.tasks import parse_document
mock_worker.return_value = {"status": "ok", "content": "hi"}
@@ -364,11 +364,11 @@ class TestParseDocumentTask:
assert result == {"status": "ok", "content": "hi"}
@pytest.mark.unit
@patch("application.api.user.tasks.parse_document_worker")
@patch("docsgpt.api.user.tasks.parse_document_worker")
def test_soft_time_limit_returns_clean_error(self, mock_worker):
from celery.exceptions import SoftTimeLimitExceeded
from application.api.user.tasks import parse_document
from docsgpt.api.user.tasks import parse_document
mock_worker.side_effect = SoftTimeLimitExceeded("parse")
@@ -381,8 +381,8 @@ class TestParseDocumentTask:
@pytest.mark.unit
def test_time_limits_derived_from_document_parse_timeout(self):
from application.api.user.tasks import parse_document
from application.core.settings import settings
from docsgpt.api.user.tasks import parse_document
from docsgpt.core.settings import settings
assert parse_document.soft_time_limit == settings.DOCUMENT_PARSE_TIMEOUT
assert parse_document.time_limit == settings.DOCUMENT_PARSE_TIMEOUT + 30
@@ -409,7 +409,7 @@ class TestDurableTaskRetryPolicy:
],
)
def test_task_has_retry_config(self, task_name):
import application.api.user.tasks as tasks_module
import docsgpt.api.user.tasks as tasks_module
task = getattr(tasks_module, task_name)
assert task.acks_late is True
@@ -418,7 +418,7 @@ class TestDurableTaskRetryPolicy:
assert task.max_retries == 3
# ``retry_kwargs`` is deliberately unset: celery mutates that dict in
# place on every retry, so sharing one across the decorators would
# race. See the DURABLE_TASK comment in application/api/user/tasks.py.
# race. See the DURABLE_TASK comment in docsgpt/api/user/tasks.py.
assert not getattr(task, "retry_kwargs", None)
@pytest.mark.unit
@@ -435,7 +435,7 @@ class TestDurableTaskRetryPolicy:
],
)
def test_short_periodic_tasks_have_no_retry_config(self, task_name):
import application.api.user.tasks as tasks_module
import docsgpt.api.user.tasks as tasks_module
task = getattr(tasks_module, task_name)
assert not getattr(task, "autoretry_for", None)
@@ -446,7 +446,7 @@ class TestProcessAgentWebhookIdempotency:
@pytest.mark.unit
def test_repeat_with_same_key_short_circuits(self, pg_conn):
from application.api.user.tasks import process_agent_webhook
from docsgpt.api.user.tasks import process_agent_webhook
worker_calls = []
@@ -455,7 +455,7 @@ class TestProcessAgentWebhookIdempotency:
return {"status": "success", "result": {"answer": "ok"}}
with _patch_decorator_db(pg_conn), patch(
"application.api.user.tasks.agent_webhook_worker",
"docsgpt.api.user.tasks.agent_webhook_worker",
side_effect=_fake_worker,
):
first = process_agent_webhook(
@@ -477,11 +477,11 @@ class TestCleanupPendingToolState:
def test_reverts_stale_and_deletes_expired(self, pg_conn):
from sqlalchemy import text as _text
from application.api.user.tasks import cleanup_pending_tool_state
from application.storage.db.repositories.conversations import (
from docsgpt.api.user.tasks import cleanup_pending_tool_state
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
from application.storage.db.repositories.pending_tool_state import (
from docsgpt.storage.db.repositories.pending_tool_state import (
PendingToolStateRepository,
)
@@ -537,10 +537,10 @@ class TestCleanupPendingToolState:
fake_engine.begin = _fake_begin
with patch(
"application.storage.db.engine.get_engine",
"docsgpt.storage.db.engine.get_engine",
return_value=fake_engine,
), patch(
"application.api.answer.services.conversation_service."
"docsgpt.api.answer.services.conversation_service."
"ConversationService.finalize_message",
) as finalize_expired:
result = cleanup_pending_tool_state.run()
@@ -564,8 +564,8 @@ class TestCleanupPendingToolState:
@pytest.mark.unit
def test_skips_when_postgres_uri_missing(self, monkeypatch):
from application.api.user.tasks import cleanup_pending_tool_state
from application.core.settings import settings
from docsgpt.api.user.tasks import cleanup_pending_tool_state
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "POSTGRES_URI", None, raising=False)
@@ -582,8 +582,8 @@ class TestCleanupMessageEventsTask:
@pytest.mark.unit
def test_skips_when_postgres_uri_missing(self, monkeypatch):
from application.api.user.tasks import cleanup_message_events
from application.core.settings import settings
from docsgpt.api.user.tasks import cleanup_message_events
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "POSTGRES_URI", None, raising=False)
@@ -596,9 +596,9 @@ class TestCleanupMessageEventsTask:
from sqlalchemy import text as _text
from application.api.user.tasks import cleanup_message_events
from application.core.settings import settings
from application.storage.db.repositories.message_events import (
from docsgpt.api.user.tasks import cleanup_message_events
from docsgpt.core.settings import settings
from docsgpt.storage.db.repositories.message_events import (
MessageEventsRepository,
)
@@ -650,7 +650,7 @@ class TestCleanupMessageEventsTask:
fake_engine.begin = _fake_begin
with patch(
"application.storage.db.engine.get_engine",
"docsgpt.storage.db.engine.get_engine",
return_value=fake_engine,
):
result = cleanup_message_events.run()
@@ -671,8 +671,8 @@ class TestCleanupOrphanMemoriesTask:
@pytest.mark.unit
def test_skips_when_postgres_uri_missing(self, monkeypatch):
from application.api.user.tasks import cleanup_orphan_memories
from application.core.settings import settings
from docsgpt.api.user.tasks import cleanup_orphan_memories
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "POSTGRES_URI", None, raising=False)
@@ -687,10 +687,10 @@ class TestCleanupOrphanMemoriesTask:
from sqlalchemy import text as _text
from application.agents.default_tools import default_tool_id
from application.api.user.tasks import cleanup_orphan_memories
from application.core.settings import settings
from application.storage.db.repositories.memories import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tasks import cleanup_orphan_memories
from docsgpt.core.settings import settings
from docsgpt.storage.db.repositories.memories import (
MemoriesRepository,
)
@@ -721,7 +721,7 @@ class TestCleanupOrphanMemoriesTask:
fake_engine.begin = _fake_begin
with patch(
"application.storage.db.engine.get_engine",
"docsgpt.storage.db.engine.get_engine",
return_value=fake_engine,
):
result = cleanup_orphan_memories.run()
@@ -737,7 +737,7 @@ class TestIngestIdempotency:
@pytest.mark.unit
def test_repeat_with_same_key_short_circuits(self, pg_conn):
from application.api.user.tasks import ingest
from docsgpt.api.user.tasks import ingest
worker_calls = []
@@ -748,7 +748,7 @@ class TestIngestIdempotency:
return {"status": "ok", "directory": directory}
with _patch_decorator_db(pg_conn), patch(
"application.api.user.tasks.ingest_worker",
"docsgpt.api.user.tasks.ingest_worker",
side_effect=_fake_worker,
):
first = ingest(
@@ -772,7 +772,7 @@ class TestIngestPoisonEvent:
@pytest.mark.unit
def test_publishes_failed_event(self):
from application.api.user.tasks import _emit_ingest_poison_event
from docsgpt.api.user.tasks import _emit_ingest_poison_event
published = []
@@ -780,7 +780,7 @@ class TestIngestPoisonEvent:
published.append((user, event_type, payload, scope))
with patch(
"application.events.publisher.publish_user_event",
"docsgpt.events.publisher.publish_user_event",
side_effect=_fake_publish,
):
_emit_ingest_poison_event(
@@ -799,10 +799,10 @@ class TestIngestPoisonEvent:
@pytest.mark.unit
def test_skips_when_source_id_missing(self):
from application.api.user.tasks import _emit_ingest_poison_event
from docsgpt.api.user.tasks import _emit_ingest_poison_event
with patch(
"application.events.publisher.publish_user_event",
"docsgpt.events.publisher.publish_user_event",
) as mock_publish:
_emit_ingest_poison_event("ingest", {"user": "u1"})
@@ -810,11 +810,11 @@ class TestIngestPoisonEvent:
@pytest.mark.unit
def test_reingest_uses_reingest_operation(self):
from application.api.user.tasks import _emit_ingest_poison_event
from docsgpt.api.user.tasks import _emit_ingest_poison_event
published = []
with patch(
"application.events.publisher.publish_user_event",
"docsgpt.events.publisher.publish_user_event",
side_effect=lambda *a, **k: published.append((a, k)),
):
_emit_ingest_poison_event(
@@ -830,8 +830,8 @@ def test_bare_worker_consumes_app_and_parsing_queues():
"""task_queues declares every queue, so a worker started without -Q serves
both app tasks and document parsing — a -Q-less dev worker must never
silently strand attachment uploads or parse_document tasks."""
import application.celeryconfig as celeryconfig
from application.core.settings import settings
import docsgpt.celeryconfig as celeryconfig
from docsgpt.core.settings import settings
names = {queue.name for queue in celeryconfig.task_queues}
assert "docsgpt" in names
@@ -843,8 +843,8 @@ class TestParseTimeoutForSize:
@pytest.mark.unit
def test_unknown_size_uses_the_base_timeout(self):
from application.api.user.tasks import parse_timeout_for_size
from application.core.settings import settings
from docsgpt.api.user.tasks import parse_timeout_for_size
from docsgpt.core.settings import settings
base = float(settings.DOCUMENT_PARSE_TIMEOUT)
assert parse_timeout_for_size(None) == base
@@ -855,8 +855,8 @@ class TestParseTimeoutForSize:
@pytest.mark.unit
def test_window_grows_with_the_document_size(self, monkeypatch):
from application.api.user import tasks
from application.core.settings import settings
from docsgpt.api.user import tasks
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "DOCUMENT_PARSE_TIMEOUT", 120, raising=False)
monkeypatch.setattr(settings, "DOCUMENT_PARSE_TIMEOUT_PER_MB", 60, raising=False)
@@ -869,8 +869,8 @@ class TestParseTimeoutForSize:
@pytest.mark.unit
def test_window_is_capped(self, monkeypatch):
from application.api.user import tasks
from application.core.settings import settings
from docsgpt.api.user import tasks
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "DOCUMENT_PARSE_TIMEOUT", 120, raising=False)
monkeypatch.setattr(settings, "DOCUMENT_PARSE_TIMEOUT_PER_MB", 60, raising=False)
@@ -881,8 +881,8 @@ class TestParseTimeoutForSize:
@pytest.mark.unit
def test_scaling_disabled_by_zero_per_mb(self, monkeypatch):
from application.api.user import tasks
from application.core.settings import settings
from docsgpt.api.user import tasks
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "DOCUMENT_PARSE_TIMEOUT", 120, raising=False)
monkeypatch.setattr(settings, "DOCUMENT_PARSE_TIMEOUT_PER_MB", 0, raising=False)
@@ -891,7 +891,7 @@ class TestParseTimeoutForSize:
@pytest.mark.unit
def test_task_time_limits_track_the_awaited_window(self):
from application.api.user.tasks import parse_document, parse_task_time_limits
from docsgpt.api.user.tasks import parse_document, parse_task_time_limits
limits = parse_task_time_limits(420.0)
assert limits == {"soft_time_limit": 420, "time_limit": 450}
@@ -907,8 +907,8 @@ class TestReconciliationTaskShape:
@pytest.mark.unit
def test_error_fallback_matches_the_real_summary_keys(self):
from application.api.user import reconciliation
from application.api.user.tasks import reconciliation_task
from docsgpt.api.user import reconciliation
from docsgpt.api.user.tasks import reconciliation_task
with patch.object(
reconciliation, "run_reconciliation", side_effect=RuntimeError("db down")
@@ -926,7 +926,7 @@ class TestReconciliationTaskShape:
@pytest.mark.unit
def test_skipped_tick_reports_the_same_counters(self, monkeypatch):
from application.api.user import reconciliation
from docsgpt.api.user import reconciliation
monkeypatch.setattr(reconciliation.settings, "POSTGRES_URI", "", raising=False)
result = reconciliation.run_reconciliation()
@@ -8,10 +8,10 @@ from contextlib import contextmanager
import pytest
from flask import request
from application.storage.db.repositories.artifacts import ArtifactsRepository
from application.storage.db.repositories.conversations import ConversationsRepository
from application.storage.db.repositories.notes import NotesRepository
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.artifacts import ArtifactsRepository
from docsgpt.storage.db.repositories.conversations import ConversationsRepository
from docsgpt.storage.db.repositories.notes import NotesRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
@pytest.fixture
@@ -22,13 +22,13 @@ def _patch_db(pg_conn, monkeypatch):
def _use_conn():
yield pg_conn
monkeypatch.setattr("application.api.user.tools.routes.db_readonly", _use_conn)
monkeypatch.setattr("application.api.user.artifacts.routes.db_readonly", _use_conn)
monkeypatch.setattr("docsgpt.api.user.tools.routes.db_readonly", _use_conn)
monkeypatch.setattr("docsgpt.api.user.artifacts.routes.db_readonly", _use_conn)
return pg_conn
def _get(flask_app, artifact_id, token):
from application.api.user.tools.routes import GetArtifact
from docsgpt.api.user.tools.routes import GetArtifact
with flask_app.app_context():
with flask_app.test_request_context():
+127 -127
View File
@@ -1,4 +1,4 @@
"""Unit tests for application.api.user.tools.mcp."""
"""Unit tests for docsgpt.api.user.tools.mcp."""
import json
import uuid
@@ -41,7 +41,7 @@ def app():
@pytest.fixture(autouse=True)
def _bypass_url_validation():
"""Bypass SSRF URL validation so tests using localhost URLs can proceed."""
with patch("application.api.user.tools.mcp.validate_url"):
with patch("docsgpt.api.user.tools.mcp.validate_url"):
yield
@@ -52,7 +52,7 @@ def _bypass_url_validation():
class TestSanitizeMcpTransport:
def test_defaults_to_auto(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
config = {}
result = _sanitize_mcp_transport(config)
@@ -60,7 +60,7 @@ class TestSanitizeMcpTransport:
assert config["transport_type"] == "auto"
def test_accepts_sse(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
config = {"transport_type": "SSE"}
result = _sanitize_mcp_transport(config)
@@ -68,21 +68,21 @@ class TestSanitizeMcpTransport:
assert config["transport_type"] == "sse"
def test_accepts_http(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
config = {"transport_type": "HTTP"}
result = _sanitize_mcp_transport(config)
assert result == "http"
def test_rejects_unsupported_transport(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
config = {"transport_type": "stdio"}
with pytest.raises(ValueError, match="Unsupported transport_type"):
_sanitize_mcp_transport(config)
def test_strips_command_and_args(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
config = {
"transport_type": "auto",
@@ -94,7 +94,7 @@ class TestSanitizeMcpTransport:
assert "args" not in config
def test_handles_none_transport_type(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
config = {"transport_type": None}
result = _sanitize_mcp_transport(config)
@@ -108,7 +108,7 @@ class TestSanitizeMcpTransport:
class TestExtractAuthCredentials:
def test_api_key_auth(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {
"auth_type": "api_key",
@@ -119,7 +119,7 @@ class TestExtractAuthCredentials:
assert result == {"api_key": "my-key", "api_key_header": "X-API-Key"}
def test_api_key_auth_only_key(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {"auth_type": "api_key", "api_key": "my-key"}
result = _extract_auth_credentials(config)
@@ -127,49 +127,49 @@ class TestExtractAuthCredentials:
assert "api_key_header" not in result
def test_bearer_auth(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {"auth_type": "bearer", "bearer_token": "tok123"}
result = _extract_auth_credentials(config)
assert result == {"bearer_token": "tok123"}
def test_bearer_auth_empty_token(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {"auth_type": "bearer"}
result = _extract_auth_credentials(config)
assert result == {}
def test_basic_auth(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {"auth_type": "basic", "username": "user", "password": "pass"}
result = _extract_auth_credentials(config)
assert result == {"username": "user", "password": "pass"}
def test_basic_auth_partial(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {"auth_type": "basic", "username": "user"}
result = _extract_auth_credentials(config)
assert result == {"username": "user"}
def test_none_auth(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {"auth_type": "none"}
result = _extract_auth_credentials(config)
assert result == {}
def test_default_no_auth_type(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {}
result = _extract_auth_credentials(config)
assert result == {}
def test_unknown_auth_type(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
config = {"auth_type": "oauth"}
result = _extract_auth_credentials(config)
@@ -183,7 +183,7 @@ class TestExtractAuthCredentials:
class TestTestMCPServerConfig:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with app.test_request_context(
"/api/mcp_server/test",
@@ -198,7 +198,7 @@ class TestTestMCPServerConfig:
assert response.status_code == 401
def test_returns_400_missing_config(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with app.test_request_context(
"/api/mcp_server/test",
@@ -213,7 +213,7 @@ class TestTestMCPServerConfig:
assert response.status_code == 400
def test_returns_400_unsupported_transport(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with app.test_request_context(
"/api/mcp_server/test",
@@ -229,7 +229,7 @@ class TestTestMCPServerConfig:
assert "Unsupported transport_type" in response.json["error"]
def test_successful_connection_test(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
mock_mcp_tool = Mock()
mock_mcp_tool.test_connection.return_value = {
@@ -238,7 +238,7 @@ class TestTestMCPServerConfig:
}
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
):
with app.test_request_context(
@@ -261,7 +261,7 @@ class TestTestMCPServerConfig:
assert response.json["success"] is True
def test_returns_oauth_required(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
mock_mcp_tool = Mock()
mock_mcp_tool.test_connection.return_value = {
@@ -270,7 +270,7 @@ class TestTestMCPServerConfig:
}
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
):
with app.test_request_context(
@@ -293,7 +293,7 @@ class TestTestMCPServerConfig:
assert response.json["requires_oauth"] is True
def test_redacts_failure_message(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
mock_mcp_tool = Mock()
mock_mcp_tool.test_connection.return_value = {
@@ -302,7 +302,7 @@ class TestTestMCPServerConfig:
}
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
):
with app.test_request_context(
@@ -325,10 +325,10 @@ class TestTestMCPServerConfig:
assert response.json["message"] == "Connection test failed"
def test_returns_500_on_exception(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
side_effect=RuntimeError("boom"),
):
with app.test_request_context(
@@ -351,13 +351,13 @@ class TestTestMCPServerConfig:
assert "Connection test failed" in response.json["error"]
def test_passes_auth_credentials_to_mcp_tool(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
mock_mcp_tool = Mock()
mock_mcp_tool.test_connection.return_value = {"success": True}
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
) as mock_cls:
with app.test_request_context(
@@ -390,7 +390,7 @@ class TestTestMCPServerConfig:
class TestMCPServerSave:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save",
@@ -405,7 +405,7 @@ class TestMCPServerSave:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save",
@@ -420,7 +420,7 @@ class TestMCPServerSave:
assert response.status_code == 400
def test_returns_400_unsupported_transport(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save",
@@ -439,7 +439,7 @@ class TestMCPServerSave:
assert "Unsupported transport_type" in response.json["error"]
def test_creates_new_mcp_server_no_auth(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
inserted_id = _FakeOid()
mock_mcp_tool = Mock()
@@ -451,10 +451,10 @@ class TestMCPServerSave:
mock_collection.insert_one.return_value = Mock(inserted_id=inserted_id)
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context(
@@ -481,7 +481,7 @@ class TestMCPServerSave:
mock_collection.insert_one.assert_called_once()
def test_creates_with_bearer_auth(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
inserted_id = _FakeOid()
mock_mcp_tool = Mock()
@@ -491,13 +491,13 @@ class TestMCPServerSave:
mock_collection.insert_one.return_value = Mock(inserted_id=inserted_id)
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
), patch(
"application.api.user.tools.mcp.encrypt_credentials",
"docsgpt.api.user.tools.mcp.encrypt_credentials",
return_value="enc-blob",
):
with app.test_request_context(
@@ -524,7 +524,7 @@ class TestMCPServerSave:
assert "bearer_token" not in call_arg["config"]
def test_updates_existing_mcp_server(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
tool_id = _FakeOid()
mock_mcp_tool = Mock()
@@ -541,10 +541,10 @@ class TestMCPServerSave:
mock_collection.update_one.return_value = Mock(matched_count=1)
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context(
@@ -571,7 +571,7 @@ class TestMCPServerSave:
assert "updated" in response.json["message"].lower()
def test_returns_404_update_not_found(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
tool_id = _FakeOid()
mock_mcp_tool = Mock()
@@ -582,10 +582,10 @@ class TestMCPServerSave:
mock_collection.update_one.return_value = Mock(matched_count=0)
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context(
@@ -609,7 +609,7 @@ class TestMCPServerSave:
assert response.status_code == 404
def test_oauth_auth_without_task_id(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save",
@@ -632,16 +632,16 @@ class TestMCPServerSave:
assert "OAuth authorization" in response.json["error"]
def test_oauth_auth_not_completed(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
mock_manager = Mock()
mock_manager.get_oauth_status.return_value = {"status": "pending"}
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=Mock(),
), patch(
"application.api.user.tools.mcp.MCPOAuthManager",
"docsgpt.api.user.tools.mcp.MCPOAuthManager",
return_value=mock_manager,
):
with app.test_request_context(
@@ -666,7 +666,7 @@ class TestMCPServerSave:
assert "OAuth failed" in response.json["error"]
def test_oauth_auth_completed_successfully(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
inserted_id = _FakeOid()
mock_manager = Mock()
@@ -678,13 +678,13 @@ class TestMCPServerSave:
mock_collection.insert_one.return_value = Mock(inserted_id=inserted_id)
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=Mock(),
), patch(
"application.api.user.tools.mcp.MCPOAuthManager",
"docsgpt.api.user.tools.mcp.MCPOAuthManager",
return_value=mock_manager,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context(
@@ -709,7 +709,7 @@ class TestMCPServerSave:
assert response.json["success"] is True
def test_no_credentials_for_non_none_auth_raises(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save",
@@ -731,10 +731,10 @@ class TestMCPServerSave:
assert response.status_code == 500
def test_returns_500_on_exception(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
side_effect=RuntimeError("boom"),
):
with app.test_request_context(
@@ -758,7 +758,7 @@ class TestMCPServerSave:
assert "Failed to save MCP server" in response.json["error"]
def test_strips_sensitive_fields_from_storage(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
inserted_id = _FakeOid()
mock_mcp_tool = Mock()
@@ -768,13 +768,13 @@ class TestMCPServerSave:
mock_collection.insert_one.return_value = Mock(inserted_id=inserted_id)
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
), patch(
"application.api.user.tools.mcp.encrypt_credentials",
"docsgpt.api.user.tools.mcp.encrypt_credentials",
return_value="enc",
):
with app.test_request_context(
@@ -806,7 +806,7 @@ class TestMCPServerSave:
assert field not in stored_config
def test_merges_existing_encrypted_credentials_on_update(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
tool_id = _FakeOid()
mock_mcp_tool = Mock()
@@ -820,16 +820,16 @@ class TestMCPServerSave:
mock_collection.update_one.return_value = Mock(matched_count=1)
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
), patch(
"application.api.user.tools.mcp.decrypt_credentials",
"docsgpt.api.user.tools.mcp.decrypt_credentials",
return_value={"api_key": "old-key"},
), patch(
"application.api.user.tools.mcp.encrypt_credentials",
"docsgpt.api.user.tools.mcp.encrypt_credentials",
return_value="merged-enc",
) as mock_encrypt:
with app.test_request_context(
@@ -856,7 +856,7 @@ class TestMCPServerSave:
assert merged_call["api_key"] == "new-key"
def test_preserves_existing_encrypted_when_no_new_credentials(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
tool_id = _FakeOid()
mock_mcp_tool = Mock()
@@ -870,10 +870,10 @@ class TestMCPServerSave:
mock_collection.update_one.return_value = Mock(matched_count=1)
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=mock_mcp_tool,
), patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context(
@@ -906,7 +906,7 @@ class TestMCPServerSave:
class TestMCPOAuthCallback:
def test_redirects_on_error_param(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with app.test_request_context(
"/api/mcp_server/callback?error=access_denied&code=abc&state=xyz"
@@ -918,7 +918,7 @@ class TestMCPOAuthCallback:
assert "access_denied" in response.headers["Location"]
def test_redirects_on_missing_code_or_state(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with app.test_request_context("/api/mcp_server/callback"):
response = MCPOAuthCallback().get()
@@ -927,7 +927,7 @@ class TestMCPOAuthCallback:
assert "error" in response.headers["Location"]
def test_redirects_on_missing_code(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with app.test_request_context("/api/mcp_server/callback?state=xyz"):
response = MCPOAuthCallback().get()
@@ -936,16 +936,16 @@ class TestMCPOAuthCallback:
assert "error" in response.headers["Location"]
def test_redirects_success_on_valid_callback(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
mock_manager = Mock()
mock_manager.handle_oauth_callback.return_value = True
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=Mock(),
), patch(
"application.api.user.tools.mcp.MCPOAuthManager",
"docsgpt.api.user.tools.mcp.MCPOAuthManager",
return_value=mock_manager,
):
with app.test_request_context(
@@ -957,16 +957,16 @@ class TestMCPOAuthCallback:
assert "success" in response.headers["Location"]
def test_redirects_error_on_failed_callback(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
mock_manager = Mock()
mock_manager.handle_oauth_callback.return_value = False
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=Mock(),
), patch(
"application.api.user.tools.mcp.MCPOAuthManager",
"docsgpt.api.user.tools.mcp.MCPOAuthManager",
return_value=mock_manager,
):
with app.test_request_context(
@@ -979,10 +979,10 @@ class TestMCPOAuthCallback:
assert "failed" in response.headers["Location"].lower()
def test_redirects_error_when_redis_unavailable(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=None,
):
with app.test_request_context(
@@ -994,10 +994,10 @@ class TestMCPOAuthCallback:
assert "Redis" in response.headers["Location"]
def test_redirects_error_on_exception(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
side_effect=RuntimeError("redis down"),
):
with app.test_request_context(
@@ -1016,13 +1016,13 @@ class TestMCPOAuthCallback:
class TestMCPOAuthStatus:
def test_returns_pending_when_no_status(self, app):
from application.api.user.tools.mcp import MCPOAuthStatus
from docsgpt.api.user.tools.mcp import MCPOAuthStatus
mock_redis = Mock()
mock_redis.get.return_value = None
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=mock_redis,
):
with app.test_request_context("/api/mcp_server/oauth_status/task123"):
@@ -1033,7 +1033,7 @@ class TestMCPOAuthStatus:
assert response.json["task_id"] == "task123"
def test_returns_status_with_tools(self, app):
from application.api.user.tools.mcp import MCPOAuthStatus
from docsgpt.api.user.tools.mcp import MCPOAuthStatus
status_data = {
"status": "completed",
@@ -1046,7 +1046,7 @@ class TestMCPOAuthStatus:
mock_redis.get.return_value = json.dumps(status_data)
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=mock_redis,
):
with app.test_request_context("/api/mcp_server/oauth_status/task123"):
@@ -1060,14 +1060,14 @@ class TestMCPOAuthStatus:
assert "extra" not in tools[0]
def test_returns_status_without_tools(self, app):
from application.api.user.tools.mcp import MCPOAuthStatus
from docsgpt.api.user.tools.mcp import MCPOAuthStatus
status_data = {"status": "in_progress", "message": "Authorizing..."}
mock_redis = Mock()
mock_redis.get.return_value = json.dumps(status_data)
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=mock_redis,
):
with app.test_request_context("/api/mcp_server/oauth_status/task123"):
@@ -1077,10 +1077,10 @@ class TestMCPOAuthStatus:
assert response.json["status"] == "in_progress"
def test_returns_500_on_exception(self, app):
from application.api.user.tools.mcp import MCPOAuthStatus
from docsgpt.api.user.tools.mcp import MCPOAuthStatus
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
side_effect=RuntimeError("redis down"),
):
with app.test_request_context("/api/mcp_server/oauth_status/task123"):
@@ -1097,7 +1097,7 @@ class TestMCPOAuthStatus:
class TestMCPAuthStatus:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
with app.test_request_context("/api/mcp_server/auth_status"):
from flask import request
@@ -1108,13 +1108,13 @@ class TestMCPAuthStatus:
assert response.status_code == 401
def test_returns_empty_statuses_when_no_mcp_tools(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
mock_collection = Mock()
mock_collection.find.return_value = []
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1127,7 +1127,7 @@ class TestMCPAuthStatus:
assert response.json["statuses"] == {}
def test_returns_configured_for_non_oauth_tools(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
tool_id = _FakeOid()
mock_collection = Mock()
@@ -1139,7 +1139,7 @@ class TestMCPAuthStatus:
]
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1152,7 +1152,7 @@ class TestMCPAuthStatus:
assert response.json["statuses"][str(tool_id)] == "configured"
def test_returns_connected_for_oauth_with_tokens(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
tool_id = _FakeOid()
mock_collection = Mock()
@@ -1174,10 +1174,10 @@ class TestMCPAuthStatus:
]
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
), patch(
"application.api.user.tools.mcp._connector_sessions",
"docsgpt.api.user.tools.mcp._connector_sessions",
mock_sessions,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1190,7 +1190,7 @@ class TestMCPAuthStatus:
assert response.json["statuses"][str(tool_id)] == "connected"
def test_returns_needs_auth_for_oauth_without_tokens(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
tool_id = _FakeOid()
mock_collection = Mock()
@@ -1207,10 +1207,10 @@ class TestMCPAuthStatus:
mock_sessions.find.return_value = []
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
), patch(
"application.api.user.tools.mcp._connector_sessions",
"docsgpt.api.user.tools.mcp._connector_sessions",
mock_sessions,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1223,7 +1223,7 @@ class TestMCPAuthStatus:
assert response.json["statuses"][str(tool_id)] == "needs_auth"
def test_returns_needs_auth_for_oauth_without_server_url(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
tool_id = _FakeOid()
mock_collection = Mock()
@@ -1235,7 +1235,7 @@ class TestMCPAuthStatus:
]
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1248,7 +1248,7 @@ class TestMCPAuthStatus:
assert response.json["statuses"][str(tool_id)] == "needs_auth"
def test_returns_configured_for_none_auth_type(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
tool_id = _FakeOid()
mock_collection = Mock()
@@ -1257,7 +1257,7 @@ class TestMCPAuthStatus:
]
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1270,13 +1270,13 @@ class TestMCPAuthStatus:
assert response.json["statuses"][str(tool_id)] == "configured"
def test_returns_500_on_exception(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
mock_collection = Mock()
mock_collection.find.side_effect = RuntimeError("db fail")
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1289,7 +1289,7 @@ class TestMCPAuthStatus:
assert "Failed to check auth status" in response.json["error"]
def test_multiple_tools_mixed_auth(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
tool_id_1 = _FakeOid()
tool_id_2 = _FakeOid()
@@ -1321,10 +1321,10 @@ class TestMCPAuthStatus:
]
with patch(
"application.api.user.tools.mcp.user_tools_collection",
"docsgpt.api.user.tools.mcp.user_tools_collection",
mock_collection,
), patch(
"application.api.user.tools.mcp._connector_sessions",
"docsgpt.api.user.tools.mcp._connector_sessions",
mock_sessions,
):
with app.test_request_context("/api/mcp_server/auth_status"):
@@ -1346,30 +1346,30 @@ class TestMCPAuthStatus:
class TestValidateMcpServerUrl:
def test_raises_when_url_is_empty(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
with pytest.raises(ValueError, match="server_url is required"):
_validate_mcp_server_url({})
def test_raises_when_url_is_none(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
with pytest.raises(ValueError, match="server_url is required"):
_validate_mcp_server_url({"server_url": None})
def test_raises_when_url_is_ssrf(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from application.core.url_validation import SSRFError
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.core.url_validation import SSRFError
with patch(
"application.api.user.tools.mcp.validate_url",
"docsgpt.api.user.tools.mcp.validate_url",
side_effect=SSRFError("private address"),
):
with pytest.raises(ValueError, match="Invalid server URL"):
_validate_mcp_server_url({"server_url": "http://169.254.169.254"})
def test_passes_valid_url(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
# Should not raise
_validate_mcp_server_url({"server_url": "https://mcp.example.com"})
@@ -1382,14 +1382,14 @@ class TestValidateMcpServerUrl:
class TestMCPServerConfigValueError:
def test_returns_400_when_url_missing(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with patch(
"application.api.user.tools.mcp.validate_url",
"docsgpt.api.user.tools.mcp.validate_url",
side_effect=None, # let validate_url pass but override _validate_mcp_server_url
):
with patch(
"application.api.user.tools.mcp._validate_mcp_server_url",
"docsgpt.api.user.tools.mcp._validate_mcp_server_url",
side_effect=ValueError("server_url is required"),
):
with app.test_request_context(
@@ -1406,11 +1406,11 @@ class TestMCPServerConfigValueError:
assert "Invalid MCP server configuration" in response.json["error"]
def test_returns_400_when_ssrf_url(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from application.core.url_validation import SSRFError
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.core.url_validation import SSRFError
with patch(
"application.api.user.tools.mcp.validate_url",
"docsgpt.api.user.tools.mcp.validate_url",
side_effect=SSRFError("private range"),
):
with app.test_request_context(
@@ -1437,11 +1437,11 @@ class TestMCPServerConfigValueError:
class TestMCPServerSaveValueError:
def test_returns_400_when_ssrf_url(self, app):
from application.api.user.tools.mcp import MCPServerSave
from application.core.url_validation import SSRFError
from docsgpt.api.user.tools.mcp import MCPServerSave
from docsgpt.core.url_validation import SSRFError
with patch(
"application.api.user.tools.mcp.validate_url",
"docsgpt.api.user.tools.mcp.validate_url",
side_effect=SSRFError("private range"),
):
with app.test_request_context(
@@ -1465,10 +1465,10 @@ class TestMCPServerSaveValueError:
assert "Invalid MCP server configuration" in response.json["error"]
def test_returns_400_when_url_missing(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with patch(
"application.api.user.tools.mcp._validate_mcp_server_url",
"docsgpt.api.user.tools.mcp._validate_mcp_server_url",
side_effect=ValueError("server_url is required"),
):
with app.test_request_context(
+47 -47
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/tools/mcp.py using real PG."""
"""Tests for docsgpt/api/user/tools/mcp.py using real PG."""
from contextlib import contextmanager
from unittest.mock import MagicMock, patch
@@ -19,43 +19,43 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.tools.mcp.db_session", _yield
"docsgpt.api.user.tools.mcp.db_session", _yield
), patch(
"application.api.user.tools.mcp.db_readonly", _yield
"docsgpt.api.user.tools.mcp.db_readonly", _yield
):
yield
class TestSanitizeMcpTransport:
def test_defaults_to_auto(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
cfg = {}
got = _sanitize_mcp_transport(cfg)
assert got == "auto"
assert cfg["transport_type"] == "auto"
def test_accepts_supported_transports(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
for t in ("auto", "sse", "http"):
cfg = {"transport_type": t}
assert _sanitize_mcp_transport(cfg) == t
def test_strips_command_and_args(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
cfg = {"transport_type": "http", "command": "/bin/x", "args": ["a"]}
_sanitize_mcp_transport(cfg)
assert "command" not in cfg
assert "args" not in cfg
def test_unsupported_transport_raises(self):
from application.api.user.tools.mcp import _sanitize_mcp_transport
from docsgpt.api.user.tools.mcp import _sanitize_mcp_transport
with pytest.raises(ValueError):
_sanitize_mcp_transport({"transport_type": "websocket"})
class TestExtractAuthCredentials:
def test_api_key_auth(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
got = _extract_auth_credentials({
"auth_type": "api_key",
"api_key": "secret",
@@ -64,7 +64,7 @@ class TestExtractAuthCredentials:
assert got == {"api_key": "secret", "api_key_header": "X-API-Key"}
def test_bearer_auth(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
got = _extract_auth_credentials({
"auth_type": "bearer",
"bearer_token": "my-token",
@@ -72,7 +72,7 @@ class TestExtractAuthCredentials:
assert got == {"bearer_token": "my-token"}
def test_basic_auth(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
got = _extract_auth_credentials({
"auth_type": "basic",
"username": "u", "password": "p",
@@ -80,28 +80,28 @@ class TestExtractAuthCredentials:
assert got == {"username": "u", "password": "p"}
def test_none_auth_empty_creds(self):
from application.api.user.tools.mcp import _extract_auth_credentials
from docsgpt.api.user.tools.mcp import _extract_auth_credentials
assert _extract_auth_credentials({"auth_type": "none"}) == {}
class TestValidateMcpServerUrl:
def test_empty_url_raises(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
with pytest.raises(ValueError):
_validate_mcp_server_url({})
def test_missing_server_url(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
with pytest.raises(ValueError):
_validate_mcp_server_url({"server_url": ""})
def test_ssrf_url_raises(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
with pytest.raises(ValueError):
_validate_mcp_server_url({"server_url": "http://127.0.0.1"})
def test_valid_public_url_passes(self):
from application.api.user.tools.mcp import _validate_mcp_server_url
from docsgpt.api.user.tools.mcp import _validate_mcp_server_url
# Should not raise for a public-ish URL
try:
_validate_mcp_server_url({"server_url": "https://example.com/mcp"})
@@ -113,7 +113,7 @@ class TestValidateMcpServerUrl:
class TestTestMCPServerConfig:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with app.test_request_context(
"/api/mcp_server/test", method="POST",
@@ -125,7 +125,7 @@ class TestTestMCPServerConfig:
assert response.status_code == 401
def test_returns_400_missing_config(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with app.test_request_context(
"/api/mcp_server/test", method="POST", json={},
@@ -136,7 +136,7 @@ class TestTestMCPServerConfig:
assert response.status_code == 400
def test_unsupported_transport_returns_400(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with app.test_request_context(
"/api/mcp_server/test", method="POST",
@@ -148,7 +148,7 @@ class TestTestMCPServerConfig:
assert response.status_code == 400
def test_missing_url_returns_400(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with app.test_request_context(
"/api/mcp_server/test", method="POST",
@@ -160,7 +160,7 @@ class TestTestMCPServerConfig:
assert response.status_code == 400
def test_connection_success(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
fake_tool = MagicMock()
fake_tool.test_connection.return_value = {
@@ -169,7 +169,7 @@ class TestTestMCPServerConfig:
}
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=fake_tool,
), app.test_request_context(
"/api/mcp_server/test", method="POST",
@@ -189,7 +189,7 @@ class TestTestMCPServerConfig:
assert response.json["tools_count"] == 3
def test_connection_failure_returns_200_with_failure_message(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
fake_tool = MagicMock()
fake_tool.test_connection.return_value = {
@@ -197,7 +197,7 @@ class TestTestMCPServerConfig:
}
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=fake_tool,
), app.test_request_context(
"/api/mcp_server/test", method="POST",
@@ -215,7 +215,7 @@ class TestTestMCPServerConfig:
assert response.json["success"] is False
def test_oauth_required_returns_200(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
fake_tool = MagicMock()
fake_tool.test_connection.return_value = {
@@ -225,7 +225,7 @@ class TestTestMCPServerConfig:
}
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=fake_tool,
), app.test_request_context(
"/api/mcp_server/test", method="POST",
@@ -244,10 +244,10 @@ class TestTestMCPServerConfig:
assert response.json["requires_oauth"] is True
def test_unexpected_exception_returns_500(self, app):
from application.api.user.tools.mcp import TestMCPServerConfig
from docsgpt.api.user.tools.mcp import TestMCPServerConfig
with patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
side_effect=RuntimeError("boom"),
), app.test_request_context(
"/api/mcp_server/test", method="POST",
@@ -266,7 +266,7 @@ class TestTestMCPServerConfig:
class TestMCPServerSave:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save", method="POST",
@@ -278,7 +278,7 @@ class TestMCPServerSave:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save", method="POST", json={},
@@ -289,7 +289,7 @@ class TestMCPServerSave:
assert response.status_code == 400
def test_unsupported_transport_returns_400(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save", method="POST",
@@ -304,7 +304,7 @@ class TestMCPServerSave:
assert response.status_code == 400
def test_missing_server_url_returns_400(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save", method="POST",
@@ -316,7 +316,7 @@ class TestMCPServerSave:
assert response.status_code == 400
def test_oauth_missing_task_id_returns_400(self, app):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
with app.test_request_context(
"/api/mcp_server/save", method="POST",
@@ -335,7 +335,7 @@ class TestMCPServerSave:
assert response.status_code == 400
def test_creates_mcp_tool_successfully(self, app, pg_conn):
from application.api.user.tools.mcp import MCPServerSave
from docsgpt.api.user.tools.mcp import MCPServerSave
user = "u-mcp-save"
fake_tool = MagicMock()
@@ -343,7 +343,7 @@ class TestMCPServerSave:
fake_tool.get_actions_metadata.return_value = [{"name": "t1"}]
with _patch_db(pg_conn), patch(
"application.api.user.tools.mcp.MCPTool",
"docsgpt.api.user.tools.mcp.MCPTool",
return_value=fake_tool,
), app.test_request_context(
"/api/mcp_server/save", method="POST",
@@ -365,7 +365,7 @@ class TestMCPServerSave:
class TestMCPOAuthCallback:
def test_error_param_redirects_error(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with app.test_request_context(
"/api/mcp_server/oauth_callback?error=access_denied"
@@ -375,7 +375,7 @@ class TestMCPOAuthCallback:
assert "status=error" in response.location
def test_missing_code_or_state_redirects_error(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with app.test_request_context(
"/api/mcp_server/oauth_callback"
@@ -384,17 +384,17 @@ class TestMCPOAuthCallback:
assert response.status_code == 302
def test_success_redirects_success(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
fake_redis = MagicMock()
fake_manager = MagicMock()
fake_manager.handle_oauth_callback.return_value = True
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=fake_redis,
), patch(
"application.api.user.tools.mcp.MCPOAuthManager",
"docsgpt.api.user.tools.mcp.MCPOAuthManager",
return_value=fake_manager,
), app.test_request_context(
"/api/mcp_server/oauth_callback?code=c1&state=s1"
@@ -404,17 +404,17 @@ class TestMCPOAuthCallback:
assert "status=success" in response.location
def test_manager_failure_redirects_error(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
fake_redis = MagicMock()
fake_manager = MagicMock()
fake_manager.handle_oauth_callback.return_value = False
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=fake_redis,
), patch(
"application.api.user.tools.mcp.MCPOAuthManager",
"docsgpt.api.user.tools.mcp.MCPOAuthManager",
return_value=fake_manager,
), app.test_request_context(
"/api/mcp_server/oauth_callback?code=c1&state=s1"
@@ -424,10 +424,10 @@ class TestMCPOAuthCallback:
assert "status=error" in response.location
def test_no_redis_redirects_error(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
return_value=None,
), app.test_request_context(
"/api/mcp_server/oauth_callback?code=c&state=s"
@@ -437,10 +437,10 @@ class TestMCPOAuthCallback:
assert "Redis" in response.location or "status=error" in response.location
def test_exception_redirects_error(self, app):
from application.api.user.tools.mcp import MCPOAuthCallback
from docsgpt.api.user.tools.mcp import MCPOAuthCallback
with patch(
"application.api.user.tools.mcp.get_redis_instance",
"docsgpt.api.user.tools.mcp.get_redis_instance",
side_effect=RuntimeError("boom"),
), app.test_request_context(
"/api/mcp_server/oauth_callback?code=c&state=s"
@@ -451,7 +451,7 @@ class TestMCPOAuthCallback:
class TestMCPAuthStatus:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.mcp import MCPAuthStatus
from docsgpt.api.user.tools.mcp import MCPAuthStatus
with app.test_request_context(
"/api/mcp_server/auth_status"
+142 -142
View File
@@ -1,4 +1,4 @@
"""Unit tests for application.api.user.tools.routes."""
"""Unit tests for docsgpt.api.user.tools.routes."""
from contextlib import contextmanager
from unittest.mock import Mock, patch
@@ -21,7 +21,7 @@ class TestEncryptSecretFields:
pass
def test_encrypts_secret_keys(self):
from application.api.user.tools.routes import _encrypt_secret_fields
from docsgpt.api.user.tools.routes import _encrypt_secret_fields
config = {"api_key": "my-secret", "base_url": "https://example.com"}
config_requirements = {
@@ -29,7 +29,7 @@ class TestEncryptSecretFields:
"base_url": {"secret": False},
}
with patch(
"application.api.user.tools.routes.encrypt_credentials",
"docsgpt.api.user.tools.routes.encrypt_credentials",
return_value="encrypted-blob",
):
result = _encrypt_secret_fields(config, config_requirements, "user1")
@@ -39,7 +39,7 @@ class TestEncryptSecretFields:
assert result["base_url"] == "https://example.com"
def test_returns_config_unchanged_when_no_secrets(self):
from application.api.user.tools.routes import _encrypt_secret_fields
from docsgpt.api.user.tools.routes import _encrypt_secret_fields
config = {"base_url": "https://example.com"}
config_requirements = {"base_url": {"secret": False}}
@@ -47,7 +47,7 @@ class TestEncryptSecretFields:
assert result == config
def test_skips_empty_secret_values(self):
from application.api.user.tools.routes import _encrypt_secret_fields
from docsgpt.api.user.tools.routes import _encrypt_secret_fields
config = {"api_key": "", "base_url": "https://example.com"}
config_requirements = {"api_key": {"secret": True}}
@@ -55,7 +55,7 @@ class TestEncryptSecretFields:
assert result == config
def test_skips_secret_key_not_in_config(self):
from application.api.user.tools.routes import _encrypt_secret_fields
from docsgpt.api.user.tools.routes import _encrypt_secret_fields
config = {"base_url": "https://example.com"}
config_requirements = {"api_key": {"secret": True}}
@@ -71,7 +71,7 @@ class TestValidateConfig:
pass
def test_returns_empty_on_valid_config(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"api_key": "abc123"}
config_requirements = {
@@ -81,7 +81,7 @@ class TestValidateConfig:
assert errors == {}
def test_reports_missing_required_field(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {}
config_requirements = {
@@ -91,7 +91,7 @@ class TestValidateConfig:
assert "api_key" in errors
def test_skips_required_secret_when_existing_secrets(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {}
config_requirements = {
@@ -101,7 +101,7 @@ class TestValidateConfig:
assert errors == {}
def test_validates_number_type(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"timeout": "abc"}
config_requirements = {
@@ -111,7 +111,7 @@ class TestValidateConfig:
assert "timeout" in errors
def test_validates_timeout_range_too_low(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"timeout": "0"}
config_requirements = {
@@ -122,7 +122,7 @@ class TestValidateConfig:
assert "between 1 and 300" in errors["timeout"]
def test_validates_timeout_range_too_high(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"timeout": "500"}
config_requirements = {
@@ -132,7 +132,7 @@ class TestValidateConfig:
assert "timeout" in errors
def test_valid_timeout(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"timeout": "60"}
config_requirements = {
@@ -142,7 +142,7 @@ class TestValidateConfig:
assert errors == {}
def test_validates_enum_value(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"mode": "invalid"}
config_requirements = {
@@ -152,7 +152,7 @@ class TestValidateConfig:
assert "mode" in errors
def test_valid_enum_value(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"mode": "fast"}
config_requirements = {
@@ -162,7 +162,7 @@ class TestValidateConfig:
assert errors == {}
def test_depends_on_skips_when_condition_not_met(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"mode": "simple"}
config_requirements = {
@@ -177,7 +177,7 @@ class TestValidateConfig:
assert errors == {}
def test_depends_on_validates_when_condition_met(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"mode": "advanced"}
config_requirements = {
@@ -192,7 +192,7 @@ class TestValidateConfig:
assert "advanced_key" in errors
def test_empty_string_not_treated_as_value_for_required(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {"api_key": ""}
config_requirements = {
@@ -202,7 +202,7 @@ class TestValidateConfig:
assert "api_key" in errors
def test_uses_key_name_when_no_label(self):
from application.api.user.tools.routes import _validate_config
from docsgpt.api.user.tools.routes import _validate_config
config = {}
config_requirements = {
@@ -221,7 +221,7 @@ class TestMergeSecretsOnUpdate:
pass
def test_no_secret_keys_returns_new_config(self):
from application.api.user.tools.routes import _merge_secrets_on_update
from docsgpt.api.user.tools.routes import _merge_secrets_on_update
new_config = {"base_url": "https://new.example.com"}
existing_config = {"base_url": "https://old.example.com"}
@@ -233,7 +233,7 @@ class TestMergeSecretsOnUpdate:
assert result == new_config
def test_merges_existing_encrypted_with_new_secret(self):
from application.api.user.tools.routes import _merge_secrets_on_update
from docsgpt.api.user.tools.routes import _merge_secrets_on_update
new_config = {"api_key": "new-key", "base_url": "https://example.com"}
existing_config = {
@@ -245,10 +245,10 @@ class TestMergeSecretsOnUpdate:
"base_url": {"secret": False},
}
with patch(
"application.api.user.tools.routes.decrypt_credentials",
"docsgpt.api.user.tools.routes.decrypt_credentials",
return_value={"api_key": "old-key"},
), patch(
"application.api.user.tools.routes.encrypt_credentials",
"docsgpt.api.user.tools.routes.encrypt_credentials",
return_value="new-blob",
) as mock_encrypt:
result = _merge_secrets_on_update(
@@ -262,7 +262,7 @@ class TestMergeSecretsOnUpdate:
assert encrypted_call["api_key"] == "new-key"
def test_keeps_existing_secret_when_not_in_new_config(self):
from application.api.user.tools.routes import _merge_secrets_on_update
from docsgpt.api.user.tools.routes import _merge_secrets_on_update
new_config = {"base_url": "https://example.com"}
existing_config = {
@@ -274,10 +274,10 @@ class TestMergeSecretsOnUpdate:
"base_url": {"secret": False},
}
with patch(
"application.api.user.tools.routes.decrypt_credentials",
"docsgpt.api.user.tools.routes.decrypt_credentials",
return_value={"api_key": "old-key"},
), patch(
"application.api.user.tools.routes.encrypt_credentials",
"docsgpt.api.user.tools.routes.encrypt_credentials",
return_value="new-blob",
) as mock_encrypt:
_merge_secrets_on_update(
@@ -288,7 +288,7 @@ class TestMergeSecretsOnUpdate:
assert encrypted_call["api_key"] == "old-key"
def test_removes_encrypted_credentials_when_no_secrets(self):
from application.api.user.tools.routes import _merge_secrets_on_update
from docsgpt.api.user.tools.routes import _merge_secrets_on_update
new_config = {"base_url": "https://example.com"}
existing_config = {"base_url": "https://old.com"}
@@ -297,7 +297,7 @@ class TestMergeSecretsOnUpdate:
"base_url": {"secret": False},
}
with patch(
"application.api.user.tools.routes.decrypt_credentials",
"docsgpt.api.user.tools.routes.decrypt_credentials",
return_value={},
):
result = _merge_secrets_on_update(
@@ -307,17 +307,17 @@ class TestMergeSecretsOnUpdate:
assert "encrypted_credentials" not in result
def test_strips_has_encrypted_credentials_flag(self):
from application.api.user.tools.routes import _merge_secrets_on_update
from docsgpt.api.user.tools.routes import _merge_secrets_on_update
new_config = {"api_key": "k", "has_encrypted_credentials": True}
existing_config = {"encrypted_credentials": "blob"}
config_requirements = {"api_key": {"secret": True}}
with patch(
"application.api.user.tools.routes.decrypt_credentials",
"docsgpt.api.user.tools.routes.decrypt_credentials",
return_value={},
), patch(
"application.api.user.tools.routes.encrypt_credentials",
"docsgpt.api.user.tools.routes.encrypt_credentials",
return_value="blob2",
):
result = _merge_secrets_on_update(
@@ -335,7 +335,7 @@ class TestTransformActions:
pass
def test_sets_active_and_param_defaults(self):
from application.api.user.tools.routes import transform_actions
from docsgpt.api.user.tools.routes import transform_actions
actions = [
{
@@ -357,7 +357,7 @@ class TestTransformActions:
assert props["limit"]["filled_by_llm"] is True
def test_handles_action_without_parameters(self):
from application.api.user.tools.routes import transform_actions
from docsgpt.api.user.tools.routes import transform_actions
actions = [{"name": "ping"}]
result = transform_actions(actions)
@@ -365,14 +365,14 @@ class TestTransformActions:
assert "parameters" not in result[0]
def test_handles_empty_properties(self):
from application.api.user.tools.routes import transform_actions
from docsgpt.api.user.tools.routes import transform_actions
actions = [{"name": "noop", "parameters": {"properties": {}}}]
result = transform_actions(actions)
assert result[0]["active"] is True
def test_handles_empty_list(self):
from application.api.user.tools.routes import transform_actions
from docsgpt.api.user.tools.routes import transform_actions
assert transform_actions([]) == []
@@ -385,7 +385,7 @@ class TestAvailableTools:
pass
def test_returns_tools_metadata(self, app):
from application.api.user.tools.routes import AvailableTools
from docsgpt.api.user.tools.routes import AvailableTools
mock_tool = Mock()
mock_tool.__doc__ = "My Tool\nA great tool description"
@@ -396,7 +396,7 @@ class TestAvailableTools:
mock_manager.tools = {"my_tool": mock_tool}
with patch(
"application.api.user.tools.routes.tool_manager", mock_manager
"docsgpt.api.user.tools.routes.tool_manager", mock_manager
):
with app.test_request_context("/api/available_tools"):
from flask import request
@@ -413,7 +413,7 @@ class TestAvailableTools:
assert data["data"][0]["description"] == "A great tool description"
def test_returns_400_on_error(self, app):
from application.api.user.tools.routes import AvailableTools
from docsgpt.api.user.tools.routes import AvailableTools
mock_tool = Mock()
mock_tool.__doc__ = "Bad Tool"
@@ -423,7 +423,7 @@ class TestAvailableTools:
mock_manager.tools = {"bad_tool": mock_tool}
with patch(
"application.api.user.tools.routes.tool_manager", mock_manager
"docsgpt.api.user.tools.routes.tool_manager", mock_manager
):
with app.test_request_context("/api/available_tools"):
from flask import request
@@ -434,7 +434,7 @@ class TestAvailableTools:
assert response.status_code == 400
def test_single_line_docstring(self, app):
from application.api.user.tools.routes import AvailableTools
from docsgpt.api.user.tools.routes import AvailableTools
mock_tool = Mock()
mock_tool.__doc__ = "Simple Tool"
@@ -445,7 +445,7 @@ class TestAvailableTools:
mock_manager.tools = {"simple": mock_tool}
with patch(
"application.api.user.tools.routes.tool_manager", mock_manager
"docsgpt.api.user.tools.routes.tool_manager", mock_manager
):
with app.test_request_context("/api/available_tools"):
from flask import request
@@ -466,7 +466,7 @@ class TestGetTools:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import GetTools
from docsgpt.api.user.tools.routes import GetTools
with app.test_request_context("/api/get_tools"):
from flask import request
@@ -504,7 +504,7 @@ class TestCreateTool:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import CreateTool
from docsgpt.api.user.tools.routes import CreateTool
with app.test_request_context(
"/api/create_tool", method="POST", json={}
@@ -517,7 +517,7 @@ class TestCreateTool:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.tools.routes import CreateTool
from docsgpt.api.user.tools.routes import CreateTool
with app.test_request_context(
"/api/create_tool",
@@ -532,13 +532,13 @@ class TestCreateTool:
assert response.status_code == 400
def test_returns_404_tool_not_found(self, app):
from application.api.user.tools.routes import CreateTool
from docsgpt.api.user.tools.routes import CreateTool
mock_manager = Mock()
mock_manager.tools = {}
with patch(
"application.api.user.tools.routes.tool_manager", mock_manager
"docsgpt.api.user.tools.routes.tool_manager", mock_manager
):
with app.test_request_context(
"/api/create_tool",
@@ -559,14 +559,14 @@ class TestCreateTool:
assert response.status_code == 404
def test_returns_400_on_validation_error(self, app):
from application.api.user.tools.routes import CreateTool
from docsgpt.api.user.tools.routes import CreateTool
tool_instance = self._make_tool_instance()
mock_manager = Mock()
mock_manager.tools = {"my_tool": tool_instance}
with patch(
"application.api.user.tools.routes.tool_manager", mock_manager
"docsgpt.api.user.tools.routes.tool_manager", mock_manager
):
with app.test_request_context(
"/api/create_tool",
@@ -588,7 +588,7 @@ class TestCreateTool:
assert response.json["message"] == "Validation failed"
def test_returns_400_on_actions_error(self, app):
from application.api.user.tools.routes import CreateTool
from docsgpt.api.user.tools.routes import CreateTool
tool_instance = Mock()
tool_instance.get_actions_metadata.side_effect = Exception("boom")
@@ -596,7 +596,7 @@ class TestCreateTool:
mock_manager.tools = {"my_tool": tool_instance}
with patch(
"application.api.user.tools.routes.tool_manager", mock_manager
"docsgpt.api.user.tools.routes.tool_manager", mock_manager
):
with app.test_request_context(
"/api/create_tool",
@@ -627,7 +627,7 @@ class TestUpdateTool:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import UpdateTool
from docsgpt.api.user.tools.routes import UpdateTool
with app.test_request_context(
"/api/update_tool", method="POST", json={"id": "abc"}
@@ -640,7 +640,7 @@ class TestUpdateTool:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.tools.routes import UpdateTool
from docsgpt.api.user.tools.routes import UpdateTool
with app.test_request_context(
"/api/update_tool", method="POST", json={}
@@ -666,7 +666,7 @@ class TestUpdateToolConfig:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import UpdateToolConfig
from docsgpt.api.user.tools.routes import UpdateToolConfig
with app.test_request_context(
"/api/update_tool_config",
@@ -681,7 +681,7 @@ class TestUpdateToolConfig:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.tools.routes import UpdateToolConfig
from docsgpt.api.user.tools.routes import UpdateToolConfig
with app.test_request_context(
"/api/update_tool_config",
@@ -707,7 +707,7 @@ class TestUpdateToolActions:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import UpdateToolActions
from docsgpt.api.user.tools.routes import UpdateToolActions
with app.test_request_context(
"/api/update_tool_actions",
@@ -722,7 +722,7 @@ class TestUpdateToolActions:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.tools.routes import UpdateToolActions
from docsgpt.api.user.tools.routes import UpdateToolActions
with app.test_request_context(
"/api/update_tool_actions",
@@ -746,7 +746,7 @@ class TestUpdateToolStatus:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import UpdateToolStatus
from docsgpt.api.user.tools.routes import UpdateToolStatus
with app.test_request_context(
"/api/update_tool_status",
@@ -761,7 +761,7 @@ class TestUpdateToolStatus:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.tools.routes import UpdateToolStatus
from docsgpt.api.user.tools.routes import UpdateToolStatus
with app.test_request_context(
"/api/update_tool_status",
@@ -785,7 +785,7 @@ class TestDeleteTool:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import DeleteTool
from docsgpt.api.user.tools.routes import DeleteTool
with app.test_request_context(
"/api/delete_tool", method="POST", json={"id": "x"}
@@ -798,7 +798,7 @@ class TestDeleteTool:
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.tools.routes import DeleteTool
from docsgpt.api.user.tools.routes import DeleteTool
with app.test_request_context(
"/api/delete_tool", method="POST", json={}
@@ -821,13 +821,13 @@ class TestParseSpec:
pass
def test_parses_json_spec_successfully(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
metadata = {"title": "Pet API"}
actions = [{"name": "listPets"}]
with patch(
"application.api.user.tools.routes.parse_spec",
"docsgpt.api.user.tools.routes.parse_spec",
return_value=(metadata, actions),
):
with app.test_request_context(
@@ -846,7 +846,7 @@ class TestParseSpec:
assert response.json["actions"] == actions
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
with app.test_request_context(
"/api/parse_spec",
@@ -861,7 +861,7 @@ class TestParseSpec:
assert response.status_code == 401
def test_returns_400_empty_spec(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
with app.test_request_context(
"/api/parse_spec",
@@ -877,7 +877,7 @@ class TestParseSpec:
assert "Empty spec content" in response.json["message"]
def test_returns_400_whitespace_only_spec(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
with app.test_request_context(
"/api/parse_spec",
@@ -892,7 +892,7 @@ class TestParseSpec:
assert response.status_code == 400
def test_returns_400_no_spec_provided(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
with app.test_request_context(
"/api/parse_spec",
@@ -909,14 +909,14 @@ class TestParseSpec:
assert "No spec provided" in response.json["message"]
def test_parses_file_upload(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
from io import BytesIO
metadata = {"title": "API"}
actions = [{"name": "a1"}]
with patch(
"application.api.user.tools.routes.parse_spec",
"docsgpt.api.user.tools.routes.parse_spec",
return_value=(metadata, actions),
):
with app.test_request_context(
@@ -934,13 +934,13 @@ class TestParseSpec:
assert response.json["success"] is True
def test_rejects_oversized_spec_upload(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
from io import BytesIO
with patch(
"application.api.user.tools.routes.settings.PARSE_SPEC_MAX_BYTES", 4
"docsgpt.api.user.tools.routes.settings.PARSE_SPEC_MAX_BYTES", 4
), patch(
"application.api.user.tools.routes.parse_spec"
"docsgpt.api.user.tools.routes.parse_spec"
) as parse_mock, app.test_request_context(
"/api/parse_spec",
method="POST",
@@ -955,12 +955,12 @@ class TestParseSpec:
parse_mock.assert_not_called()
def test_rejects_oversized_json_spec(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
with patch(
"application.api.user.tools.routes.settings.PARSE_SPEC_MAX_BYTES", 4
"docsgpt.api.user.tools.routes.settings.PARSE_SPEC_MAX_BYTES", 4
), patch(
"application.api.user.tools.routes.parse_spec"
"docsgpt.api.user.tools.routes.parse_spec"
) as parse_mock, app.test_request_context(
"/api/parse_spec",
method="POST",
@@ -974,7 +974,7 @@ class TestParseSpec:
parse_mock.assert_not_called()
def test_returns_400_file_no_filename(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
from io import BytesIO
with app.test_request_context(
@@ -992,10 +992,10 @@ class TestParseSpec:
assert "No file selected" in response.json["message"]
def test_returns_400_on_value_error(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
with patch(
"application.api.user.tools.routes.parse_spec",
"docsgpt.api.user.tools.routes.parse_spec",
side_effect=ValueError("bad spec"),
):
with app.test_request_context(
@@ -1012,10 +1012,10 @@ class TestParseSpec:
assert "Invalid specification format" in response.json["error"]
def test_returns_500_on_generic_error(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
with patch(
"application.api.user.tools.routes.parse_spec",
"docsgpt.api.user.tools.routes.parse_spec",
side_effect=RuntimeError("unexpected"),
):
with app.test_request_context(
@@ -1032,7 +1032,7 @@ class TestParseSpec:
assert "Failed to parse specification" in response.json["error"]
def test_returns_400_invalid_file_encoding(self, app):
from application.api.user.tools.routes import ParseSpec
from docsgpt.api.user.tools.routes import ParseSpec
from io import BytesIO
bad_bytes = b"\x80\x81\x82\x83"
@@ -1060,7 +1060,7 @@ class TestGetArtifact:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import GetArtifact
from docsgpt.api.user.tools.routes import GetArtifact
with app.test_request_context("/api/artifact/abc"):
from flask import request
@@ -1083,15 +1083,15 @@ def _patch_tools_db(conn):
yield conn
with patch(
"application.api.user.tools.routes.db_session", _yield
"docsgpt.api.user.tools.routes.db_session", _yield
), patch(
"application.api.user.tools.routes.db_readonly", _yield
"docsgpt.api.user.tools.routes.db_readonly", _yield
):
yield
def _seed_tool(pg_conn, user="u-tools", name="read_webpage", config=None):
from application.storage.db.repositories.user_tools import UserToolsRepository
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
repo = UserToolsRepository(pg_conn)
return repo.create(
user,
@@ -1106,12 +1106,12 @@ def _seed_tool(pg_conn, user="u-tools", name="read_webpage", config=None):
class TestGetToolsHappy:
def test_returns_user_tools(self, app, pg_conn):
from application.agents.default_tools import (
from docsgpt.agents.default_tools import (
BUILTIN_AGENT_TOOLS,
loaded_builtin_agent_tools,
loaded_default_tools,
)
from application.api.user.tools.routes import GetTools
from docsgpt.api.user.tools.routes import GetTools
user = "u-get-tools"
_seed_tool(pg_conn, user=user, name="read_webpage")
@@ -1146,7 +1146,7 @@ class TestGetToolsHappy:
assert len(builtins) == (builtins_count - dual) + dual
def test_db_error_returns_400(self, app):
from application.api.user.tools.routes import GetTools
from docsgpt.api.user.tools.routes import GetTools
@contextmanager
def _broken():
@@ -1154,7 +1154,7 @@ class TestGetToolsHappy:
yield
with patch(
"application.api.user.tools.routes.db_readonly", _broken
"docsgpt.api.user.tools.routes.db_readonly", _broken
), app.test_request_context("/api/get_tools"):
from flask import request
request.decoded_token = {"sub": "u"}
@@ -1164,7 +1164,7 @@ class TestGetToolsHappy:
class TestCreateToolHappy:
def test_creates_tool_successfully(self, app, pg_conn):
from application.api.user.tools.routes import CreateTool
from docsgpt.api.user.tools.routes import CreateTool
user = "u-create-tool"
@@ -1188,7 +1188,7 @@ class TestCreateToolHappy:
assert "id" in body
def test_db_error_returns_400(self, app):
from application.api.user.tools.routes import CreateTool
from docsgpt.api.user.tools.routes import CreateTool
@contextmanager
def _broken():
@@ -1196,7 +1196,7 @@ class TestCreateToolHappy:
yield
with patch(
"application.api.user.tools.routes.db_session", _broken
"docsgpt.api.user.tools.routes.db_session", _broken
), app.test_request_context(
"/api/create_tool",
method="POST",
@@ -1215,7 +1215,7 @@ class TestCreateToolHappy:
class TestUpdateToolHappy:
def test_returns_404_not_found(self, app, pg_conn):
from application.api.user.tools.routes import UpdateTool
from docsgpt.api.user.tools.routes import UpdateTool
with _patch_tools_db(pg_conn), app.test_request_context(
"/api/update_tool",
@@ -1231,8 +1231,8 @@ class TestUpdateToolHappy:
assert response.status_code == 404
def test_updates_tool_display_name(self, app, pg_conn):
from application.api.user.tools.routes import UpdateTool
from application.storage.db.repositories.user_tools import (
from docsgpt.api.user.tools.routes import UpdateTool
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -1254,7 +1254,7 @@ class TestUpdateToolHappy:
class TestUpdateToolConfigHappy:
def test_returns_404_not_found(self, app, pg_conn):
from application.api.user.tools.routes import UpdateToolConfig
from docsgpt.api.user.tools.routes import UpdateToolConfig
with _patch_tools_db(pg_conn), app.test_request_context(
"/api/update_tool_config",
@@ -1270,7 +1270,7 @@ class TestUpdateToolConfigHappy:
assert response.status_code == 404
def test_updates_config(self, app, pg_conn):
from application.api.user.tools.routes import UpdateToolConfig
from docsgpt.api.user.tools.routes import UpdateToolConfig
user = "u-cfg"
tool = _seed_tool(pg_conn, user=user)
@@ -1291,7 +1291,7 @@ class TestUpdateToolConfigHappy:
class TestUpdateToolActionsHappy:
def test_returns_404_not_found(self, app, pg_conn):
from application.api.user.tools.routes import UpdateToolActions
from docsgpt.api.user.tools.routes import UpdateToolActions
with _patch_tools_db(pg_conn), app.test_request_context(
"/api/update_tool_actions",
@@ -1307,7 +1307,7 @@ class TestUpdateToolActionsHappy:
assert response.status_code == 404
def test_updates_actions(self, app, pg_conn):
from application.api.user.tools.routes import UpdateToolActions
from docsgpt.api.user.tools.routes import UpdateToolActions
user = "u-actions"
tool = _seed_tool(pg_conn, user=user)
@@ -1330,7 +1330,7 @@ class TestUpdateToolActionsHappy:
class TestUpdateToolStatusHappy:
def test_returns_401_unauthenticated(self, app):
from application.api.user.tools.routes import UpdateToolStatus
from docsgpt.api.user.tools.routes import UpdateToolStatus
with app.test_request_context(
"/api/update_tool_status",
@@ -1343,7 +1343,7 @@ class TestUpdateToolStatusHappy:
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.tools.routes import UpdateToolStatus
from docsgpt.api.user.tools.routes import UpdateToolStatus
with app.test_request_context(
"/api/update_tool_status",
@@ -1356,7 +1356,7 @@ class TestUpdateToolStatusHappy:
assert response.status_code == 400
def test_returns_404_not_found(self, app, pg_conn):
from application.api.user.tools.routes import UpdateToolStatus
from docsgpt.api.user.tools.routes import UpdateToolStatus
with _patch_tools_db(pg_conn), app.test_request_context(
"/api/update_tool_status",
@@ -1372,8 +1372,8 @@ class TestUpdateToolStatusHappy:
assert response.status_code == 404
def test_updates_status(self, app, pg_conn):
from application.api.user.tools.routes import UpdateToolStatus
from application.storage.db.repositories.user_tools import (
from docsgpt.api.user.tools.routes import UpdateToolStatus
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -1395,7 +1395,7 @@ class TestUpdateToolStatusHappy:
class TestDeleteToolHappy:
def test_returns_404_not_found(self, app, pg_conn):
from application.api.user.tools.routes import DeleteTool
from docsgpt.api.user.tools.routes import DeleteTool
with _patch_tools_db(pg_conn), app.test_request_context(
"/api/delete_tool",
@@ -1408,8 +1408,8 @@ class TestDeleteToolHappy:
assert response.status_code == 404
def test_deletes_tool(self, app, pg_conn):
from application.api.user.tools.routes import DeleteTool
from application.storage.db.repositories.user_tools import (
from docsgpt.api.user.tools.routes import DeleteTool
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -1430,7 +1430,7 @@ class TestDeleteToolHappy:
class TestGetArtifactHappy:
def test_returns_404_tool_not_found(self, app, pg_conn):
from application.api.user.tools.routes import GetArtifact
from docsgpt.api.user.tools.routes import GetArtifact
with _patch_tools_db(pg_conn), app.test_request_context(
"/api/artifact/00000000-0000-0000-0000-000000000000"
@@ -1448,11 +1448,11 @@ class TestGetArtifactHappy:
# ---------------------------------------------------------------------------
class TestDefaultToolsRoutes:
def test_get_tools_flags_defaults(self, app, pg_conn):
from application.agents.default_tools import (
from docsgpt.agents.default_tools import (
default_tool_id,
loaded_default_tools,
)
from application.api.user.tools.routes import GetTools
from docsgpt.api.user.tools.routes import GetTools
user = "u-def-get"
with _patch_tools_db(pg_conn), app.test_request_context("/api/get_tools"):
@@ -1469,8 +1469,8 @@ class TestDefaultToolsRoutes:
def test_get_tools_surfaces_scheduler_with_both_flags(self, app, pg_conn):
"""Dual-registered scheduler appears once with default+builtin flags."""
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import GetTools
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import GetTools
user = "u-sched-dual"
with _patch_tools_db(pg_conn), app.test_request_context("/api/get_tools"):
@@ -1489,8 +1489,8 @@ class TestDefaultToolsRoutes:
assert row["name"] == "scheduler"
def test_get_tools_status_reflects_opt_out(self, app, pg_conn):
from application.api.user.tools.routes import GetTools
from application.storage.db.repositories.users import UsersRepository
from docsgpt.api.user.tools.routes import GetTools
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-def-optout"
UsersRepository(pg_conn).set_default_tool_enabled(
@@ -1509,9 +1509,9 @@ class TestDefaultToolsRoutes:
assert by_name["memory"]["status"] is True
def test_update_tool_status_toggles_default_off(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateToolStatus
from application.storage.db.repositories.users import UsersRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateToolStatus
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-def-toggle"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -1527,9 +1527,9 @@ class TestDefaultToolsRoutes:
assert user_doc["tool_preferences"]["disabled_default_tools"] == ["memory"]
def test_update_tool_status_toggles_default_back_on(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateToolStatus
from application.storage.db.repositories.users import UsersRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateToolStatus
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-def-on"
UsersRepository(pg_conn).set_default_tool_enabled(user, "memory", False)
@@ -1546,9 +1546,9 @@ class TestDefaultToolsRoutes:
assert user_doc["tool_preferences"]["disabled_default_tools"] == []
def test_update_tool_toggles_default_via_status(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateTool
from application.storage.db.repositories.users import UsersRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateTool
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-def-updtool"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -1566,8 +1566,8 @@ class TestDefaultToolsRoutes:
]
def test_delete_tool_rejects_default(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import DeleteTool
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import DeleteTool
user = "u-def-del"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -1584,8 +1584,8 @@ class TestDefaultToolsRoutes:
def test_update_tool_default_without_status_is_rejected(
self, app, pg_conn
):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateTool
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateTool
user = "u-def-noedit"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -1601,8 +1601,8 @@ class TestDefaultToolsRoutes:
assert "not editable" in response.json["message"]
def test_update_tool_config_rejects_default(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateToolConfig
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateToolConfig
user = "u-def-cfg"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -1618,8 +1618,8 @@ class TestDefaultToolsRoutes:
assert "config-free" in response.json["message"]
def test_update_tool_actions_rejects_default(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateToolActions
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateToolActions
user = "u-def-act"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -1643,9 +1643,9 @@ class TestDefaultToolsRoutes:
# ---------------------------------------------------------------------------
class TestDualRegisteredToggle:
def test_update_tool_status_off_writes_disabled_default(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateToolStatus
from application.storage.db.repositories.users import UsersRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateToolStatus
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-sched-off"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -1664,9 +1664,9 @@ class TestDualRegisteredToggle:
)
def test_update_tool_status_on_removes_disabled_default(self, app, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateToolStatus
from application.storage.db.repositories.users import UsersRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateToolStatus
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-sched-on"
UsersRepository(pg_conn).set_default_tool_enabled(
@@ -1689,9 +1689,9 @@ class TestDualRegisteredToggle:
def test_update_tool_status_round_trip(self, app, pg_conn):
"""Off → on returns to the empty-list baseline."""
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateToolStatus
from application.storage.db.repositories.users import UsersRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateToolStatus
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-sched-rt"
scheduler_id = default_tool_id("scheduler")
@@ -1710,9 +1710,9 @@ class TestDualRegisteredToggle:
def test_update_tool_with_status_writes_disabled_default(self, app, pg_conn):
"""The /api/update_tool route also honours the default branch first."""
from application.agents.default_tools import default_tool_id
from application.api.user.tools.routes import UpdateTool
from application.storage.db.repositories.users import UsersRepository
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.user.tools.routes import UpdateTool
from docsgpt.storage.db.repositories.users import UsersRepository
user = "u-sched-upd"
with _patch_tools_db(pg_conn), app.test_request_context(
@@ -23,16 +23,16 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.user.models.routes.db_session", _yield_conn
"docsgpt.api.user.models.routes.db_session", _yield_conn
), patch(
"application.api.user.models.routes.db_readonly", _yield_conn
"docsgpt.api.user.models.routes.db_readonly", _yield_conn
):
yield
@pytest.fixture(autouse=True)
def _reset_registry():
from application.core.model_registry import ModelRegistry
from docsgpt.core.model_registry import ModelRegistry
ModelRegistry.reset()
yield
@@ -45,7 +45,7 @@ def _reset_registry():
@pytest.mark.unit
class TestAuth:
def test_list_unauthenticated_returns_401(self, app):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
@@ -57,7 +57,7 @@ class TestAuth:
assert resp.status_code == 401
def test_create_unauthenticated_returns_401(self, app):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
@@ -84,13 +84,13 @@ class TestAuth:
@pytest.mark.unit
class TestCreate:
def test_creates_and_returns_201_without_api_key(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
# Mock DNS so the SSRF check passes for api.mistral.ai without
# hitting the network.
with patch("application.security.safe_url.socket.getaddrinfo") as gai:
with patch("docsgpt.security.safe_url.socket.getaddrinfo") as gai:
gai.return_value = [
(None, None, None, None, ("104.18.0.1", 0))
]
@@ -124,7 +124,7 @@ class TestCreate:
assert v != "sk-mistral-test"
def test_create_rejects_missing_required_fields(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
@@ -141,7 +141,7 @@ class TestCreate:
assert resp.status_code == 400
def test_create_rejects_loopback_url(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
@@ -169,11 +169,11 @@ class TestCreate:
typos) must reject at the boundary so the DB never holds
garbage that the registry would later silently drop.
"""
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
with patch("application.security.safe_url.socket.getaddrinfo") as gai:
with patch("docsgpt.security.safe_url.socket.getaddrinfo") as gai:
gai.return_value = [
(None, None, None, None, ("104.18.0.1", 0))
]
@@ -199,11 +199,11 @@ class TestCreate:
def test_create_accepts_image_alias_and_raw_mime(self, app, pg_conn):
"""The known ``image`` alias and raw MIME types both pass."""
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
with patch("application.security.safe_url.socket.getaddrinfo") as gai:
with patch("docsgpt.security.safe_url.socket.getaddrinfo") as gai:
gai.return_value = [
(None, None, None, None, ("104.18.0.1", 0))
]
@@ -228,11 +228,11 @@ class TestCreate:
assert resp.status_code == 201
def test_create_accepts_responses_capabilities(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
with patch("application.security.safe_url.socket.getaddrinfo") as gai:
with patch("docsgpt.security.safe_url.socket.getaddrinfo") as gai:
gai.return_value = [(None, None, None, None, ("104.18.0.1", 0))]
with app.test_request_context(
"/api/user/models",
@@ -267,11 +267,11 @@ class TestCreate:
def test_create_rejects_invalid_responses_capabilities(
self, app, pg_conn, capability, value
):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
with patch("application.security.safe_url.socket.getaddrinfo") as gai:
with patch("docsgpt.security.safe_url.socket.getaddrinfo") as gai:
gai.return_value = [(None, None, None, None, ("104.18.0.1", 0))]
with app.test_request_context(
"/api/user/models",
@@ -294,11 +294,11 @@ class TestCreate:
assert capability in resp.get_json()["error"]
def test_create_rejects_private_ip_dns(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
with patch("application.security.safe_url.socket.getaddrinfo") as gai:
with patch("docsgpt.security.safe_url.socket.getaddrinfo") as gai:
# Hostname resolves to a private IP only — must reject
gai.return_value = [
(None, None, None, None, ("10.0.0.5", 0))
@@ -325,7 +325,7 @@ class TestCreate:
def _create_via_repo(pg_conn, user_id="user-1", **kwargs):
from application.storage.db.repositories.user_custom_models import (
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -342,7 +342,7 @@ def _create_via_repo(pg_conn, user_id="user-1", **kwargs):
@pytest.mark.unit
class TestList:
def test_lists_only_users_own(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
@@ -367,7 +367,7 @@ class TestList:
@pytest.mark.unit
class TestGet:
def test_returns_404_for_other_users_model(self, app, pg_conn):
from application.api.user.models.routes import UserModelResource
from docsgpt.api.user.models.routes import UserModelResource
created = _create_via_repo(pg_conn, user_id="alice")
with app.test_request_context(
@@ -384,7 +384,7 @@ class TestGet:
@pytest.mark.unit
class TestPatch:
def test_patch_updates_display_name(self, app, pg_conn):
from application.api.user.models.routes import UserModelResource
from docsgpt.api.user.models.routes import UserModelResource
created = _create_via_repo(pg_conn, user_id="user-1")
with app.test_request_context(
@@ -405,8 +405,8 @@ class TestPatch:
"""Critical PATCH semantic: empty/missing api_key in body must
preserve the stored ciphertext (the UI sends a blank password
field when the user wants to keep the existing key)."""
from application.api.user.models.routes import UserModelResource
from application.storage.db.repositories.user_custom_models import (
from docsgpt.api.user.models.routes import UserModelResource
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -436,13 +436,13 @@ class TestPatch:
@pytest.mark.unit
class TestDelete:
def test_delete_removes_row_and_invalidates_cache(self, app, pg_conn):
from application.api.user.models.routes import UserModelResource
from application.core.model_registry import ModelRegistry
from docsgpt.api.user.models.routes import UserModelResource
from docsgpt.core.model_registry import ModelRegistry
created = _create_via_repo(pg_conn, user_id="user-1")
# Warm the registry's per-user cache via a lookup
with patch(
"application.storage.db.session.db_readonly"
"docsgpt.storage.db.session.db_readonly"
) as ro:
@contextmanager
def _y():
@@ -475,11 +475,11 @@ class TestSecurityCreateRejectsBlankFields:
LLMCreator to leak settings.API_KEY to the user-supplied URL."""
def test_create_rejects_blank_api_key(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelsCollectionResource,
)
with patch("application.security.safe_url.socket.getaddrinfo") as gai:
with patch("docsgpt.security.safe_url.socket.getaddrinfo") as gai:
gai.return_value = [(None, None, None, None, ("104.18.0.1", 0))]
with app.test_request_context(
"/api/user/models",
@@ -501,8 +501,8 @@ class TestSecurityCreateRejectsBlankFields:
assert "api_key" in (body.get("error") or "").lower()
def test_patch_rejects_blank_required_field(self, app, pg_conn):
from application.api.user.models.routes import UserModelResource
from application.storage.db.repositories.user_custom_models import (
from docsgpt.api.user.models.routes import UserModelResource
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -534,7 +534,7 @@ class TestPayloadConnectionTest:
expect to validate their endpoint + key before committing."""
def test_payload_test_rejects_unsafe_url(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelTestPayloadResource,
)
@@ -559,14 +559,14 @@ class TestPayloadConnectionTest:
def test_payload_test_returns_ok_when_upstream_responds_2xx(
self, app, pg_conn
):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelTestPayloadResource,
)
# pinned_post is the IP-pinned dispatch helper. Patching it
# bypasses both the SSRF guard and the network — the success
# path we're verifying here is the route's response handling.
with patch("application.api.user.models.routes.pinned_post") as rp:
with patch("docsgpt.api.user.models.routes.pinned_post") as rp:
rp.return_value = MagicMock(
status_code=200,
headers={"Content-Type": "application/json"},
@@ -596,11 +596,11 @@ class TestPayloadConnectionTest:
assert call_args.kwargs["json"]["model"] == "mistral-large-latest"
def test_payload_test_uses_responses_protocol(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelTestPayloadResource,
)
with patch("application.api.user.models.routes.pinned_post") as rp:
with patch("docsgpt.api.user.models.routes.pinned_post") as rp:
rp.return_value = MagicMock(
status_code=200,
headers={"Content-Type": "application/json"},
@@ -634,7 +634,7 @@ class TestPayloadConnectionTest:
}
def test_payload_test_unauthenticated_returns_401(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelTestPayloadResource,
)
@@ -655,7 +655,7 @@ class TestPayloadConnectionTest:
assert resp.status_code == 401
def test_payload_test_missing_fields_returns_400(self, app, pg_conn):
from application.api.user.models.routes import (
from docsgpt.api.user.models.routes import (
UserModelTestPayloadResource,
)
@@ -678,7 +678,7 @@ class TestByIdConnectionTestAcceptsOverrides:
so the test reflects in-flight edits (not the saved record)."""
def _make_row(self, pg_conn):
from application.storage.db.repositories.user_custom_models import (
from docsgpt.storage.db.repositories.user_custom_models import (
UserCustomModelsRepository,
)
@@ -691,9 +691,9 @@ class TestByIdConnectionTestAcceptsOverrides:
)
def _post_test(self, app, pg_conn, model_id, body):
from application.api.user.models.routes import UserModelTestResource
from docsgpt.api.user.models.routes import UserModelTestResource
with patch("application.api.user.models.routes.pinned_post") as rp:
with patch("docsgpt.api.user.models.routes.pinned_post") as rp:
rp.return_value = MagicMock(
status_code=200,
headers={"Content-Type": "application/json"},
@@ -758,8 +758,8 @@ class TestApiModelsListWithUser:
def test_includes_user_models_when_authenticated(self, app, pg_conn):
"""GET /api/models with auth should surface the user's BYOM
records alongside built-ins, each tagged with `source`."""
from application.api.user.models.routes import ModelsListResource
from application.core.model_registry import ModelRegistry
from docsgpt.api.user.models.routes import ModelsListResource
from docsgpt.core.model_registry import ModelRegistry
created = _create_via_repo(
pg_conn, user_id="user-1", display_name="My Mistral"
@@ -772,7 +772,7 @@ class TestApiModelsListWithUser:
yield pg_conn
with patch(
"application.storage.db.session.db_readonly", _yield
"docsgpt.storage.db.session.db_readonly", _yield
):
ModelRegistry.reset()
with app.test_request_context("/api/models"):
+10 -10
View File
@@ -14,7 +14,7 @@ class TestGetUserId:
pass
def test_returns_user_id_from_decoded_token(self, app):
from application.api.user.utils import get_user_id
from docsgpt.api.user.utils import get_user_id
with app.test_request_context():
from flask import request
@@ -23,13 +23,13 @@ class TestGetUserId:
assert get_user_id() == "user_123"
def test_returns_none_when_no_decoded_token(self, app):
from application.api.user.utils import get_user_id
from docsgpt.api.user.utils import get_user_id
with app.test_request_context():
assert get_user_id() is None
def test_returns_none_when_decoded_token_has_no_sub(self, app):
from application.api.user.utils import get_user_id
from docsgpt.api.user.utils import get_user_id
with app.test_request_context():
from flask import request
@@ -43,7 +43,7 @@ class TestRequireAuth:
pass
def test_allows_authenticated_request(self, app):
from application.api.user.utils import require_auth
from docsgpt.api.user.utils import require_auth
@require_auth
def protected():
@@ -56,7 +56,7 @@ class TestRequireAuth:
assert protected() == "ok"
def test_returns_401_when_unauthenticated(self, app):
from application.api.user.utils import require_auth
from docsgpt.api.user.utils import require_auth
@require_auth
def protected():
@@ -72,7 +72,7 @@ class TestSuccessResponse:
pass
def test_default_success_response(self, app):
from application.api.user.utils import success_response
from docsgpt.api.user.utils import success_response
with app.app_context():
resp = success_response()
@@ -87,14 +87,14 @@ class TestErrorResponse:
pass
def test_error_response_custom_status(self, app):
from application.api.user.utils import error_response
from docsgpt.api.user.utils import error_response
with app.app_context():
resp = error_response("Not found", 404)
assert resp.status_code == 404
def test_error_response_extra_kwargs(self, app):
from application.api.user.utils import error_response
from docsgpt.api.user.utils import error_response
with app.app_context():
resp = error_response("Bad", 400, errors=["field1", "field2"])
@@ -126,7 +126,7 @@ class TestRequireFields:
pass
def test_allows_valid_request(self, app):
from application.api.user.utils import require_fields
from docsgpt.api.user.utils import require_fields
@require_fields(["name", "email"])
def handler():
@@ -139,7 +139,7 @@ class TestRequireFields:
def test_rejects_empty_body(self, app):
from application.api.user.utils import require_fields
from docsgpt.api.user.utils import require_fields
@require_fields(["name"])
def handler():
+1 -1
View File
@@ -1,4 +1,4 @@
"""Additional tests for application/api/user/utils.py to cover paginated_response.
"""Additional tests for docsgpt/api/user/utils.py to cover paginated_response.
Target missing lines:
- 257-262: paginated_response (collection query + serializer + response)
+1 -1
View File
@@ -1,4 +1,4 @@
"""Tests for application/api/user/agents/webhooks.py.
"""Tests for docsgpt/api/user/agents/webhooks.py.
Previously coupled to bson.ObjectId + patched agents_collection. Scheduled
for rewrite against pg_conn + AgentsRepository.
@@ -1,4 +1,4 @@
"""Additional coverage tests for application.api.user.workflows.routes.
"""Additional coverage tests for docsgpt.api.user.workflows.routes.
No bson/ObjectId imports. Mongo collections are replaced by Mock objects.
``validate_object_id`` (which calls bson internally) is patched wherever
@@ -29,7 +29,7 @@ def _mock_validate_object_id(wf_id):
mock_oid = Mock()
mock_oid.__str__ = lambda self: wf_id
return patch(
"application.api.user.workflows.routes.validate_object_id",
"docsgpt.api.user.workflows.routes.validate_object_id",
return_value=(mock_oid, None),
)
@@ -65,18 +65,18 @@ class TestGetWorkflowGraphVersionCoverage:
pass
def test_large_version_number(self):
from application.api.user.workflows.routes import get_workflow_graph_version
from docsgpt.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": 99}) == 99
def test_float_string_falls_back_to_1(self):
from application.api.user.workflows.routes import get_workflow_graph_version
from docsgpt.api.user.workflows.routes import get_workflow_graph_version
# int("3.5") raises ValueError → falls back to 1
assert get_workflow_graph_version({"current_graph_version": "3.5"}) == 1
def test_none_value_returns_1(self):
from application.api.user.workflows.routes import get_workflow_graph_version
from docsgpt.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": None}) == 1
@@ -104,7 +104,7 @@ class TestValidateWorkflowStructureCoverage:
pass
def test_valid_condition_node_with_two_outgoing_edges(self):
from application.api.user.workflows.routes import validate_workflow_structure
from docsgpt.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
@@ -127,7 +127,7 @@ class TestValidateWorkflowStructureCoverage:
assert errors == []
def test_multiple_end_nodes_allowed(self):
from application.api.user.workflows.routes import validate_workflow_structure
from docsgpt.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
@@ -151,7 +151,7 @@ class TestWorkflowListPostCoverage:
pass
def test_create_unauthorized_returns_401(self, app):
from application.api.user.workflows.routes import WorkflowList
from docsgpt.api.user.workflows.routes import WorkflowList
with app.test_request_context(
"/api/workflows", method="POST", json={"name": "WF"}
@@ -164,7 +164,7 @@ class TestWorkflowListPostCoverage:
assert response.status_code == 401
def test_create_missing_name_returns_400(self, app):
from application.api.user.workflows.routes import WorkflowList
from docsgpt.api.user.workflows.routes import WorkflowList
with app.test_request_context(
"/api/workflows", method="POST", json={"description": "no name"}
@@ -188,7 +188,7 @@ class TestWorkflowDetailGetCoverage:
pass
def test_returns_401_unauthenticated(self, app):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
with app.test_request_context("/api/workflows/abc", method="GET"):
from flask import request
@@ -211,7 +211,7 @@ class TestWorkflowDetailDeleteCoverage:
pass
def test_delete_unauthorized(self, app):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
with app.test_request_context("/api/workflows/abc", method="DELETE"):
from flask import request
@@ -234,9 +234,9 @@ def _patch_wf_db(conn):
yield conn
with patch(
"application.api.user.workflows.routes.db_session", _yield
"docsgpt.api.user.workflows.routes.db_session", _yield
), patch(
"application.api.user.workflows.routes.db_readonly", _yield
"docsgpt.api.user.workflows.routes.db_readonly", _yield
):
yield
@@ -257,7 +257,7 @@ def _minimal_workflow_body(name="WF1"):
class TestSerializers:
def test_serialize_workflow_fields(self):
from application.api.user.workflows.routes import serialize_workflow
from docsgpt.api.user.workflows.routes import serialize_workflow
import datetime
wf = {
@@ -277,7 +277,7 @@ class TestSerializers:
assert got["created_at"] == "2024-01-01T12:00:00"
def test_serialize_node_shape(self):
from application.api.user.workflows.routes import serialize_node
from docsgpt.api.user.workflows.routes import serialize_node
node = {
"id": "00000000-0000-0000-0000-000000000002",
@@ -294,7 +294,7 @@ class TestSerializers:
assert out["position"] == {"x": 0, "y": 0}
def test_serialize_edge_shape(self):
from application.api.user.workflows.routes import serialize_edge
from docsgpt.api.user.workflows.routes import serialize_edge
edge = {
"id": "00000000-0000-0000-0000-000000000003",
@@ -311,7 +311,7 @@ class TestSerializers:
class TestWorkflowListPost:
def test_creates_valid_workflow(self, app, pg_conn):
from application.api.user.workflows.routes import WorkflowList
from docsgpt.api.user.workflows.routes import WorkflowList
with _patch_wf_db(pg_conn), app.test_request_context(
"/api/workflows",
@@ -329,7 +329,7 @@ class TestWorkflowListPost:
def test_create_validation_failure_returns_400(self, app, pg_conn):
"""Workflow with no start node should fail validation."""
from application.api.user.workflows.routes import WorkflowList
from docsgpt.api.user.workflows.routes import WorkflowList
body = {
"name": "bad",
@@ -346,7 +346,7 @@ class TestWorkflowListPost:
assert response.status_code == 400
def test_create_db_error_returns_400(self, app):
from application.api.user.workflows.routes import WorkflowList
from docsgpt.api.user.workflows.routes import WorkflowList
@contextmanager
def _broken():
@@ -354,7 +354,7 @@ class TestWorkflowListPost:
yield
with patch(
"application.api.user.workflows.routes.db_session", _broken
"docsgpt.api.user.workflows.routes.db_session", _broken
), app.test_request_context(
"/api/workflows",
method="POST",
@@ -369,7 +369,7 @@ class TestWorkflowListPost:
class TestWorkflowDetailGet:
def test_returns_404_for_missing_workflow(self, app, pg_conn):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
with _patch_wf_db(pg_conn), app.test_request_context(
"/api/workflows/00000000-0000-0000-0000-000000000000",
@@ -383,7 +383,7 @@ class TestWorkflowDetailGet:
assert response.status_code == 404
def test_returns_workflow_after_create(self, app, pg_conn):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
WorkflowDetail,
WorkflowList,
)
@@ -413,7 +413,7 @@ class TestWorkflowDetailGet:
assert len(data["edges"]) == 1
def test_db_error_returns_400(self, app):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
@contextmanager
def _broken():
@@ -421,7 +421,7 @@ class TestWorkflowDetailGet:
yield
with patch(
"application.api.user.workflows.routes.db_readonly", _broken
"docsgpt.api.user.workflows.routes.db_readonly", _broken
), app.test_request_context("/api/workflows/abc"):
from flask import request
request.decoded_token = {"sub": "u1"}
@@ -431,7 +431,7 @@ class TestWorkflowDetailGet:
class TestWorkflowDetailPut:
def test_returns_401_unauthenticated(self, app):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
with app.test_request_context(
"/api/workflows/abc",
@@ -444,7 +444,7 @@ class TestWorkflowDetailPut:
assert response.status_code == 401
def test_returns_400_missing_name(self, app, pg_conn):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
with _patch_wf_db(pg_conn), app.test_request_context(
"/api/workflows/abc",
@@ -457,7 +457,7 @@ class TestWorkflowDetailPut:
assert response.status_code == 400
def test_returns_404_missing_workflow(self, app, pg_conn):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
body = _minimal_workflow_body("new")
with _patch_wf_db(pg_conn), app.test_request_context(
@@ -473,7 +473,7 @@ class TestWorkflowDetailPut:
assert response.status_code == 404
def test_validation_failure_returns_400(self, app, pg_conn):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
body = {
"name": "bad",
@@ -491,7 +491,7 @@ class TestWorkflowDetailPut:
assert response.status_code == 400
def test_updates_workflow(self, app, pg_conn):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
WorkflowDetail,
WorkflowList,
)
@@ -528,7 +528,7 @@ class TestWorkflowDetailPut:
assert data["workflow"]["name"] == "after"
def test_db_error_returns_400(self, app):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
@contextmanager
def _broken():
@@ -536,7 +536,7 @@ class TestWorkflowDetailPut:
yield
with patch(
"application.api.user.workflows.routes.db_session", _broken
"docsgpt.api.user.workflows.routes.db_session", _broken
), app.test_request_context(
"/api/workflows/abc",
method="PUT",
@@ -550,7 +550,7 @@ class TestWorkflowDetailPut:
class TestWorkflowDetailDelete:
def test_returns_404_missing(self, app, pg_conn):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
with _patch_wf_db(pg_conn), app.test_request_context(
"/api/workflows/00000000-0000-0000-0000-000000000000",
@@ -564,7 +564,7 @@ class TestWorkflowDetailDelete:
assert response.status_code == 404
def test_deletes_workflow(self, app, pg_conn):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
WorkflowDetail,
WorkflowList,
)
@@ -588,7 +588,7 @@ class TestWorkflowDetailDelete:
assert response.status_code == 200
def test_db_error_returns_400(self, app):
from application.api.user.workflows.routes import WorkflowDetail
from docsgpt.api.user.workflows.routes import WorkflowDetail
@contextmanager
def _broken():
@@ -596,7 +596,7 @@ class TestWorkflowDetailDelete:
yield
with patch(
"application.api.user.workflows.routes.db_session", _broken
"docsgpt.api.user.workflows.routes.db_session", _broken
), app.test_request_context(
"/api/workflows/abc", method="DELETE"
):
@@ -608,14 +608,14 @@ class TestWorkflowDetailDelete:
class TestValidateWorkflowStructureExtras:
def test_no_nodes_returns_error(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_workflow_structure,
)
errors = validate_workflow_structure([], [])
assert any("at least one node" in e for e in errors)
def test_missing_start_node_returns_error(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_workflow_structure,
)
errors = validate_workflow_structure(
@@ -624,7 +624,7 @@ class TestValidateWorkflowStructureExtras:
assert any("start" in e for e in errors)
def test_missing_end_node_returns_error(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_workflow_structure,
)
errors = validate_workflow_structure(
@@ -634,7 +634,7 @@ class TestValidateWorkflowStructureExtras:
assert any("end" in e for e in errors)
def test_edge_with_missing_source_reports_error(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_workflow_structure,
)
errors = validate_workflow_structure(
@@ -650,7 +650,7 @@ class TestValidateWorkflowStructureExtras:
assert any("non-existent source" in err for err in errors)
def test_condition_node_without_else_branch_errors(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_workflow_structure,
)
nodes = [
@@ -672,14 +672,14 @@ class TestValidateWorkflowStructureExtras:
class TestValidateJsonSchemaPayload:
def test_none_returns_pair_of_none(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_json_schema_payload,
)
got, err = validate_json_schema_payload(None)
assert got is None and err is None
def test_valid_schema(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_json_schema_payload,
)
got, err = validate_json_schema_payload(
@@ -689,7 +689,7 @@ class TestValidateJsonSchemaPayload:
assert got is not None
def test_invalid_schema_returns_error(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
validate_json_schema_payload,
)
# Force an invalid payload by passing something that isn't dict
@@ -700,14 +700,14 @@ class TestValidateJsonSchemaPayload:
class TestNormalizeAgentNodeJsonSchemas:
def test_returns_non_dict_entries_as_is(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
got = normalize_agent_node_json_schemas(["not-a-dict"])
assert got == ["not-a-dict"]
def test_non_agent_node_passes_through(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
got = normalize_agent_node_json_schemas(
@@ -716,7 +716,7 @@ class TestNormalizeAgentNodeJsonSchemas:
assert got[0]["type"] == "start"
def test_agent_node_without_json_schema_passes_through(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
got = normalize_agent_node_json_schemas(
@@ -725,7 +725,7 @@ class TestNormalizeAgentNodeJsonSchemas:
assert got[0]["data"]["other"] == 1
def test_agent_node_with_schema_normalizes(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
got = normalize_agent_node_json_schemas([
@@ -738,7 +738,7 @@ class TestNormalizeAgentNodeJsonSchemas:
assert got[0]["data"]["json_schema"] is not None
def test_agent_node_invalid_schema_kept_original(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
got = normalize_agent_node_json_schemas([
@@ -754,10 +754,10 @@ class TestNormalizeAgentNodeJsonSchemas:
class TestWriteGraphEdgesWithUnresolvedNodes:
def test_drops_edge_with_unknown_source(self, pg_conn, app):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
_write_graph,
)
from application.storage.db.repositories.workflows import (
from docsgpt.storage.db.repositories.workflows import (
WorkflowsRepository,
)
@@ -778,7 +778,7 @@ class TestWriteGraphEdgesWithUnresolvedNodes:
_write_graph(pg_conn, pg_wf_id, 1, nodes_data, edges_data)
def test_get_workflow_graph_version_negative_falls_back(self):
from application.api.user.workflows.routes import (
from docsgpt.api.user.workflows.routes import (
get_workflow_graph_version,
)
assert get_workflow_graph_version({"current_graph_version": -5}) == 1