fed8b2eed7
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
180 lines
7.0 KiB
Python
180 lines
7.0 KiB
Python
"""Integration tests for ``UsersRepository`` against a live Postgres.
|
|
|
|
These tests need:
|
|
|
|
* A running Postgres reachable via ``POSTGRES_URI``
|
|
* Alembic migration ``0001_initial`` applied
|
|
|
|
They are skipped automatically by the default ``pytest`` run because
|
|
``pytest.ini`` has ``--ignore=tests/integration`` in ``addopts``. They
|
|
are additionally marked ``@pytest.mark.integration`` so they can be
|
|
selected/excluded explicitly. Run them locally with::
|
|
|
|
.venv/bin/python -m pytest tests/integration/test_users_repository.py \\
|
|
--override-ini="addopts=" --no-cov
|
|
|
|
Covers every operation the legacy Mongo code performs on
|
|
``users_collection``: upsert + get + add/remove on the pinned and
|
|
shared_with_me JSONB arrays, plus explicit tenant-isolation checks
|
|
(a user's operations must never touch another user's data).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from application.storage.db.repositories.users import UsersRepository
|
|
|
|
|
|
@pytest.fixture
|
|
def repo(pg_clean_users):
|
|
return UsersRepository(pg_clean_users)
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestUpsert:
|
|
def test_upsert_creates_new_user_with_default_preferences(self, repo):
|
|
doc = repo.upsert("alice@example.com")
|
|
assert doc["user_id"] == "alice@example.com"
|
|
assert doc["agent_preferences"] == {"pinned": [], "shared_with_me": []}
|
|
assert "id" in doc
|
|
assert "_id" in doc # Mongo-compat alias
|
|
|
|
def test_upsert_is_idempotent(self, repo):
|
|
first = repo.upsert("alice@example.com")
|
|
second = repo.upsert("alice@example.com")
|
|
assert first["id"] == second["id"]
|
|
assert first["agent_preferences"] == second["agent_preferences"]
|
|
|
|
def test_upsert_preserves_existing_preferences(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-1")
|
|
doc = repo.upsert("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-1"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestGet:
|
|
def test_get_returns_none_for_missing_user(self, repo):
|
|
assert repo.get("nobody@example.com") is None
|
|
|
|
def test_get_returns_dict_for_existing_user(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
doc = repo.get("alice@example.com")
|
|
assert doc is not None
|
|
assert doc["user_id"] == "alice@example.com"
|
|
assert doc["agent_preferences"] == {"pinned": [], "shared_with_me": []}
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestAddPinned:
|
|
def test_add_pinned_to_new_user_creates_user(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-1")
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-1"]
|
|
|
|
def test_add_pinned_is_idempotent(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-1")
|
|
repo.add_pinned("alice@example.com", "agent-1")
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-1"]
|
|
|
|
def test_add_pinned_preserves_order(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-1")
|
|
repo.add_pinned("alice@example.com", "agent-2")
|
|
repo.add_pinned("alice@example.com", "agent-3")
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-1", "agent-2", "agent-3"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestRemovePinned:
|
|
def test_remove_pinned_single(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-1")
|
|
repo.add_pinned("alice@example.com", "agent-2")
|
|
repo.remove_pinned("alice@example.com", "agent-1")
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-2"]
|
|
|
|
def test_remove_pinned_missing_is_noop(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-1")
|
|
repo.remove_pinned("alice@example.com", "agent-999")
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-1"]
|
|
|
|
def test_remove_pinned_bulk(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
for i in range(5):
|
|
repo.add_pinned("alice@example.com", f"agent-{i}")
|
|
repo.remove_pinned_bulk("alice@example.com", ["agent-1", "agent-3", "agent-999"])
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-0", "agent-2", "agent-4"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestSharedWithMe:
|
|
def test_add_shared_is_idempotent(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_shared("alice@example.com", "agent-x")
|
|
repo.add_shared("alice@example.com", "agent-x")
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["shared_with_me"] == ["agent-x"]
|
|
|
|
def test_remove_shared_bulk(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
for i in range(3):
|
|
repo.add_shared("alice@example.com", f"shared-{i}")
|
|
repo.remove_shared_bulk("alice@example.com", ["shared-0", "shared-2"])
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["shared_with_me"] == ["shared-1"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestRemoveAgentFromAll:
|
|
def test_removes_from_both_pinned_and_shared(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-X")
|
|
repo.add_pinned("alice@example.com", "agent-keep")
|
|
repo.add_shared("alice@example.com", "agent-X")
|
|
repo.add_shared("alice@example.com", "agent-keep-2")
|
|
|
|
repo.remove_agent_from_all("alice@example.com", "agent-X")
|
|
|
|
doc = repo.get("alice@example.com")
|
|
assert doc["agent_preferences"]["pinned"] == ["agent-keep"]
|
|
assert doc["agent_preferences"]["shared_with_me"] == ["agent-keep-2"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestTenantIsolation:
|
|
"""Security-critical: operations on one user must never touch another's data."""
|
|
|
|
def test_add_pinned_does_not_leak_across_users(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.upsert("bob@example.com")
|
|
repo.add_pinned("alice@example.com", "agent-a")
|
|
repo.add_pinned("bob@example.com", "agent-b")
|
|
|
|
alice = repo.get("alice@example.com")
|
|
bob = repo.get("bob@example.com")
|
|
assert alice["agent_preferences"]["pinned"] == ["agent-a"]
|
|
assert bob["agent_preferences"]["pinned"] == ["agent-b"]
|
|
|
|
def test_remove_does_not_leak_across_users(self, repo):
|
|
repo.upsert("alice@example.com")
|
|
repo.upsert("bob@example.com")
|
|
repo.add_pinned("alice@example.com", "shared-agent-id")
|
|
repo.add_pinned("bob@example.com", "shared-agent-id")
|
|
|
|
repo.remove_pinned("alice@example.com", "shared-agent-id")
|
|
|
|
assert repo.get("alice@example.com")["agent_preferences"]["pinned"] == []
|
|
assert repo.get("bob@example.com")["agent_preferences"]["pinned"] == [
|
|
"shared-agent-id"
|
|
]
|