162 lines
5.4 KiB
Python
162 lines
5.4 KiB
Python
import time
|
|
from io import StringIO
|
|
|
|
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)
|