mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 22:14:46 +00:00
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:
1 parent
3e7f1f91b4
commit
574f96341e
984 files changed
+9847
-9682
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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}})
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
):
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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"):
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
Reference in new issue
Block a user