@@ -1106,3 +1106,104 @@ def test_api_minor_versions(journalist_app, journalist_api_token, test_files, mi
11061106
11071107 else :
11081108 assert "events" not in resp .json
1109+
1110+
1111+ def test_api2_source_conversation_truncated (
1112+ journalist_app ,
1113+ journalist_api_token ,
1114+ test_files ,
1115+ ):
1116+ """
1117+ Test processing of the "source_conversation_truncated" event.
1118+ Items with interaction_count <= upper_bound must be deleted.
1119+ Items with interaction_count > upper_bound must remain.
1120+ """
1121+ with journalist_app .test_client () as app :
1122+ source = test_files ["source" ]
1123+
1124+ # Ensure we have submissions/replies and interaction_count fields
1125+ assert len (test_files ["submissions" ]) >= 1
1126+ assert len (test_files ["replies" ]) >= 1
1127+
1128+ # Fetch index to get current versions and interaction counts
1129+ index = app .get (
1130+ url_for ("api2.index" ),
1131+ headers = get_api_headers (journalist_api_token ),
1132+ )
1133+ assert index .status_code == 200
1134+
1135+ # Build a map of item_uuid -> interaction_count
1136+ item_uuids = [item .uuid for item in (test_files ["submissions" ] + test_files ["replies" ])]
1137+
1138+ batch_resp = app .post (
1139+ url_for ("api2.data" ),
1140+ json = {"items" : item_uuids },
1141+ headers = get_api_headers (journalist_api_token ),
1142+ )
1143+ assert batch_resp .status_code == 200
1144+ data = batch_resp .json
1145+
1146+ initial_counts = {
1147+ item_uuid : item ["interaction_count" ] for item_uuid , item in data ["items" ].items ()
1148+ }
1149+
1150+ # Choose a bound that deletes some but not all items
1151+ # Pick the median interaction_count so we get both outcomes
1152+ sorted_counts = sorted (initial_counts .values ())
1153+ upper_bound = sorted_counts [len (sorted_counts ) // 2 ]
1154+
1155+ source_version = index .json ["sources" ][source .uuid ]
1156+
1157+ event = Event (
1158+ id = "999001" ,
1159+ target = SourceTarget (source_uuid = source .uuid , version = source_version ),
1160+ type = EventType .SOURCE_CONVERSATION_TRUNCATED ,
1161+ data = {"upper_bound" : upper_bound },
1162+ )
1163+
1164+ response = app .post (
1165+ url_for ("api2.data" ),
1166+ json = {"events" : [asdict (event )]},
1167+ headers = get_api_headers (journalist_api_token ),
1168+ )
1169+ assert response .status_code == 200
1170+
1171+ status_code , msg = response .json ["events" ][event .id ]
1172+ # Because some deletes may fail (simulated) and some succeed, the handler
1173+ # returns 200 if all succeed, or 207 (MultiStatus) if any fail.
1174+ # The test_files fixtures never cause delete_file_object() to raise,
1175+ # so OK (200) is expected.
1176+ assert status_code in (200 , 207 )
1177+
1178+ # Verify item-wise results
1179+ returned_items = response .json ["items" ]
1180+ assert isinstance (returned_items , dict )
1181+
1182+ for item_uuid , count in initial_counts .items ():
1183+ if count <= upper_bound :
1184+ # Must be returned as deleted: {uuid: None}
1185+ assert item_uuid in returned_items
1186+ assert returned_items [item_uuid ] is None
1187+ # Also confirm removal in DB
1188+ assert (
1189+ Submission .query .filter (Submission .uuid == item_uuid ).one_or_none ()
1190+ or Reply .query .filter (Reply .uuid == item_uuid ).one_or_none ()
1191+ ) is None
1192+ else :
1193+ # Must not be deleted
1194+ assert (
1195+ Submission .query .filter (Submission .uuid == item_uuid ).one_or_none ()
1196+ or Reply .query .filter (Reply .uuid == item_uuid ).one_or_none ()
1197+ ) is not None
1198+
1199+ # Source must still exist
1200+ assert Source .query .filter (Source .uuid == source .uuid ).one_or_none () is not None
1201+
1202+ # Resubmission must yield "Already Reported" (208)
1203+ res2 = app .post (
1204+ url_for ("api2.data" ),
1205+ json = {"events" : [asdict (event )]},
1206+ headers = get_api_headers (journalist_api_token ),
1207+ )
1208+ assert res2 .status_code == 200
1209+ assert res2 .json ["events" ][event .id ][0 ] == 208
0 commit comments