mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-04 04:12:36 +00:00
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.
298 lines
10 KiB
Python
298 lines
10 KiB
Python
"""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
|
|
those collection objects so the tests run without pymongo installed.
|
|
"""
|
|
|
|
import uuid
|
|
from contextlib import contextmanager
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
|
|
def _fake_oid():
|
|
"""24-character hex string used as a substitute for a Mongo ObjectId string."""
|
|
return uuid.uuid4().hex[:24]
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
return Flask(__name__)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AgentWebhook.get — additional coverage beyond test_webhooks.py
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestAgentWebhookGetGaps:
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AgentWebhookListener — additional POST / GET edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestAgentWebhookListenerGaps:
|
|
pass
|
|
|
|
def test_post_empty_payload_still_enqueues(self, app):
|
|
"""Empty dict payload does not block task enqueue (warning only)."""
|
|
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
|
|
|
|
mock_task = Mock()
|
|
mock_task.id = "task_empty"
|
|
|
|
with patch(
|
|
"docsgpt.api.user.agents.webhooks.process_agent_webhook"
|
|
) as mock_process:
|
|
mock_process.apply_async.return_value = mock_task
|
|
with app.test_request_context(
|
|
"/api/webhooks/agents/tok",
|
|
method="POST",
|
|
json={},
|
|
):
|
|
listener = AgentWebhookListener()
|
|
response = listener._enqueue_webhook_task("agent1", {}, "POST")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json["task_id"] == "task_empty"
|
|
|
|
def test_get_empty_query_string(self, app):
|
|
"""GET request with no query params produces an empty payload dict."""
|
|
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
|
|
|
|
mock_task = Mock()
|
|
mock_task.id = "task_noqs"
|
|
|
|
with patch(
|
|
"docsgpt.api.user.agents.webhooks.process_agent_webhook"
|
|
) as mock_process:
|
|
mock_process.apply_async.return_value = mock_task
|
|
with app.test_request_context(
|
|
"/api/webhooks/agents/tok",
|
|
method="GET",
|
|
):
|
|
listener = AgentWebhookListener()
|
|
response = listener.get(
|
|
webhook_token="tok",
|
|
agent={"_id": uuid.uuid4().hex},
|
|
agent_id_str="agentXYZ",
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
call_kwargs = mock_process.apply_async.call_args[1]
|
|
# apply_async wraps task args under ``kwargs=``.
|
|
assert call_kwargs["kwargs"]["payload"] == {}
|
|
|
|
def test_enqueue_returns_task_id_in_response(self, app):
|
|
"""Success response body includes task_id from the Celery task."""
|
|
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
|
|
|
|
mock_task = Mock()
|
|
mock_task.id = "celery-task-99"
|
|
|
|
with patch(
|
|
"docsgpt.api.user.agents.webhooks.process_agent_webhook"
|
|
) as mock_process:
|
|
mock_process.apply_async.return_value = mock_task
|
|
with app.test_request_context(
|
|
"/api/webhooks/agents/tok",
|
|
method="POST",
|
|
json={"k": "v"},
|
|
):
|
|
listener = AgentWebhookListener()
|
|
response = listener._enqueue_webhook_task("a1", {"k": "v"}, "POST")
|
|
|
|
assert response.json["success"] is True
|
|
assert response.json["task_id"] == "celery-task-99"
|
|
|
|
def test_enqueue_error_returns_500_with_message(self, app):
|
|
"""Queue failure returns 500 with a human-readable message."""
|
|
from docsgpt.api.user.agents.webhooks import AgentWebhookListener
|
|
|
|
with patch(
|
|
"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(
|
|
"/api/webhooks/agents/tok",
|
|
method="POST",
|
|
json={"x": 1},
|
|
):
|
|
listener = AgentWebhookListener()
|
|
response = listener._enqueue_webhook_task("a1", {"x": 1}, "POST")
|
|
|
|
assert response.status_code == 500
|
|
assert response.json["success"] is False
|
|
assert "message" in response.json
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Real-PG tests for AgentWebhook (get) and AgentWebhookListener
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@contextmanager
|
|
def _patch_webhooks_db(conn):
|
|
@contextmanager
|
|
def _yield():
|
|
yield conn
|
|
|
|
with patch(
|
|
"docsgpt.api.user.agents.webhooks.db_session", _yield
|
|
), patch(
|
|
"docsgpt.api.user.agents.webhooks.db_readonly", _yield
|
|
):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def _patch_base_db(conn):
|
|
@contextmanager
|
|
def _yield():
|
|
yield conn
|
|
|
|
with patch(
|
|
"docsgpt.api.user.base.db_readonly", _yield
|
|
), patch(
|
|
"docsgpt.api.user.base.db_session", _yield
|
|
):
|
|
yield
|
|
|
|
|
|
class TestAgentWebhookGet:
|
|
def test_returns_401_unauthenticated(self, app):
|
|
from docsgpt.api.user.agents.webhooks import AgentWebhook
|
|
|
|
with app.test_request_context("/api/agent_webhook?id=x"):
|
|
from flask import request
|
|
request.decoded_token = None
|
|
response = AgentWebhook().get()
|
|
assert response.status_code == 401
|
|
|
|
def test_returns_400_missing_id(self, app):
|
|
from docsgpt.api.user.agents.webhooks import AgentWebhook
|
|
|
|
with app.test_request_context("/api/agent_webhook"):
|
|
from flask import request
|
|
request.decoded_token = {"sub": "u"}
|
|
response = AgentWebhook().get()
|
|
assert response.status_code == 400
|
|
|
|
def test_returns_404_missing_agent(self, app, pg_conn):
|
|
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"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": "u"}
|
|
response = AgentWebhook().get()
|
|
assert response.status_code == 404
|
|
|
|
def test_generates_webhook_url(self, app, pg_conn):
|
|
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(
|
|
"docsgpt.api.user.agents.webhooks.settings.API_URL",
|
|
"https://api.test",
|
|
), app.test_request_context(
|
|
f"/api/agent_webhook?id={agent['id']}"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = AgentWebhook().get()
|
|
assert response.status_code == 200
|
|
assert response.json["webhook_url"].startswith(
|
|
"https://api.test/api/webhooks/agents/"
|
|
)
|
|
|
|
def test_reuses_existing_webhook_token(self, app, pg_conn):
|
|
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(
|
|
user, "a", "published", incoming_webhook_token="existing-tok",
|
|
)
|
|
|
|
with _patch_webhooks_db(pg_conn), patch(
|
|
"docsgpt.api.user.agents.webhooks.settings.API_URL",
|
|
"https://api.test",
|
|
), app.test_request_context(
|
|
f"/api/agent_webhook?id={agent['id']}"
|
|
):
|
|
from flask import request
|
|
request.decoded_token = {"sub": user}
|
|
response = AgentWebhook().get()
|
|
assert "existing-tok" in response.json["webhook_url"]
|
|
|
|
|
|
class TestAgentWebhookListener:
|
|
def test_post_valid_enqueues_task(self, app, pg_conn):
|
|
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"
|
|
agent = AgentsRepository(pg_conn).create(
|
|
user, "a", "published", incoming_webhook_token="tk-enq",
|
|
)
|
|
fake_task = MagicMock(id="task-post")
|
|
|
|
with patch(
|
|
"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",
|
|
json={"event": "x"},
|
|
):
|
|
listener = AgentWebhookListener()
|
|
# Call directly with manually-injected kwargs (bypass decorator)
|
|
response = listener.post(
|
|
webhook_token="tk-enq",
|
|
agent=agent,
|
|
agent_id_str=str(agent["id"]),
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json["task_id"] == "task-post"
|
|
|
|
def test_get_collects_query_params(self, app, pg_conn):
|
|
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"
|
|
agent = AgentsRepository(pg_conn).create(
|
|
user, "a", "published", incoming_webhook_token="tk-get",
|
|
)
|
|
fake_task = MagicMock(id="task-get")
|
|
|
|
with patch(
|
|
"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"
|
|
):
|
|
listener = AgentWebhookListener()
|
|
response = listener.get(
|
|
webhook_token="tk-get",
|
|
agent=agent,
|
|
agent_id_str=str(agent["id"]),
|
|
)
|
|
assert response.status_code == 200
|