Files
GIA/core/tests/test_event_projection_shadow.py

160 lines
5.3 KiB
Python

from io import StringIO
import time
from django.core.management import call_command
from django.test import TestCase, override_settings
from core.events.ledger import append_event_sync
from core.events.projection import shadow_compare_session
from core.models import ChatSession, Message, Person, PersonIdentifier, User
@override_settings(EVENT_LEDGER_DUAL_WRITE=True)
class EventProjectionShadowTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username="projection-user",
email="projection@example.com",
password="x",
)
self.person = Person.objects.create(user=self.user, name="Projection Person")
self.identifier = PersonIdentifier.objects.create(
user=self.user,
person=self.person,
service="signal",
identifier="+15555550333",
)
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
def test_shadow_compare_has_zero_mismatch_when_projection_matches(self):
message = Message.objects.create(
user=self.user,
session=self.session,
ts=1700000000000,
sender_uuid="+15555550333",
text="hello",
delivered_ts=1700000000000,
read_ts=1700000000500,
receipt_payload={
"reactions": [
{
"source_service": "signal",
"actor": "user:1:signal",
"emoji": "👍",
"removed": False,
}
]
},
)
append_event_sync(
user=self.user,
session=self.session,
ts=1700000000000,
event_type="message_created",
direction="in",
origin_transport="signal",
origin_message_id=str(message.id),
payload={"message_id": str(message.id), "text": "hello"},
)
append_event_sync(
user=self.user,
session=self.session,
ts=1700000000500,
event_type="read_receipt",
direction="system",
origin_transport="signal",
origin_message_id=str(message.id),
payload={"message_id": str(message.id), "read_ts": 1700000000500},
)
append_event_sync(
user=self.user,
session=self.session,
ts=1700000000600,
event_type="reaction_added",
direction="system",
actor_identifier="user:1:signal",
origin_transport="signal",
origin_message_id=str(message.id),
payload={
"message_id": str(message.id),
"emoji": "👍",
"source_service": "signal",
"actor": "user:1:signal",
},
)
compared = shadow_compare_session(self.session, detail_limit=10)
self.assertEqual(0, compared["mismatch_total"])
def test_shadow_compare_detects_missing_projection_row(self):
Message.objects.create(
user=self.user,
session=self.session,
ts=1700000000000,
sender_uuid="+15555550333",
text="no-event",
)
compared = shadow_compare_session(self.session, detail_limit=10)
self.assertGreater(compared["counters"]["missing_in_projection"], 0)
self.assertIn("cause_samples", compared)
self.assertIn("missing_event_write", compared["cause_samples"])
self.assertGreaterEqual(
len(compared["cause_samples"]["missing_event_write"]),
1,
)
def test_management_command_emits_summary(self):
out = StringIO()
call_command(
"event_projection_shadow",
user_id=str(self.user.id),
limit_sessions=5,
stdout=out,
)
rendered = out.getvalue()
self.assertIn("shadow compare:", rendered)
self.assertIn("cause_counts=", rendered)
def test_management_command_supports_service_and_recent_filters(self):
Message.objects.create(
user=self.user,
session=self.session,
ts=int(time.time() * 1000),
sender_uuid="+15550000000",
text="recent",
source_service="signal",
source_message_id="recent-1",
)
out = StringIO()
call_command(
"event_projection_shadow",
user_id=str(self.user.id),
service="signal",
recent_minutes=120,
limit_sessions=5,
stdout=out,
)
rendered = out.getvalue()
self.assertIn("shadow compare:", rendered)
def test_management_command_supports_recent_only_switch(self):
Message.objects.create(
user=self.user,
session=self.session,
ts=int(time.time() * 1000),
sender_uuid="+15550000001",
text="recent-only",
source_service="signal",
source_message_id="recent-only-1",
)
out = StringIO()
call_command(
"event_projection_shadow",
user_id=str(self.user.id),
recent_only=True,
limit_sessions=5,
stdout=out,
)
rendered = out.getvalue()
self.assertIn("shadow compare:", rendered)