Files
mcp-nextcloud/tests/unit/vector/test_nats_producer.py
T
Chris CoutinhoandClaude Opus 4.8 2d845cb70f fix: address PR #814 reviewer follow-ups
- gateway_client: guard token cache with a lazy anyio.Lock so concurrent
  embed calls share one M2M token request instead of racing
- status subscriber: distinguish idle fetch timeouts from real broker
  errors (log + 5s backoff) instead of swallowing all and spinning
- nats: warn when the bus URL uses unencrypted transport (non-tls://)
- collection_metadata: accept an optional shared httpx client, make TLS
  verify explicit, document the unauthenticated control-plane contract
- replace python -O-stripped asserts with explicit ValueError in the bus
  status builder and the api metadata source
- document why the nil-UUID sentinel point can't collide with content ids

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:42:09 +02:00

156 lines
4.7 KiB
Python

"""NATS ingest producer: DocumentTask → IngestMessage + dedup header (§3.4)."""
import hashlib
import json
from pathlib import Path
import pytest
from nextcloud_mcp_server.canonical import canonical_json
from nextcloud_mcp_server.vector.queue.factory import _transport_for
from nextcloud_mcp_server.vector.queue.nats import (
NatsTaskProducer,
_modified_at_rfc3339,
msg_id,
warn_if_insecure_nats_url,
)
from nextcloud_mcp_server.vector.queue.postgres import PostgresTaskProducer
from nextcloud_mcp_server.vector.scanner import DocumentTask
FIXTURE = Path(__file__).parents[2] / "fixtures" / "ingest_message_example.json"
TENANT = "00000000-0000-0000-0000-000000000001"
def _producer(mocker, tenant_id=TENANT):
return NatsTaskProducer(
nc=mocker.MagicMock(), js=mocker.AsyncMock(), tenant_id=tenant_id
)
def test_ingest_message_translation(mocker):
p = _producer(mocker)
task = DocumentTask(
user_id="alice",
doc_id="12345",
doc_type="file",
operation="index",
modified_at=1700000000,
file_path="/Documents/report.pdf",
etag="etag-abc123",
)
msg = p.ingest_message(task)
assert msg["tenant_id"] == TENANT # from settings, not the task
assert msg["content_hash"] == "etag-abc123" # etag wins
assert msg["user_id"] == "alice"
assert msg["doc_type"] == "file"
assert msg["operation"] == "index"
assert msg["file_path"] == "/Documents/report.pdf"
def test_content_hash_falls_back_to_modified_at(mocker):
p = _producer(mocker)
task = DocumentTask(
user_id="u", doc_id="d", doc_type="note", operation="delete", modified_at=0
)
assert p.ingest_message(task)["content_hash"] == "0"
async def test_send_publishes_with_dedup_header(mocker):
p = _producer(mocker)
task = DocumentTask(
user_id="alice",
doc_id="12345",
doc_type="file",
operation="index",
modified_at=1700000000,
etag="e",
)
await p.send(task)
p._js.publish.assert_awaited_once()
args = p._js.publish.await_args.args
kwargs = p._js.publish.await_args.kwargs
assert args[0] == f"mcp.ingest.requested.{TENANT}"
expected_mid = msg_id(TENANT, "12345", _modified_at_rfc3339(1700000000))
assert kwargs["headers"]["Nats-Msg-Id"] == expected_mid
assert json.loads(args[1])["doc_id"] == "12345"
def test_msg_id_known_vector():
mid = msg_id("t", "d", "2026-01-01T00:00:00+00:00")
expected = hashlib.sha256(
canonical_json(
{
"tenant_id": "t",
"doc_id": "d",
"modified_at": "2026-01-01T00:00:00+00:00",
}
)
).hexdigest()
assert mid == expected
def test_publisher_matches_shared_fixture(mocker):
# The same fixture is validated as an IngestMessage in the processor repo.
# Here we assert the publisher emits exactly the fixture's key set + stable
# field values (modified_at format is allowed to differ — epoch→ISO).
fixture = json.loads(FIXTURE.read_text(encoding="utf-8"))
p = _producer(mocker, tenant_id=fixture["tenant_id"])
task = DocumentTask(
user_id=fixture["user_id"],
doc_id=fixture["doc_id"],
doc_type=fixture["doc_type"],
operation=fixture["operation"],
modified_at=1764201600,
file_path=fixture["file_path"],
etag=fixture["content_hash"],
)
msg = p.ingest_message(task)
assert set(msg.keys()) == set(fixture.keys())
for key in (
"tenant_id",
"doc_id",
"content_hash",
"doc_type",
"operation",
"user_id",
"file_path",
):
assert msg[key] == fixture[key]
assert msg["modified_at"] # non-empty ISO timestamp
@pytest.mark.parametrize(
"url,expected",
[
("nats://nats:4222", "nats"),
("postgres://h/db", "postgres"),
("postgresql://h/db", "postgres"),
("https://elsewhere", "nats"),
],
)
def test_transport_for(url, expected):
assert _transport_for(url) == expected
async def test_postgres_producer_is_a_seam():
with pytest.raises(NotImplementedError, match="documented seam"):
await PostgresTaskProducer.connect(object())
@pytest.mark.parametrize(
"url,should_warn",
[
("nats://nats:4222", True),
("ws://nats:8080", True),
("tls://nats:4222", False),
("wss://nats:8080", False),
],
)
def test_warn_if_insecure_nats_url(url, should_warn, caplog):
import logging
with caplog.at_level(logging.WARNING):
warn_if_insecure_nats_url(url)
warned = any("unencrypted transport" in r.getMessage() for r in caplog.records)
assert warned is should_warn