chore: import upstream snapshot with attribution
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:29 +08:00
commit fed8b2eed7
1531 changed files with 1107494 additions and 0 deletions
View File
+524
View File
@@ -0,0 +1,524 @@
"""Tests for application/api/user/agents/folders.py.
Uses the ephemeral ``pg_conn`` fixture to exercise real PG repository code.
"""
from contextlib import contextmanager
from unittest.mock import patch
import pytest
from flask import Flask
@pytest.fixture
def app():
return Flask(__name__)
@contextmanager
def _patch_folders_db(conn):
@contextmanager
def _yield():
yield conn
with patch(
"application.api.user.agents.folders.db_session", _yield
), patch(
"application.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 (
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
repo = AgentsRepository(pg_conn)
agent = repo.create(user, "test-agent", "published", description="x")
if folder_id:
repo.set_folder(str(agent["id"]), user, folder_id)
return agent
class TestAgentFoldersGet:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolders
with app.test_request_context("/api/agents/folders/"):
from flask import request
request.decoded_token = None
response = AgentFolders().get()
assert response.status_code == 401
def test_returns_folders_list(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolders
user = "u-folders-list"
_seed_folder(pg_conn, user, name="A")
_seed_folder(pg_conn, user, name="B")
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/"
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolders().get()
assert response.status_code == 200
names = [f["name"] for f in response.json["folders"]]
assert "A" in names and "B" in names
class TestAgentFoldersPost:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolders
with app.test_request_context(
"/api/agents/folders/", method="POST", json={"name": "F"}
):
from flask import request
request.decoded_token = None
response = AgentFolders().post()
assert response.status_code == 401
def test_returns_400_missing_name(self, app):
from application.api.user.agents.folders import AgentFolders
with app.test_request_context(
"/api/agents/folders/", method="POST", json={}
):
from flask import request
request.decoded_token = {"sub": "u"}
response = AgentFolders().post()
assert response.status_code == 400
def test_creates_folder_at_root(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolders
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/", method="POST",
json={"name": "Root Folder"},
):
from flask import request
request.decoded_token = {"sub": "u-create"}
response = AgentFolders().post()
assert response.status_code == 201
assert response.json["name"] == "Root Folder"
assert response.json["parent_id"] is None
def test_creates_nested_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolders
user = "u-nested"
parent = _seed_folder(pg_conn, user, name="parent")
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/", method="POST",
json={"name": "child", "parent_id": str(parent["id"])},
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolders().post()
assert response.status_code == 201
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
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/", method="POST",
json={
"name": "child",
"parent_id": "00000000-0000-0000-0000-000000000000",
},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = AgentFolders().post()
assert response.status_code == 404
class TestAgentFolderGet:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolder
with app.test_request_context("/api/agents/folders/abc"):
from flask import request
request.decoded_token = None
response = AgentFolder().get("abc")
assert response.status_code == 401
def test_returns_404_missing_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/00000000-0000-0000-0000-000000000000"
):
from flask import request
request.decoded_token = {"sub": "u"}
response = AgentFolder().get(
"00000000-0000-0000-0000-000000000000"
)
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
user = "u-folder-detail"
parent = _seed_folder(pg_conn, user, name="parent")
_seed_folder(pg_conn, user, name="sub", parent_id=str(parent["id"]))
_seed_agent(pg_conn, user, folder_id=str(parent["id"]))
with _patch_folders_db(pg_conn), app.test_request_context(
f"/api/agents/folders/{parent['id']}"
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolder().get(str(parent["id"]))
assert response.status_code == 200
data = response.json
assert data["name"] == "parent"
assert len(data["agents"]) == 1
assert len(data["subfolders"]) == 1
class TestAgentFolderPut:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolder
with app.test_request_context(
"/api/agents/folders/abc", method="PUT", json={"name": "new"}
):
from flask import request
request.decoded_token = None
response = AgentFolder().put("abc")
assert response.status_code == 401
def test_returns_404_missing_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/00000000-0000-0000-0000-000000000000",
method="PUT",
json={"name": "new"},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = AgentFolder().put(
"00000000-0000-0000-0000-000000000000"
)
assert response.status_code == 404
def test_renames_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
user = "u-rename"
folder = _seed_folder(pg_conn, user, name="old")
with _patch_folders_db(pg_conn), app.test_request_context(
f"/api/agents/folders/{folder['id']}",
method="PUT",
json={"name": "renamed"},
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolder().put(str(folder["id"]))
assert response.status_code == 200
def test_prevents_setting_self_as_parent(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
user = "u-self-parent"
folder = _seed_folder(pg_conn, user, name="f1")
with _patch_folders_db(pg_conn), app.test_request_context(
f"/api/agents/folders/{folder['id']}",
method="PUT",
json={"parent_id": str(folder["id"])},
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolder().put(str(folder["id"]))
assert response.status_code == 400
def test_sets_parent_to_other_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
user = "u-moveparent"
f1 = _seed_folder(pg_conn, user, name="f1")
f2 = _seed_folder(pg_conn, user, name="f2")
with _patch_folders_db(pg_conn), app.test_request_context(
f"/api/agents/folders/{f1['id']}",
method="PUT",
json={"parent_id": str(f2["id"])},
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolder().put(str(f1["id"]))
assert response.status_code == 200
def test_returns_404_for_missing_parent(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
user = "u-missingparent"
folder = _seed_folder(pg_conn, user, name="f")
with _patch_folders_db(pg_conn), app.test_request_context(
f"/api/agents/folders/{folder['id']}",
method="PUT",
json={"parent_id": "00000000-0000-0000-0000-000000000000"},
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolder().put(str(folder["id"]))
assert response.status_code == 404
def test_clears_parent_id_when_null(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
user = "u-clear"
parent = _seed_folder(pg_conn, user, name="p")
child = _seed_folder(
pg_conn, user, name="c", parent_id=str(parent["id"])
)
with _patch_folders_db(pg_conn), app.test_request_context(
f"/api/agents/folders/{child['id']}",
method="PUT",
json={"parent_id": None},
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolder().put(str(child["id"]))
assert response.status_code == 200
class TestAgentFolderDelete:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolder
with app.test_request_context(
"/api/agents/folders/abc", method="DELETE"
):
from flask import request
request.decoded_token = None
response = AgentFolder().delete("abc")
assert response.status_code == 401
def test_returns_404_missing_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/00000000-0000-0000-0000-000000000000",
method="DELETE",
):
from flask import request
request.decoded_token = {"sub": "u"}
response = AgentFolder().delete(
"00000000-0000-0000-0000-000000000000"
)
assert response.status_code == 404
def test_deletes_folder(self, app, pg_conn):
from application.api.user.agents.folders import AgentFolder
user = "u-del"
folder = _seed_folder(pg_conn, user, name="tbd")
with _patch_folders_db(pg_conn), app.test_request_context(
f"/api/agents/folders/{folder['id']}", method="DELETE"
):
from flask import request
request.decoded_token = {"sub": user}
response = AgentFolder().delete(str(folder["id"]))
assert response.status_code == 200
class TestMoveAgentToFolder:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
with app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={"agent_id": "x"},
):
from flask import request
request.decoded_token = None
response = MoveAgentToFolder().post()
assert response.status_code == 401
def test_returns_400_missing_agent_id(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
with app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = MoveAgentToFolder().post()
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={"agent_id": "00000000-0000-0000-0000-000000000000"},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = MoveAgentToFolder().post()
assert response.status_code == 404
def test_moves_agent_into_folder(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
user = "u-move"
folder = _seed_folder(pg_conn, user, name="target")
agent = _seed_agent(pg_conn, user)
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={
"agent_id": str(agent["id"]),
"folder_id": str(folder["id"]),
},
):
from flask import request
request.decoded_token = {"sub": user}
response = MoveAgentToFolder().post()
assert response.status_code == 200
def test_returns_404_folder_not_found(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
user = "u-move-nofolder"
agent = _seed_agent(pg_conn, user)
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={
"agent_id": str(agent["id"]),
"folder_id": "00000000-0000-0000-0000-000000000000",
},
):
from flask import request
request.decoded_token = {"sub": user}
response = MoveAgentToFolder().post()
assert response.status_code == 404
def test_removes_agent_from_folder(self, app, pg_conn):
from application.api.user.agents.folders import MoveAgentToFolder
user = "u-remove"
folder = _seed_folder(pg_conn, user, name="target")
agent = _seed_agent(pg_conn, user, folder_id=str(folder["id"]))
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={"agent_id": str(agent["id"]), "folder_id": None},
):
from flask import request
request.decoded_token = {"sub": user}
response = MoveAgentToFolder().post()
assert response.status_code == 200
class TestBulkMoveAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import BulkMoveAgents
with app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={"agent_ids": ["a"]},
):
from flask import request
request.decoded_token = None
response = BulkMoveAgents().post()
assert response.status_code == 401
def test_returns_400_missing_agent_ids(self, app):
from application.api.user.agents.folders import BulkMoveAgents
with app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = BulkMoveAgents().post()
assert response.status_code == 400
def test_bulk_moves_agents(self, app, pg_conn):
from application.api.user.agents.folders import BulkMoveAgents
user = "u-bulk"
folder = _seed_folder(pg_conn, user, name="dest")
a1 = _seed_agent(pg_conn, user)
a2 = _seed_agent(pg_conn, user)
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={
"agent_ids": [str(a1["id"]), str(a2["id"])],
"folder_id": str(folder["id"]),
},
):
from flask import request
request.decoded_token = {"sub": user}
response = BulkMoveAgents().post()
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
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={
"agent_ids": ["a"],
"folder_id": "00000000-0000-0000-0000-000000000000",
},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = BulkMoveAgents().post()
assert response.status_code == 404
def test_bulk_move_tolerates_missing_agents(self, app, pg_conn):
from application.api.user.agents.folders import BulkMoveAgents
user = "u-bulk-partial"
folder = _seed_folder(pg_conn, user, name="f")
a1 = _seed_agent(pg_conn, user)
with _patch_folders_db(pg_conn), app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={
"agent_ids": [
str(a1["id"]),
"00000000-0000-0000-0000-000000000000",
],
"folder_id": str(folder["id"]),
},
):
from flask import request
request.decoded_token = {"sub": user}
response = BulkMoveAgents().post()
assert response.status_code == 200
+120
View File
@@ -0,0 +1,120 @@
"""Gap-coverage tests for application.api.user.agents.routes.
No bson/ObjectId imports. The routes internally use ObjectId (patched where
needed) and Mongo collections (mocked). Repository assertions use
AgentsRepository patches for Postgres-specific paths.
"""
import uuid
from unittest.mock import Mock
import pytest
from flask import Flask
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def app():
return Flask(__name__)
def _fake_oid():
"""Return a 24-character hex string that looks like a Mongo ObjectId."""
return uuid.uuid4().hex[:24]
# ---------------------------------------------------------------------------
# normalize_workflow_reference — pure utility, no bson needed
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestNormalizeWorkflowReferenceGaps:
pass
def test_handles_integer_zero(self):
from application.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
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
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
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
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
result = normalize_workflow_reference({"other_key": "value"})
assert result is None
# ---------------------------------------------------------------------------
# validate_workflow_access — mocked ObjectId + workflows_collection
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestValidateWorkflowAccessGaps:
pass
def _fake_objectid(self, valid: bool = True):
"""Return a Mock that mimics bson.ObjectId."""
oid_cls = Mock()
oid_cls.is_valid = Mock(return_value=valid)
oid_cls.return_value = Mock()
return oid_cls
# ---------------------------------------------------------------------------
# build_agent_document — pure logic, no DB needed
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestBuildAgentDocumentGaps:
pass
# ---------------------------------------------------------------------------
# GetAgent route — additional edge cases (uuid-based IDs, no bson)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestGetAgentGaps:
pass
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,260 @@
"""Tests for helper functions in application/api/user/agents/routes.py."""
import json
from contextlib import contextmanager
from unittest.mock import patch
import pytest
from flask import Flask
@pytest.fixture
def app():
return Flask(__name__)
@contextmanager
def _patch_db(conn):
@contextmanager
def _yield():
yield conn
with patch(
"application.api.user.agents.routes.db_session", _yield
), patch(
"application.api.user.agents.routes.db_readonly", _yield
):
yield
class TestNormalizeWorkflowReference:
def test_none_returns_none(self):
from application.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 (
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 (
normalize_workflow_reference,
)
assert (
normalize_workflow_reference({"workflow_id": "wf99"}) == "wf99"
)
def test_dict_with_underscore_id(self):
from application.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 (
normalize_workflow_reference,
)
assert normalize_workflow_reference("") == ""
def test_plain_string_returned(self):
from application.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 (
normalize_workflow_reference,
)
assert (
normalize_workflow_reference(json.dumps({"id": "x"})) == "x"
)
def test_json_string_as_string(self):
from application.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 (
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 (
normalize_workflow_reference,
)
assert normalize_workflow_reference(42) == "42"
class TestResolveWorkflowForUser:
def test_none_workflow_returns_none(self, pg_conn):
from application.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 (
_resolve_workflow_for_user,
)
with app.app_context():
pg_id, err = _resolve_workflow_for_user(
pg_conn,
"00000000-0000-0000-0000-000000000000",
"u",
)
assert pg_id is None
assert err is not None
assert err.status_code == 404
def test_resolves_owned_workflow(self, pg_conn, app):
from application.api.user.agents.routes import (
_resolve_workflow_for_user,
)
from application.storage.db.repositories.workflows import (
WorkflowsRepository,
)
user = "owner"
wf = WorkflowsRepository(pg_conn).create(user, "wf")
with app.app_context():
pg_id, err = _resolve_workflow_for_user(
pg_conn, str(wf["id"]), user,
)
assert pg_id == str(wf["id"])
assert err is None
class TestResolveFolderId:
def test_none_returns_none(self, pg_conn):
from application.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
with app.app_context():
pg_id, err = _resolve_folder_id(
pg_conn, "00000000-0000-0000-0000-000000000000", "u",
)
assert pg_id is None
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 (
AgentFoldersRepository,
)
user = "u"
folder = AgentFoldersRepository(pg_conn).create(user, "F")
with app.app_context():
pg_id, err = _resolve_folder_id(pg_conn, str(folder["id"]), user)
assert pg_id == str(folder["id"])
assert err is None
class TestFormatAgentOutput:
def test_basic_shape(self):
from application.api.user.agents.routes import _format_agent_output
agent = {
"id": "agent-1",
"name": "My",
"description": "D",
"source_id": "src-1",
"extra_source_ids": ["s1", "s2"],
"chunks": 4,
"retriever": "classic",
"prompt_id": "p-1",
"tools": [],
"agent_type": "classic",
"status": "draft",
"key": "secret-api-key-long",
}
out = _format_agent_output(agent)
assert out["id"] == "agent-1"
assert out["name"] == "My"
assert out["source"] == "src-1"
assert out["sources"] == ["s1", "s2"]
# masked key: first4...last4
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
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
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
agent = {
"id": "a", "name": "n",
"folder_id": "f-1", "workflow_id": "w-1",
}
out = _format_agent_output(agent)
assert out["folder_id"] == "f-1"
assert out["workflow"] == "w-1"
class TestBuildCreateKwargs:
def test_classic_kwargs(self):
from application.api.user.agents.routes import _build_create_kwargs
data = {
"description": "d",
"agent_type": "classic",
"chunks": "3",
"retriever": "classic",
}
out = _build_create_kwargs(
data, image_url="", agent_type="classic",
)
assert out["description"] == "d"
assert out["chunks"] == 3
def test_invalid_chunks_skipped(self, app):
from application.api.user.agents.routes import _build_create_kwargs
data = {"chunks": "abc"}
with app.app_context():
out = _build_create_kwargs(
data, image_url="", agent_type="classic",
)
assert "chunks" not in out
def test_prompt_id_default_not_set(self):
from application.api.user.agents.routes import _build_create_kwargs
data = {"prompt_id": "default"}
out = _build_create_kwargs(
data, image_url="", agent_type="classic",
)
assert "prompt_id" not in out
def test_image_url_used_when_provided(self):
from application.api.user.agents.routes import _build_create_kwargs
out = _build_create_kwargs(
{}, image_url="/upload/img.png", agent_type="classic",
)
assert out.get("image") == "/upload/img.png"
+290
View File
@@ -0,0 +1,290 @@
"""Tests for application/api/user/agents/sharing.py.
Uses the ephemeral ``pg_conn`` fixture to exercise the real PG repository
code paths (agents, users).
"""
from contextlib import contextmanager
from unittest.mock import patch
import pytest
from flask import Flask
@pytest.fixture
def app():
return Flask(__name__)
@contextmanager
def _patch_db(conn):
@contextmanager
def _yield():
yield conn
with patch(
"application.api.user.agents.sharing.db_session", _yield
), patch(
"application.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
agent = AgentsRepository(pg_conn).create(
user_id,
"Agent",
"published",
description="desc",
)
if shared:
AgentsRepository(pg_conn).update(
str(agent["id"]), user_id,
{"shared": True, "shared_token": shared_token},
)
agent = AgentsRepository(pg_conn).get(str(agent["id"]), user_id)
return agent
class TestSharedAgentGet:
def test_returns_400_missing_token(self, app):
from application.api.user.agents.sharing import SharedAgent
with app.test_request_context("/api/shared_agent"):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 400
def test_returns_404_for_unknown_token(self, app, pg_conn):
from application.api.user.agents.sharing import SharedAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agent?token=unknown"
):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 404
def test_returns_agent_for_known_token(self, app, pg_conn):
from application.api.user.agents.sharing import SharedAgent
_make_agent(pg_conn, shared=True, shared_token="abc123")
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agent?token=abc123"
):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 200
data = response.json
assert data["shared_token"] == "abc123"
assert data["shared"] is True
def test_records_shared_with_different_user(self, app, pg_conn):
from application.api.user.agents.sharing import SharedAgent
_make_agent(pg_conn, user_id="owner", shared=True, shared_token="tk1")
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agent?token=tk1"
):
from flask import request
request.decoded_token = {"sub": "other-user"}
response = SharedAgent().get()
assert response.status_code == 200
def test_db_error_returns_400(self, app):
from application.api.user.agents.sharing import SharedAgent
@contextmanager
def _broken():
raise RuntimeError("boom")
yield
with patch(
"application.api.user.agents.sharing.db_readonly", _broken
), app.test_request_context("/api/shared_agent?token=x"):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 400
class TestSharedAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import SharedAgents
with app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = None
response = SharedAgents().get()
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
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agents"
):
from flask import request
request.decoded_token = {"sub": "new-user"}
response = SharedAgents().get()
assert response.status_code == 200
assert response.json == []
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
_make_agent(pg_conn, user_id="owner", shared=True, shared_token="tk2")
# Trigger add-shared flow via SharedAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agent?token=tk2"
):
from flask import request
request.decoded_token = {"sub": "viewer-user"}
SharedAgent().get()
with _patch_db(pg_conn), app.test_request_context(
"/api/shared_agents"
):
from flask import request
request.decoded_token = {"sub": "viewer-user"}
response = SharedAgents().get()
assert response.status_code == 200
assert len(response.json) == 1
assert response.json[0]["shared"] is True
def test_db_error_returns_400(self, app):
from application.api.user.agents.sharing import SharedAgents
@contextmanager
def _broken():
raise RuntimeError("boom")
yield
with patch(
"application.api.user.agents.sharing.db_session", _broken
), app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = {"sub": "u"}
response = SharedAgents().get()
assert response.status_code == 400
class TestShareAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent", method="PUT", json={"id": "x", "shared": True}
):
from flask import request
request.decoded_token = None
response = ShareAgent().put()
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent", method="PUT", json={"shared": True}
):
from flask import request
request.decoded_token = {"sub": "u"}
response = ShareAgent().put()
assert response.status_code == 400
def test_returns_400_missing_shared(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent", method="PUT", json={"id": "x"}
):
from flask import request
request.decoded_token = {"sub": "u"}
response = ShareAgent().put()
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app, pg_conn):
from application.api.user.agents.sharing import ShareAgent
with _patch_db(pg_conn), app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": "00000000-0000-0000-0000-000000000000", "shared": True},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = ShareAgent().put()
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
agent = _make_agent(pg_conn, user_id="owner")
agent_id = str(agent["id"])
with _patch_db(pg_conn), app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": agent_id, "shared": True, "username": "alice"},
):
from flask import request
request.decoded_token = {"sub": "owner"}
response = ShareAgent().put()
assert response.status_code == 200
assert response.json["success"] is True
assert response.json["shared_token"] # non-empty token generated
got = AgentsRepository(pg_conn).get(agent_id, "owner")
assert got["shared"] is True
assert got["shared_token"]
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
agent = _make_agent(pg_conn, user_id="owner", shared=True, shared_token="tk")
agent_id = str(agent["id"])
with _patch_db(pg_conn), app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": agent_id, "shared": False},
):
from flask import request
request.decoded_token = {"sub": "owner"}
response = ShareAgent().put()
assert response.status_code == 200
got = AgentsRepository(pg_conn).get(agent_id, "owner")
assert got["shared"] is False
assert got["shared_token"] is None
def test_db_error_returns_400(self, app):
from application.api.user.agents.sharing import ShareAgent
@contextmanager
def _broken():
raise RuntimeError("boom")
yield
with patch(
"application.api.user.agents.sharing.db_session", _broken
), app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": "x", "shared": True},
):
from flask import request
request.decoded_token = {"sub": "u"}
response = ShareAgent().put()
assert response.status_code == 400
+297
View File
@@ -0,0 +1,297 @@
"""Gap-coverage tests for application.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 application.api.user.agents.webhooks import AgentWebhookListener
mock_task = Mock()
mock_task.id = "task_empty"
with patch(
"application.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 application.api.user.agents.webhooks import AgentWebhookListener
mock_task = Mock()
mock_task.id = "task_noqs"
with patch(
"application.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 application.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"
) 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 application.api.user.agents.webhooks import AgentWebhookListener
with patch(
"application.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(
"application.api.user.agents.webhooks.db_session", _yield
), patch(
"application.api.user.agents.webhooks.db_readonly", _yield
):
yield
@contextmanager
def _patch_base_db(conn):
@contextmanager
def _yield():
yield conn
with patch(
"application.api.user.base.db_readonly", _yield
), patch(
"application.api.user.base.db_session", _yield
):
yield
class TestAgentWebhookGet:
def test_returns_401_unauthenticated(self, app):
from application.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 application.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 application.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 application.api.user.agents.webhooks import AgentWebhook
from application.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",
"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 application.api.user.agents.webhooks import AgentWebhook
from application.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(
"application.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 application.api.user.agents.webhooks import AgentWebhookListener
from application.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(
"application.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 application.api.user.agents.webhooks import AgentWebhookListener
from application.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(
"application.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
@@ -0,0 +1,407 @@
"""Idempotency-Key behavior on the agent webhook listener route."""
from contextlib import contextmanager
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
@pytest.fixture
def app():
return Flask(__name__)
@contextmanager
def _patch_db(conn):
@contextmanager
def _yield():
yield conn
with patch(
"application.api.user.agents.webhooks.db_session", _yield
), patch(
"application.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
return AgentsRepository(pg_conn).create(
user, "a", "published", incoming_webhook_token=token, **kw,
)
def _apply_async_mock():
"""Mock for ``process_agent_webhook.apply_async``; ``task.id`` mirrors the predetermined id."""
def _side_effect(*args, **kwargs):
return MagicMock(id=kwargs.get("task_id") or "auto-task-id")
return MagicMock(side_effect=_side_effect)
class TestWebhookIdempotency:
def test_no_header_enqueues_normally(self, app, pg_conn):
from application.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",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-noh", method="POST",
json={"event": "x"},
):
listener = AgentWebhookListener()
response = listener.post(
webhook_token="tk-noh",
agent=agent,
agent_id_str=str(agent["id"]),
)
assert response.status_code == 200
assert apply_mock.call_count == 1
def test_header_first_post_records_row(self, app, pg_conn):
from sqlalchemy import text
from application.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",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-first", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": "key-abc"},
):
listener = AgentWebhookListener()
response = listener.post(
webhook_token="tk-first",
agent=agent,
agent_id_str=str(agent["id"]),
)
assert response.status_code == 200
assert apply_mock.call_count == 1
predetermined_id = apply_mock.call_args.kwargs["task_id"]
assert response.json["task_id"] == predetermined_id
# Stored under the *scoped* form ``"{agent_id}:{key}"`` so two
# agents sharing the same raw header don't collapse on PK.
scoped_key = f"{agent['id']}:key-abc"
row = pg_conn.execute(
text("SELECT task_id, agent_id FROM webhook_dedup WHERE idempotency_key = :k"),
{"k": scoped_key},
).fetchone()
assert row is not None
assert row[0] == predetermined_id
assert str(row[1]) == str(agent["id"])
def test_header_forwards_idempotency_key_to_delay(self, app, pg_conn):
"""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
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",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-fwd", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": "key-fwd"},
):
listener = AgentWebhookListener()
listener.post(
webhook_token="tk-fwd",
agent=agent,
agent_id_str=str(agent["id"]),
)
# Worker sees the agent-scoped form so its dedup row is also
# agent-distinct.
scoped_key = f"{agent['id']}:key-fwd"
assert (
apply_mock.call_args.kwargs["kwargs"]["idempotency_key"]
== scoped_key
)
def test_same_header_second_post_returns_cached(self, app, pg_conn):
from application.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",
apply_mock,
):
with app.test_request_context(
"/api/webhooks/agents/tk-rep", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": "key-rep"},
):
listener = AgentWebhookListener()
first = listener.post(
webhook_token="tk-rep",
agent=agent,
agent_id_str=str(agent["id"]),
)
with app.test_request_context(
"/api/webhooks/agents/tk-rep", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": "key-rep"},
):
listener = AgentWebhookListener()
second = listener.post(
webhook_token="tk-rep",
agent=agent,
agent_id_str=str(agent["id"]),
)
assert first.status_code == 200
assert second.status_code == 200
assert first.json == second.json
assert apply_mock.call_count == 1
def test_concurrent_same_key_only_one_apply_async(self, app, pg_engine):
"""Race test (M3): N parallel webhook POSTs with same key → only ONE apply_async.
Uses ``pg_engine`` so each thread checks out its own DB connection
(sharing a single Connection serializes at the driver level).
"""
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
with pg_engine.begin() as conn:
agent = AgentsRepository(conn).create(
"u-race", "a", "published", incoming_webhook_token="tk-race",
)
apply_mock = _apply_async_mock()
@contextmanager
def _engine_session():
with pg_engine.begin() as conn:
yield conn
@contextmanager
def _engine_readonly():
with pg_engine.connect() as conn:
yield conn
def fire(idx):
# Patches sit outside the thread pool (see below); only the
# per-thread Flask request context is set up inside.
with app.test_request_context(
"/api/webhooks/agents/tk-race", method="POST",
json={"event": idx},
headers={"Idempotency-Key": "wh-race"},
):
listener = AgentWebhookListener()
return listener.post(
webhook_token="tk-race",
agent=agent,
agent_id_str=str(agent["id"]),
)
# ``unittest.mock.patch`` is not thread-safe; set up
# 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",
_engine_session,
), patch(
"application.api.user.agents.webhooks.db_readonly",
_engine_readonly,
), patch(
"application.api.user.agents.webhooks.process_agent_webhook.apply_async",
apply_mock,
), ThreadPoolExecutor(max_workers=8) as ex:
responses = list(ex.map(fire, range(8)))
assert all(r.status_code == 200 for r in responses)
assert apply_mock.call_count == 1
ids = {r.json["task_id"] for r in responses}
assert len(ids) == 1
assert "deduplicated" not in ids
def test_same_key_different_agent_does_not_collide(self, app, pg_conn):
"""Idempotency keys are now scoped by ``agent_id`` — two agents
sending the same raw header each get their own dedup row, both
requests enqueue work, and the responses carry distinct
``task_id``s. (Pre-fix, the second agent's request was silently
deduplicated against the first agent's row.)
"""
from sqlalchemy import text as sql_text
from application.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",
apply_mock,
):
with app.test_request_context(
"/api/webhooks/agents/tk-a", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": "global-key"},
):
listener = AgentWebhookListener()
first = listener.post(
webhook_token="tk-a",
agent=agent_a,
agent_id_str=str(agent_a["id"]),
)
with app.test_request_context(
"/api/webhooks/agents/tk-b", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": "global-key"},
):
listener = AgentWebhookListener()
second = listener.post(
webhook_token="tk-b",
agent=agent_b,
agent_id_str=str(agent_b["id"]),
)
assert first.status_code == 200
assert second.status_code == 200
assert first.json["task_id"] != second.json["task_id"]
assert apply_mock.call_count == 2
# And there are two ``webhook_dedup`` rows: one per agent scope.
rows = pg_conn.execute(
sql_text(
"SELECT idempotency_key, agent_id FROM webhook_dedup "
"WHERE idempotency_key LIKE :pat ORDER BY idempotency_key"
),
{"pat": "%:global-key"},
).fetchall()
assert len(rows) == 2
scopes = {str(r[1]) for r in rows}
assert scopes == {str(agent_a["id"]), str(agent_b["id"])}
def test_empty_header_treated_as_absent(self, app, pg_conn):
from sqlalchemy import text
from application.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",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-empty", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": ""},
):
listener = AgentWebhookListener()
response = listener.post(
webhook_token="tk-empty",
agent=agent,
agent_id_str=str(agent["id"]),
)
assert response.status_code == 200
assert apply_mock.call_count == 1
count = pg_conn.execute(
text("SELECT count(*) FROM webhook_dedup")
).scalar()
assert count == 0
def test_oversized_header_rejected_with_400(self, app, pg_conn):
from application.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",
) as mock_apply, app.test_request_context(
"/api/webhooks/agents/tk-big", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": oversized},
):
listener = AgentWebhookListener()
response = listener.post(
webhook_token="tk-big",
agent=agent,
agent_id_str=str(agent["id"]),
)
assert response.status_code == 400
assert mock_apply.call_count == 0
def test_stale_dedup_row_does_not_block_new_work(self, app, pg_conn):
"""Regression for the TTL fail-shut bug: a >24h-old dedup row
must not silently drop a new request. Pre-fix, the second POST
returned ``task_id="deduplicated"`` and never enqueued.
"""
from sqlalchemy import text
from application.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",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-stale", method="POST",
json={"event": "x"},
headers={"Idempotency-Key": "stale-key"},
):
listener = AgentWebhookListener()
first = listener.post(
webhook_token="tk-stale",
agent=agent,
agent_id_str=str(agent["id"]),
)
assert first.status_code == 200
first_task_id = first.json["task_id"]
assert first_task_id != "deduplicated"
# Backdate the row so it looks 25h old.
scoped_key = f"{agent['id']}:stale-key"
pg_conn.execute(
text(
"UPDATE webhook_dedup SET created_at = "
"clock_timestamp() - make_interval(hours => 25) "
"WHERE idempotency_key = :k"
),
{"k": scoped_key},
)
# 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",
apply_mock,
), app.test_request_context(
"/api/webhooks/agents/tk-stale", method="POST",
json={"event": "x2"},
headers={"Idempotency-Key": "stale-key"},
):
listener = AgentWebhookListener()
second = listener.post(
webhook_token="tk-stale",
agent=agent,
agent_id_str=str(agent["id"]),
)
assert second.status_code == 200
assert second.json["task_id"] != "deduplicated"
assert second.json["task_id"] != first_task_id
assert apply_mock.call_count == 2