Coverage for app/backend/src/tests/test_upload_uses.py: 100%
153 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 22:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 22:46 +0000
1from datetime import timedelta
3import pytest
4from google.protobuf import wrappers_pb2
5from sqlalchemy import select
7from couchers.crypto import random_hex
8from couchers.db import session_scope
9from couchers.helpers.upload_uses import UploadUseType, get_upload_uses, get_upload_uses_for_keys
10from couchers.models import Base, EventOccurrence, Page, PageType, PhotoGallery, PhotoGalleryItem, Upload
11from couchers.proto import events_pb2, pages_pb2
12from couchers.utils import datetime_to_iso8601_local, now
13from tests.conftest import testconfig # noqa
14from tests.fixtures.db import generate_user
15from tests.fixtures.sessions import events_session, pages_session
16from tests.test_communities import create_community
19@pytest.fixture(autouse=True)
20def _(testconfig):
21 pass
24def _add_upload(user_id, key):
25 with session_scope() as session:
26 session.add(Upload(key=key, filename=f"{key}.jpg", creator_user_id=user_id))
29def test_get_upload_uses_unused(db):
30 user, _ = generate_user()
31 _add_upload(user.id, "unused_key")
33 with session_scope() as session:
34 assert get_upload_uses(session, "unused_key") == []
37def test_get_upload_uses_profile_gallery(db):
38 user, _ = generate_user()
39 _add_upload(user.id, "avatar_key")
40 _add_upload(user.id, "other_key")
42 with session_scope() as session:
43 gallery = PhotoGallery(owner_user_id=user.id)
44 session.add(gallery)
45 session.flush()
46 # lower position sorts first, so it's the avatar
47 session.add(PhotoGalleryItem(gallery_id=gallery.id, upload_key="avatar_key", position=1.0))
48 session.add(PhotoGalleryItem(gallery_id=gallery.id, upload_key="other_key", position=2.0))
50 with session_scope() as session:
51 avatar_uses = get_upload_uses(session, "avatar_key")
52 assert len(avatar_uses) == 1
53 assert avatar_uses[0].use_type == UploadUseType.profile_gallery_photo_avatar
54 assert avatar_uses[0].is_current
55 assert avatar_uses[0].user_id == user.id
56 assert avatar_uses[0].url is not None
57 assert f"/user/{user.username}" in avatar_uses[0].url
59 other_uses = get_upload_uses(session, "other_key")
60 assert len(other_uses) == 1
61 assert other_uses[0].use_type == UploadUseType.profile_gallery_photo
62 assert other_uses[0].is_current
63 assert other_uses[0].user_id == user.id
66def test_get_upload_uses_event(db):
67 user, token = generate_user()
68 start_time = now() + timedelta(hours=2)
69 end_time = start_time + timedelta(hours=3)
71 with session_scope() as session:
72 create_community(session, 0, 2, "Community", [user], [], None)
73 _add_upload(user.id, "event_key")
75 with events_session(token) as api:
76 res = api.CreateEvent(
77 events_pb2.CreateEventReq(
78 title="Event With Photo",
79 content="content",
80 photo_key="event_key",
81 location=events_pb2.EventLocation(address="Null Island", lat=0.1, lng=0.2),
82 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
83 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
84 )
85 )
86 event_id = res.event_id
88 with session_scope() as session:
89 uses = get_upload_uses(session, "event_key")
90 assert len(uses) == 1
91 assert uses[0].use_type == UploadUseType.event
92 assert uses[0].is_current
93 assert uses[0].event_id == event_id
94 assert uses[0].url is not None
95 assert f"/event/{event_id}/" in uses[0].url
97 # a deleted occurrence still references the upload, but is no longer shown
98 with session_scope() as session:
99 occurrence = session.execute(
100 select(EventOccurrence).where(EventOccurrence.photo_key == "event_key")
101 ).scalar_one()
102 occurrence.is_deleted = True
104 with session_scope() as session:
105 uses = get_upload_uses(session, "event_key")
106 assert len(uses) == 1
107 assert uses[0].use_type == UploadUseType.event
108 assert not uses[0].is_current
111def test_get_upload_uses_page(db):
112 user, token = generate_user()
113 with session_scope() as session:
114 create_community(session, 0, 2, "Root node", [user], [], None)
116 key = random_hex(32)
117 _add_upload(user.id, key)
119 with pages_session(token) as api:
120 res = api.CreatePlace(
121 pages_pb2.CreatePlaceReq(
122 title="title",
123 content="content",
124 photo_key=key,
125 address="address",
126 location=pages_pb2.Coordinate(lat=1, lng=1),
127 )
128 )
129 page_id = res.page_id
131 with session_scope() as session:
132 uses = get_upload_uses(session, key)
133 assert len(uses) == 1
134 assert uses[0].use_type == UploadUseType.page
135 assert uses[0].is_current
136 assert uses[0].page_id == page_id
138 # clearing the photo creates a new version; the old version still references the upload
139 api.UpdatePage(pages_pb2.UpdatePageReq(page_id=page_id, photo_key=wrappers_pb2.StringValue(value="")))
141 with session_scope() as session:
142 uses = get_upload_uses(session, key)
143 assert len(uses) == 1
144 assert uses[0].use_type == UploadUseType.page
145 assert not uses[0].is_current
146 assert uses[0].page_id == page_id
149def test_get_upload_uses_community_page(db):
150 user, _ = generate_user()
151 with session_scope() as session:
152 node_id = create_community(session, 0, 2, "Community", [user], [], None).id
154 key = random_hex(32)
155 _add_upload(user.id, key)
157 with session_scope() as session:
158 main_page = session.execute(
159 select(Page).where(Page.type == PageType.main_page).where(Page.parent_node_id == node_id)
160 ).scalar_one()
161 main_page.versions[-1].photo_key = key
163 with session_scope() as session:
164 uses = get_upload_uses(session, key)
165 assert len(uses) == 1
166 assert uses[0].use_type == UploadUseType.page
167 assert uses[0].is_current
168 assert uses[0].url is not None
169 assert f"/community/{node_id}/" in uses[0].url
172def test_get_upload_uses_multiple(db):
173 """An upload can be used in several places at once; all are returned."""
174 user, token = generate_user()
175 with session_scope() as session:
176 create_community(session, 0, 2, "Root node", [user], [], None)
178 key = random_hex(32)
179 _add_upload(user.id, key)
181 with session_scope() as session:
182 gallery = PhotoGallery(owner_user_id=user.id)
183 session.add(gallery)
184 session.flush()
185 session.add(PhotoGalleryItem(gallery_id=gallery.id, upload_key=key, position=1.0))
187 with pages_session(token) as api:
188 api.CreatePlace(
189 pages_pb2.CreatePlaceReq(
190 title="title",
191 content="content",
192 photo_key=key,
193 address="address",
194 location=pages_pb2.Coordinate(lat=1, lng=1),
195 )
196 )
198 with session_scope() as session:
199 uses = get_upload_uses(session, key)
200 assert {use.use_type for use in uses} == {
201 UploadUseType.profile_gallery_photo_avatar,
202 UploadUseType.page,
203 }
206def test_get_upload_uses_for_keys_batch(db):
207 user, _ = generate_user()
208 _add_upload(user.id, "gallery_key")
209 _add_upload(user.id, "unused_key")
211 with session_scope() as session:
212 gallery = PhotoGallery(owner_user_id=user.id)
213 session.add(gallery)
214 session.flush()
215 session.add(PhotoGalleryItem(gallery_id=gallery.id, upload_key="gallery_key", position=1.0))
217 with session_scope() as session:
218 result = get_upload_uses_for_keys(session, ["gallery_key", "unused_key", "nonexistent_key"])
220 # only keys with uses appear in the mapping
221 assert set(result.keys()) == {"gallery_key"}
222 assert len(result["gallery_key"]) == 1
223 assert result["gallery_key"][0].use_type == UploadUseType.profile_gallery_photo_avatar
224 assert result["gallery_key"][0].user_id == user.id
227def test_get_upload_uses_for_keys_empty(db):
228 with session_scope() as session:
229 assert get_upload_uses_for_keys(session, []) == {}
232def test_upload_uses_covers_all_foreign_keys(db):
233 """
234 Guards against drift: every foreign key targeting uploads.key must be handled by get_upload_uses.
236 If this fails, you added a new reference to uploads.key. Add it to get_upload_uses (and likely a new
237 UploadUseType), then add the (table, column) here.
238 """
239 referencing = set()
240 for table in Base.metadata.tables.values():
241 for fk in table.foreign_keys:
242 if fk.column.table.name == "uploads" and fk.column.name == "key":
243 referencing.add((table.name, fk.parent.name))
245 assert referencing == {
246 ("photo_gallery_items", "upload_key"),
247 ("event_occurrences", "photo_key"),
248 ("page_versions", "photo_key"),
249 }