gateway/tests/unit/services/test_p1d_consent_prefs.py

298 lines
11 KiB
Python

#!/usr/bin/env python3
"""Unit tests for P1d: consent gating, preference parsing, and walker behaviour.
Tests
-----
1. Bootstrap runner skips when ``knowledgeIngestionEnabled=False``.
2. ``loadConnectionPrefs`` returns safe defaults when preferences are absent.
3. ``loadConnectionPrefs`` maps all §2.6 keys correctly from a full prefs dict.
4. Gmail walker passes ``neutralize=True`` and ``mailContentDepth`` to IngestionJob.
5. Gmail walker produces only a header content-object when depth="metadata".
6. ClickUp walker skips description when scope="titles".
"""
from __future__ import annotations
import asyncio
import os
import sys
import types
import unittest
from typing import Any, Dict, Optional
from unittest.mock import AsyncMock, MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../.."))
# ---------------------------------------------------------------------------
# 1. Bootstrap runner consent gate
# ---------------------------------------------------------------------------
class TestBootstrapConsentGate(unittest.TestCase):
"""_bootstrapJobHandler must no-op when knowledgeIngestionEnabled is False."""
def _makeJob(self, connectionId="c-test", authority="google"):
return {"payload": {"connectionId": connectionId, "authority": authority}}
def _makeConn(self, enabled: bool):
conn = MagicMock()
conn.knowledgeIngestionEnabled = enabled
return conn
def test_skips_when_consent_disabled(self):
from modules.serviceCenter.services.serviceKnowledge import subConnectorIngestConsumer as sut
fake_root = MagicMock()
fake_root.getUserConnectionById.return_value = self._makeConn(False)
with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=fake_root):
result = asyncio.get_event_loop().run_until_complete(
sut._bootstrapJobHandler(self._makeJob(), lambda *a: None)
)
assert result.get("skipped") is True
assert result.get("reason") == "consent_disabled"
fake_root.getUserConnectionById.assert_called_once_with("c-test")
def test_proceeds_when_consent_enabled(self):
"""When consent is enabled, the handler should call at least one walker."""
from modules.serviceCenter.services.serviceKnowledge import subConnectorIngestConsumer as sut
fake_root = MagicMock()
fake_root.getUserConnectionById.return_value = self._makeConn(True)
# Patch the inner walker so it doesn't do real I/O.
async def _fakeBootstrap(**kwargs):
return {"indexed": 0}
with (
patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=fake_root),
patch(
"modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGdrive.bootstrapGdrive",
new=AsyncMock(return_value={"indexed": 0}),
),
patch(
"modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail.bootstrapGmail",
new=AsyncMock(return_value={"indexed": 0}),
),
):
result = asyncio.get_event_loop().run_until_complete(
sut._bootstrapJobHandler(self._makeJob(authority="google"), lambda *a: None)
)
# Should not have 'skipped' at the top level.
assert result.get("skipped") is not True
assert result.get("authority") == "google"
# ---------------------------------------------------------------------------
# 2 + 3. loadConnectionPrefs
# ---------------------------------------------------------------------------
class TestLoadConnectionPrefs(unittest.TestCase):
def _makeConn(self, prefs: Optional[Dict[str, Any]]):
conn = MagicMock()
conn.knowledgePreferences = prefs
return conn
def _mockRoot(self, prefs):
root = MagicMock()
root.getUserConnectionById.return_value = self._makeConn(prefs)
return root
def test_returns_safe_defaults_when_prefs_none(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorPrefs import (
ConnectionIngestionPrefs,
loadConnectionPrefs,
)
with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=self._mockRoot(None)):
prefs = loadConnectionPrefs("x")
assert prefs.neutralizeBeforeEmbed is False
assert prefs.mailContentDepth == "full"
assert prefs.mailIndexAttachments is False
assert prefs.maxAgeDays == 90
assert prefs.clickupScope == "title_description"
assert prefs.gmailEnabled is True
assert prefs.driveEnabled is True
def test_maps_all_keys(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorPrefs import loadConnectionPrefs
raw = {
"neutralizeBeforeEmbed": True,
"mailContentDepth": "metadata",
"mailIndexAttachments": True,
"filesIndexBinaries": False,
"clickupScope": "with_comments",
"maxAgeDays": 30,
"surfaceToggles": {
"google": {"gmail": False, "drive": True},
"msft": {"sharepoint": False, "outlook": True},
},
}
with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=self._mockRoot(raw)):
prefs = loadConnectionPrefs("x")
assert prefs.neutralizeBeforeEmbed is True
assert prefs.mailContentDepth == "metadata"
assert prefs.mailIndexAttachments is True
assert prefs.filesIndexBinaries is False
assert prefs.clickupScope == "with_comments"
assert prefs.maxAgeDays == 30
assert prefs.gmailEnabled is False
assert prefs.driveEnabled is True
assert prefs.sharepointEnabled is False
assert prefs.outlookEnabled is True
def test_invalid_depth_falls_back_to_default(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorPrefs import loadConnectionPrefs
raw = {"mailContentDepth": "everything_please"}
with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=self._mockRoot(raw)):
prefs = loadConnectionPrefs("x")
assert prefs.mailContentDepth == "full"
# ---------------------------------------------------------------------------
# 4. Gmail walker passes neutralize + mailContentDepth to IngestionJob
# ---------------------------------------------------------------------------
class TestGmailWalkerPrefs(unittest.TestCase):
def _make_message(self, *, subject="Test", snippet="hello", body_text="full body"):
import base64
encoded = base64.urlsafe_b64encode(body_text.encode()).decode()
return {
"id": "msg-1",
"historyId": "h-42",
"threadId": "t-1",
"snippet": snippet,
"payload": {
"mimeType": "multipart/alternative",
"headers": [
{"name": "Subject", "value": subject},
{"name": "From", "value": "alice@example.com"},
{"name": "To", "value": "bob@example.com"},
{"name": "Date", "value": "Mon, 20 Apr 2026 10:00:00 +0000"},
],
"parts": [
{
"mimeType": "text/plain",
"body": {"data": encoded},
}
],
},
}
def test_neutralize_flag_forwarded(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail import (
GmailBootstrapLimits,
_ingestMessage,
GmailBootstrapResult,
)
from modules.serviceCenter.services.serviceKnowledge.mainServiceKnowledge import IngestionJob
captured_jobs = []
async def fake_requestIngestion(job: IngestionJob):
captured_jobs.append(job)
return MagicMock(status="indexed", error=None)
ks = MagicMock()
ks.requestIngestion = fake_requestIngestion
limits = GmailBootstrapLimits(neutralize=True, mailContentDepth="full")
result = GmailBootstrapResult(connectionId="c-1")
asyncio.get_event_loop().run_until_complete(
_ingestMessage(
googleGetFn=AsyncMock(return_value={}),
knowledgeService=ks,
connectionId="c-1",
mandateId="",
userId="u-1",
labelId="INBOX",
message=self._make_message(),
limits=limits,
result=result,
progressCb=None,
)
)
assert len(captured_jobs) == 1
assert captured_jobs[0].neutralize is True
def test_metadata_depth_yields_only_header(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail import (
_buildContentObjects,
)
message = self._make_message(snippet="hi", body_text="should be excluded")
parts = _buildContentObjects(message, maxBodyChars=4000, mailContentDepth="metadata")
ids = [p["contentObjectId"] for p in parts]
assert ids == ["header"]
def test_snippet_depth_yields_header_and_snippet(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail import (
_buildContentObjects,
)
message = self._make_message(snippet="hi", body_text="should be excluded")
parts = _buildContentObjects(message, maxBodyChars=4000, mailContentDepth="snippet")
ids = [p["contentObjectId"] for p in parts]
assert "header" in ids
assert "snippet" in ids
assert "body" not in ids
# ---------------------------------------------------------------------------
# 5. ClickUp walker respects clickupScope="titles"
# ---------------------------------------------------------------------------
class TestClickupWalkerScope(unittest.TestCase):
def _make_task(self):
return {
"id": "task-1",
"name": "Ship feature X",
"date_updated": "1713888000000",
"description": "This should be omitted",
"text_content": "Also omitted",
"status": {"status": "open"},
"assignees": [],
"tags": [],
"list": {"name": "Backlog"},
"folder": {},
"space": {"name": "Engineering"},
}
def test_titles_scope_omits_description(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncClickup import (
ClickupBootstrapLimits,
_buildContentObjects,
)
limits = ClickupBootstrapLimits(clickupScope="titles")
parts = _buildContentObjects(self._make_task(), limits)
ids = [p["contentObjectId"] for p in parts]
assert ids == ["header"]
assert "description" not in ids
def test_with_description_scope_includes_description(self):
from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncClickup import (
ClickupBootstrapLimits,
_buildContentObjects,
)
limits = ClickupBootstrapLimits(clickupScope="title_description")
parts = _buildContentObjects(self._make_task(), limits)
ids = [p["contentObjectId"] for p in parts]
assert "header" in ids
assert "description" in ids
if __name__ == "__main__":
unittest.main()