Skip to content

Commit d2a804a

Browse files
authored
Merge pull request #7706 from freedomofpress/7704-written-and-read
fix(`/data`): don't reread items already emitted by processed events
2 parents e74ebb2 + c53ad76 commit d2a804a

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

securedrop/journalist_app/api2/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,28 @@ def data() -> Response:
121121
response.items[uuid] = item.to_api_v2() if item is not None else None
122122
response.events[result.event_id] = result.status
123123

124+
# The set of items (UUIDs) that were emitted by processed events.
125+
items_emitted = frozenset(response.items.keys())
126+
124127
if requested.sources:
125128
source_query: EagerQuery = eager_query("Source")
126129
for source in source_query.filter(Source.uuid.in_(str(uuid) for uuid in requested.sources)):
127130
response.sources[source.uuid] = source.to_api_v2()
128131

129132
if requested.items:
133+
# If an item was explicitly requested but was already emitted by a
134+
# processed event, we don't need to (and shouldn't) reread it.
135+
left_to_read = set(requested.items) - items_emitted
136+
130137
submission_query: EagerQuery = eager_query("Submission")
131138
for item in submission_query.filter(
132-
Submission.uuid.in_(str(uuid) for uuid in requested.items)
139+
Submission.uuid.in_(str(uuid) for uuid in left_to_read)
133140
):
134141
response.items[item.uuid] = item.to_api_v2()
135142

136143
reply_query: EagerQuery = eager_query("Reply")
137-
for item in reply_query.filter(Reply.uuid.in_(str(uuid) for uuid in requested.items)):
138-
if item.uuid in response.items:
144+
for item in reply_query.filter(Reply.uuid.in_(str(uuid) for uuid in left_to_read)):
145+
if item.uuid in response.items.keys() - items_emitted:
139146
# Fail if we get unlucky and hit a UUID collision between the
140147
# `Submission` and `Reply` tables. This is vanishingly unlikely,
141148
# but SQLite can't enforce uniqueness between them.

securedrop/tests/test_journalist_api2.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,3 +949,52 @@ def test_api2_source_conversation_deleted_resubmission(
949949
)
950950
assert res3.status_code == 200
951951
assert res3.json["events"][event.id][0] == 208
952+
953+
954+
def test_api2_reply_sent_then_requested_item_is_deduped(
955+
journalist_app,
956+
journalist_api_token,
957+
test_files,
958+
):
959+
"""
960+
When a reply is created by a `REPLY_SENT` event and the same UUID is also
961+
requested in the same `BatchRequest`, the request should succeed (200) and
962+
return the reply once.
963+
"""
964+
with journalist_app.test_client() as app:
965+
# Fetch an existing reply so that we can resubmit it.
966+
source = test_files["source"]
967+
reply = test_files["replies"][0]
968+
reply_res = app.get(
969+
url_for("api.download_reply", source_uuid=source.uuid, reply_uuid=reply.uuid),
970+
headers=get_api_headers(journalist_api_token),
971+
)
972+
reply_ct = reply_res.data
973+
armored_ct = ascii_armor(reply_ct)
974+
975+
# Get current source version to build a valid "reply_sent" event.
976+
index = app.get(
977+
url_for("api2.index"),
978+
headers=get_api_headers(journalist_api_token),
979+
)
980+
assert index.status_code == 200
981+
source_version = index.json["sources"][source.uuid]
982+
983+
new_reply_uuid = str(uuid.uuid4())
984+
event = Event(
985+
id="987654321",
986+
target=SourceTarget(source_uuid=source.uuid, version=source_version),
987+
type=EventType.REPLY_SENT,
988+
data={"uuid": new_reply_uuid, "reply": armored_ct},
989+
)
990+
991+
# The same batch both creates the reply (`events`) and requests it (`items`).
992+
response = app.post(
993+
url_for("api2.data"),
994+
json={"events": [asdict(event)], "items": [new_reply_uuid]},
995+
headers=get_api_headers(journalist_api_token),
996+
)
997+
assert response.status_code == 200
998+
assert response.json["events"][event.id] == [200, None]
999+
assert new_reply_uuid in response.json["items"]
1000+
assert response.json["items"][new_reply_uuid] is not None

0 commit comments

Comments
 (0)