Coverage for app/backend/src/tests/test_galleries.py: 100%
496 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
1import grpc
2import pytest
3from sqlalchemy import select
4from sqlalchemy.exc import IntegrityError
6from couchers.db import session_scope
7from couchers.models import PhotoGallery, PhotoGalleryItem, Upload, User
8from couchers.models.uploads import get_avatar_upload, has_avatar_photo_expression
9from couchers.proto import api_pb2, galleries_pb2
10from tests.fixtures.db import generate_user
11from tests.fixtures.sessions import api_session, galleries_session
14def create_upload(session, user_id, filename="test.jpg"):
15 """Helper to create an upload for testing"""
16 upload = Upload(
17 key=f"test_key_{filename}_{user_id}",
18 filename=filename,
19 creator_user_id=user_id,
20 )
21 session.add(upload)
22 session.commit()
23 return upload.key
26def test_user_has_profile_gallery(db):
27 """Each user should have a profile gallery created automatically"""
28 user1, token1 = generate_user(complete_profile=False)
30 with session_scope() as session:
31 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
32 assert user.profile_gallery_id is not None
34 gallery = session.execute(select(PhotoGallery).where(PhotoGallery.id == user.profile_gallery_id)).scalar_one()
35 assert gallery.owner_user_id == user1.id
38def test_GetGalleryEditInfo(db):
39 user1, token1 = generate_user(complete_profile=False)
41 with galleries_session(token1) as api:
42 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
43 assert res.gallery_id == user1.profile_gallery_id
44 assert res.max_photos == 2
45 assert res.current_photo_count == 0
48def test_GetGalleryEditInfo_verified_user(db):
49 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
51 with galleries_session(token1) as api:
52 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
53 assert res.gallery_id == user1.profile_gallery_id
54 assert res.max_photos == 5
55 assert res.current_photo_count == 0
58def test_GetGalleryEditInfo_not_owner(db):
59 user1, token1 = generate_user(complete_profile=False)
60 user2, token2 = generate_user(complete_profile=False)
62 with galleries_session(token2) as api:
63 with pytest.raises(grpc.RpcError) as e:
64 api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
65 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
66 assert e.value.details() == "You do not have permission to edit this gallery."
69def test_GetGalleryEditInfo_not_found(db):
70 user1, token1 = generate_user(complete_profile=False)
72 with galleries_session(token1) as api:
73 with pytest.raises(grpc.RpcError) as e:
74 api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=999999))
75 assert e.value.code() == grpc.StatusCode.NOT_FOUND
76 assert e.value.details() == "Gallery not found."
79def test_GetGalleryEditInfo_with_photos(db):
80 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
82 with session_scope() as session:
83 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
85 with galleries_session(token1) as api:
86 for key in keys:
87 api.AddPhotoToGallery(
88 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
89 )
91 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
92 assert res.max_photos == 5
93 assert res.current_photo_count == 3
96def test_GetGallery_as_owner(db):
97 user1, token1 = generate_user(complete_profile=False)
99 with galleries_session(token1) as api:
100 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
101 assert res.gallery_id == user1.profile_gallery_id
102 assert res.can_edit is True
103 assert len(res.photos) == 0
106def test_GetGallery_as_non_owner(db):
107 user1, token1 = generate_user(complete_profile=False)
108 user2, token2 = generate_user(complete_profile=False)
110 with galleries_session(token2) as api:
111 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
112 assert res.gallery_id == user1.profile_gallery_id
113 assert res.can_edit is False
114 assert len(res.photos) == 0
117def test_GetGallery_not_found(db):
118 user1, token1 = generate_user(complete_profile=False)
120 with galleries_session(token1) as api:
121 with pytest.raises(grpc.RpcError) as e:
122 api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=999999))
123 assert e.value.code() == grpc.StatusCode.NOT_FOUND
124 assert e.value.details() == "Gallery not found."
127def test_AddPhotoToGallery_success(db):
128 user1, token1 = generate_user(complete_profile=False)
130 with session_scope() as session:
131 upload_key = create_upload(session, user1.id)
133 with galleries_session(token1) as api:
134 res = api.AddPhotoToGallery(
135 galleries_pb2.AddPhotoToGalleryReq(
136 gallery_id=user1.profile_gallery_id,
137 upload_key=upload_key,
138 )
139 )
141 assert len(res.photos) == 1
142 photo = res.photos[0]
143 assert photo.full_url
144 assert photo.thumbnail_url
145 assert photo.caption == ""
148def test_AddPhotoToGallery_with_caption(db):
149 user1, token1 = generate_user(complete_profile=False)
151 with session_scope() as session:
152 upload_key = create_upload(session, user1.id)
154 with galleries_session(token1) as api:
155 res = api.AddPhotoToGallery(
156 galleries_pb2.AddPhotoToGalleryReq(
157 gallery_id=user1.profile_gallery_id,
158 upload_key=upload_key,
159 caption="Test caption",
160 )
161 )
163 assert len(res.photos) == 1
164 assert res.photos[0].caption == "Test caption"
167def test_AddPhotoToGallery_multiple_photos(db):
168 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
170 with session_scope() as session:
171 key1 = create_upload(session, user1.id, "photo1.jpg")
172 key2 = create_upload(session, user1.id, "photo2.jpg")
173 key3 = create_upload(session, user1.id, "photo3.jpg")
175 with galleries_session(token1) as api:
176 res = api.AddPhotoToGallery(
177 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key1)
178 )
179 assert len(res.photos) == 1
181 res = api.AddPhotoToGallery(
182 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key2)
183 )
184 assert len(res.photos) == 2
186 res = api.AddPhotoToGallery(
187 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key3)
188 )
189 assert len(res.photos) == 3
192def test_AddPhotoToGallery_not_owner(db):
193 user1, token1 = generate_user(complete_profile=False)
194 user2, token2 = generate_user(complete_profile=False)
196 with session_scope() as session:
197 upload_key = create_upload(session, user2.id)
199 with galleries_session(token2) as api:
200 with pytest.raises(grpc.RpcError) as e:
201 api.AddPhotoToGallery(
202 galleries_pb2.AddPhotoToGalleryReq(
203 gallery_id=user1.profile_gallery_id,
204 upload_key=upload_key,
205 )
206 )
207 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
208 assert e.value.details() == "You do not have permission to edit this gallery."
211def test_AddPhotoToGallery_upload_not_owned(db):
212 user1, token1 = generate_user(complete_profile=False)
213 user2, token2 = generate_user(complete_profile=False)
215 with session_scope() as session:
216 upload_key = create_upload(session, user2.id)
218 with galleries_session(token1) as api:
219 with pytest.raises(grpc.RpcError) as e:
220 api.AddPhotoToGallery(
221 galleries_pb2.AddPhotoToGalleryReq(
222 gallery_id=user1.profile_gallery_id,
223 upload_key=upload_key,
224 )
225 )
226 assert e.value.code() == grpc.StatusCode.NOT_FOUND
227 assert e.value.details() == "Upload not found or you don't own it."
230def test_AddPhotoToGallery_max_capacity(db):
231 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
233 with session_scope() as session:
234 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(6)]
236 with galleries_session(token1) as api:
237 for i in range(5):
238 api.AddPhotoToGallery(
239 galleries_pb2.AddPhotoToGalleryReq(
240 gallery_id=user1.profile_gallery_id,
241 upload_key=keys[i],
242 )
243 )
245 with pytest.raises(grpc.RpcError) as e:
246 api.AddPhotoToGallery(
247 galleries_pb2.AddPhotoToGalleryReq(
248 gallery_id=user1.profile_gallery_id,
249 upload_key=keys[5],
250 )
251 )
252 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
253 assert e.value.details() == "The gallery is at maximum capacity and cannot accept more photos."
256def test_AddPhotoToGallery_duplicate_photo(db):
257 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
259 with session_scope() as session:
260 upload_key = create_upload(session, user1.id)
262 with galleries_session(token1) as api:
263 api.AddPhotoToGallery(
264 galleries_pb2.AddPhotoToGalleryReq(
265 gallery_id=user1.profile_gallery_id,
266 upload_key=upload_key,
267 )
268 )
270 with pytest.raises(grpc.RpcError) as e:
271 api.AddPhotoToGallery(
272 galleries_pb2.AddPhotoToGalleryReq(
273 gallery_id=user1.profile_gallery_id,
274 upload_key=upload_key,
275 )
276 )
277 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
278 assert e.value.details() == "This photo is already in the gallery."
281def test_AddPhotoToGallery_gallery_not_found(db):
282 user1, token1 = generate_user(complete_profile=False)
284 with session_scope() as session:
285 upload_key = create_upload(session, user1.id)
287 with galleries_session(token1) as api:
288 with pytest.raises(grpc.RpcError) as e:
289 api.AddPhotoToGallery(
290 galleries_pb2.AddPhotoToGalleryReq(
291 gallery_id=999999,
292 upload_key=upload_key,
293 )
294 )
295 assert e.value.code() == grpc.StatusCode.NOT_FOUND
296 assert e.value.details() == "Gallery not found."
299def test_RemovePhotoFromGallery_success(db):
300 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
302 with session_scope() as session:
303 key1 = create_upload(session, user1.id, "photo1.jpg")
304 key2 = create_upload(session, user1.id, "photo2.jpg")
305 key3 = create_upload(session, user1.id, "photo3.jpg")
307 with galleries_session(token1) as api:
308 api.AddPhotoToGallery(galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key1))
309 api.AddPhotoToGallery(galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key2))
310 res = api.AddPhotoToGallery(
311 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key3)
312 )
314 item_id = res.photos[1].item_id
316 res = api.RemovePhotoFromGallery(
317 galleries_pb2.RemovePhotoFromGalleryReq(
318 gallery_id=user1.profile_gallery_id,
319 item_id=item_id,
320 )
321 )
323 assert len(res.photos) == 2
326def test_RemovePhotoFromGallery_not_owner(db):
327 user1, token1 = generate_user(complete_profile=False)
328 user2, token2 = generate_user(complete_profile=False)
330 with session_scope() as session:
331 upload_key = create_upload(session, user1.id)
333 with galleries_session(token1) as api:
334 res = api.AddPhotoToGallery(
335 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
336 )
337 item_id = res.photos[0].item_id
339 with galleries_session(token2) as api:
340 with pytest.raises(grpc.RpcError) as e:
341 api.RemovePhotoFromGallery(
342 galleries_pb2.RemovePhotoFromGalleryReq(
343 gallery_id=user1.profile_gallery_id,
344 item_id=item_id,
345 )
346 )
347 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
348 assert e.value.details() == "You do not have permission to edit this gallery."
351def test_RemovePhotoFromGallery_item_not_found(db):
352 user1, token1 = generate_user(complete_profile=False)
354 with galleries_session(token1) as api:
355 with pytest.raises(grpc.RpcError) as e:
356 api.RemovePhotoFromGallery(
357 galleries_pb2.RemovePhotoFromGalleryReq(
358 gallery_id=user1.profile_gallery_id,
359 item_id=999999,
360 )
361 )
362 assert e.value.code() == grpc.StatusCode.NOT_FOUND
363 assert e.value.details() == "Gallery item not found."
366def test_MovePhoto_to_first(db):
367 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
369 with session_scope() as session:
370 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
372 with galleries_session(token1) as api:
373 for key in keys:
374 api.AddPhotoToGallery(
375 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
376 )
378 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
379 item_ids = [photo.item_id for photo in res.photos]
381 # Move last photo to first position
382 res = api.MovePhoto(
383 galleries_pb2.MovePhotoReq(
384 gallery_id=user1.profile_gallery_id,
385 item_id=item_ids[2],
386 after_item_id=0, # 0 means first position
387 )
388 )
390 # Last photo should now be first
391 assert res.photos[0].item_id == item_ids[2]
392 assert res.photos[1].item_id == item_ids[0]
393 assert res.photos[2].item_id == item_ids[1]
396def test_MovePhoto_to_middle(db):
397 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
399 with session_scope() as session:
400 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
402 with galleries_session(token1) as api:
403 for key in keys:
404 api.AddPhotoToGallery(
405 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
406 )
408 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
409 item_ids = [photo.item_id for photo in res.photos]
411 # Move first photo after second (to middle)
412 res = api.MovePhoto(
413 galleries_pb2.MovePhotoReq(
414 gallery_id=user1.profile_gallery_id,
415 item_id=item_ids[0],
416 after_item_id=item_ids[1],
417 )
418 )
420 # Order should be: [1, 0, 2]
421 assert res.photos[0].item_id == item_ids[1]
422 assert res.photos[1].item_id == item_ids[0]
423 assert res.photos[2].item_id == item_ids[2]
426def test_MovePhoto_to_end(db):
427 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
429 with session_scope() as session:
430 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
432 with galleries_session(token1) as api:
433 for key in keys:
434 api.AddPhotoToGallery(
435 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
436 )
438 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
439 item_ids = [photo.item_id for photo in res.photos]
441 # Move first photo to end (after last)
442 res = api.MovePhoto(
443 galleries_pb2.MovePhotoReq(
444 gallery_id=user1.profile_gallery_id,
445 item_id=item_ids[0],
446 after_item_id=item_ids[2],
447 )
448 )
450 # Order should be: [1, 2, 0]
451 assert res.photos[0].item_id == item_ids[1]
452 assert res.photos[1].item_id == item_ids[2]
453 assert res.photos[2].item_id == item_ids[0]
456def test_MovePhoto_noop(db):
457 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
459 with session_scope() as session:
460 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
462 with galleries_session(token1) as api:
463 for key in keys:
464 api.AddPhotoToGallery(
465 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
466 )
468 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
469 item_ids = [photo.item_id for photo in res.photos]
471 # Move photo after itself - should be a no-op
472 res = api.MovePhoto(
473 galleries_pb2.MovePhotoReq(
474 gallery_id=user1.profile_gallery_id,
475 item_id=item_ids[1],
476 after_item_id=item_ids[1],
477 )
478 )
480 # Order should be unchanged
481 assert res.photos[0].item_id == item_ids[0]
482 assert res.photos[1].item_id == item_ids[1]
483 assert res.photos[2].item_id == item_ids[2]
486def test_MovePhoto_not_owner(db):
487 user1, token1 = generate_user(complete_profile=False)
488 user2, token2 = generate_user(complete_profile=False)
490 with session_scope() as session:
491 upload_key = create_upload(session, user1.id)
493 with galleries_session(token1) as api:
494 res = api.AddPhotoToGallery(
495 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
496 )
497 item_id = res.photos[0].item_id
499 with galleries_session(token2) as api:
500 with pytest.raises(grpc.RpcError) as e:
501 api.MovePhoto(
502 galleries_pb2.MovePhotoReq(
503 gallery_id=user1.profile_gallery_id,
504 item_id=item_id,
505 after_item_id=0,
506 )
507 )
508 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
509 assert e.value.details() == "You do not have permission to edit this gallery."
512def test_MovePhoto_item_not_found(db):
513 user1, token1 = generate_user(complete_profile=False)
515 with galleries_session(token1) as api:
516 with pytest.raises(grpc.RpcError) as e:
517 api.MovePhoto(
518 galleries_pb2.MovePhotoReq(
519 gallery_id=user1.profile_gallery_id,
520 item_id=999999,
521 after_item_id=0,
522 )
523 )
524 assert e.value.code() == grpc.StatusCode.NOT_FOUND
525 assert e.value.details() == "Gallery item not found."
528def test_MovePhoto_after_item_not_found(db):
529 user1, token1 = generate_user(complete_profile=False)
531 with session_scope() as session:
532 upload_key = create_upload(session, user1.id)
534 with galleries_session(token1) as api:
535 res = api.AddPhotoToGallery(
536 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
537 )
538 item_id = res.photos[0].item_id
540 with pytest.raises(grpc.RpcError) as e:
541 api.MovePhoto(
542 galleries_pb2.MovePhotoReq(
543 gallery_id=user1.profile_gallery_id,
544 item_id=item_id,
545 after_item_id=999999,
546 )
547 )
548 assert e.value.code() == grpc.StatusCode.NOT_FOUND
549 assert e.value.details() == "The item to place after was not found."
552def test_UpdatePhotoCaption_success(db):
553 user1, token1 = generate_user(complete_profile=False)
555 with session_scope() as session:
556 upload_key = create_upload(session, user1.id)
558 with galleries_session(token1) as api:
559 res = api.AddPhotoToGallery(
560 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
561 )
562 item_id = res.photos[0].item_id
564 res = api.UpdatePhotoCaption(
565 galleries_pb2.UpdatePhotoCaptionReq(
566 gallery_id=user1.profile_gallery_id,
567 item_id=item_id,
568 caption="New caption",
569 )
570 )
572 assert len(res.photos) == 1
573 assert res.photos[0].caption == "New caption"
576def test_UpdatePhotoCaption_clear_caption(db):
577 user1, token1 = generate_user(complete_profile=False)
579 with session_scope() as session:
580 upload_key = create_upload(session, user1.id)
582 with galleries_session(token1) as api:
583 res = api.AddPhotoToGallery(
584 galleries_pb2.AddPhotoToGalleryReq(
585 gallery_id=user1.profile_gallery_id,
586 upload_key=upload_key,
587 caption="Initial caption",
588 )
589 )
590 item_id = res.photos[0].item_id
592 res = api.UpdatePhotoCaption(
593 galleries_pb2.UpdatePhotoCaptionReq(
594 gallery_id=user1.profile_gallery_id,
595 item_id=item_id,
596 caption="",
597 )
598 )
600 assert len(res.photos) == 1
601 assert res.photos[0].caption == ""
604def test_UpdatePhotoCaption_not_owner(db):
605 user1, token1 = generate_user(complete_profile=False)
606 user2, token2 = generate_user(complete_profile=False)
608 with session_scope() as session:
609 upload_key = create_upload(session, user1.id)
611 with galleries_session(token1) as api:
612 res = api.AddPhotoToGallery(
613 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
614 )
615 item_id = res.photos[0].item_id
617 with galleries_session(token2) as api:
618 with pytest.raises(grpc.RpcError) as e:
619 api.UpdatePhotoCaption(
620 galleries_pb2.UpdatePhotoCaptionReq(
621 gallery_id=user1.profile_gallery_id,
622 item_id=item_id,
623 caption="Hacked!",
624 )
625 )
626 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
627 assert e.value.details() == "You do not have permission to edit this gallery."
630def test_UpdatePhotoCaption_item_not_found(db):
631 user1, token1 = generate_user(complete_profile=False)
633 with galleries_session(token1) as api:
634 with pytest.raises(grpc.RpcError) as e:
635 api.UpdatePhotoCaption(
636 galleries_pb2.UpdatePhotoCaptionReq(
637 gallery_id=user1.profile_gallery_id,
638 item_id=999999,
639 caption="Test",
640 )
641 )
642 assert e.value.code() == grpc.StatusCode.NOT_FOUND
643 assert e.value.details() == "Gallery item not found."
646def test_remove_and_readd_photo(db):
647 user1, token1 = generate_user(complete_profile=False)
649 with session_scope() as session:
650 upload_key = create_upload(session, user1.id)
652 with galleries_session(token1) as api:
653 res = api.AddPhotoToGallery(
654 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
655 )
656 item_id = res.photos[0].item_id
658 res = api.RemovePhotoFromGallery(
659 galleries_pb2.RemovePhotoFromGalleryReq(gallery_id=user1.profile_gallery_id, item_id=item_id)
660 )
661 assert len(res.photos) == 0
663 res = api.AddPhotoToGallery(
664 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
665 )
666 assert len(res.photos) == 1
669def test_gallery_photo_ordering_preserved(db):
670 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
672 with session_scope() as session:
673 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(4)]
675 with galleries_session(token1) as api:
676 item_ids = []
677 for key in keys:
678 res = api.AddPhotoToGallery(
679 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
680 )
681 item_ids.append(res.photos[-1].item_id)
683 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
684 assert len(res.photos) == 4
685 for i, photo in enumerate(res.photos):
686 assert photo.item_id == item_ids[i]
689def test_database_constraints_upload_uniqueness(db):
690 user1, token1 = generate_user(complete_profile=False)
692 with session_scope() as session:
693 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
695 upload = Upload(key="key1", filename="test.jpg", creator_user_id=user.id)
696 session.add(upload)
697 session.flush()
699 gallery_id = user.profile_gallery_id
700 assert gallery_id
702 item1 = PhotoGalleryItem(gallery_id=gallery_id, upload_key="key1", position=0.0)
703 item2 = PhotoGalleryItem(gallery_id=gallery_id, upload_key="key1", position=1.0)
704 session.add_all([item1, item2])
706 with pytest.raises(IntegrityError):
707 session.flush()
709 session.rollback()
712# Avatar photo selection tests
715def test_get_avatar_upload_returns_first_by_position(db):
716 """get_avatar_upload should return the upload with the lowest position value"""
717 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
719 with session_scope() as session:
720 # Create uploads with specific filenames so we can identify them
721 keys = []
722 for i, filename in enumerate(["first.jpg", "second.jpg", "third.jpg"]):
723 key = f"key_{filename}_{user1.id}"
724 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
725 session.add(upload)
726 keys.append(key)
727 session.commit()
729 # Add photos in reverse position order (third has lowest position)
730 with session_scope() as session:
731 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
732 gallery_id = user.profile_gallery_id
733 assert gallery_id is not None
735 # Add with positions: third=0.5, first=1.0, second=2.0
736 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=0.5))
737 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=1.0))
738 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=2.0))
739 session.commit()
741 with session_scope() as session:
742 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
743 avatar = get_avatar_upload(session, user)
745 assert avatar is not None
746 assert avatar.filename == "third.jpg"
749def test_get_avatar_upload_no_photos(db):
750 """get_avatar_upload should return None when user has no photos"""
751 user1, token1 = generate_user(complete_profile=False)
753 with session_scope() as session:
754 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
755 avatar = get_avatar_upload(session, user)
757 assert avatar is None
760def test_has_avatar_photo_expression_with_photos(db):
761 """has_avatar_photo_expression should return True when user has photos"""
762 user1, token1 = generate_user(complete_profile=False)
764 with session_scope() as session:
765 upload_key = create_upload(session, user1.id)
767 with galleries_session(token1) as api:
768 api.AddPhotoToGallery(
769 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
770 )
772 with session_scope() as session:
773 # Test with User class (SQL expression)
774 result = session.execute(
775 select(User.id).where(User.id == user1.id).where(has_avatar_photo_expression(User))
776 ).scalar_one_or_none()
777 assert result == user1.id
779 # Test with User instance
780 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
781 has_photo = session.execute(select(has_avatar_photo_expression(user))).scalar()
782 assert has_photo is True
785def test_has_avatar_photo_expression_no_photos(db):
786 """has_avatar_photo_expression should return False when user has no photos"""
787 user1, token1 = generate_user(complete_profile=False)
789 with session_scope() as session:
790 # Test with User class (SQL expression) - should not match
791 result = session.execute(
792 select(User.id).where(User.id == user1.id).where(has_avatar_photo_expression(User))
793 ).scalar_one_or_none()
794 assert result is None
796 # Test with User instance
797 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
798 has_photo = session.execute(select(has_avatar_photo_expression(user))).scalar()
799 assert has_photo is False
802def test_avatar_url_via_api_reflects_first_photo(db):
803 """GetUser should return avatar URL matching the first photo by position"""
804 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
805 user2, token2 = generate_user()
807 with session_scope() as session:
808 keys = []
809 for i, filename in enumerate(["avatar1.jpg", "avatar2.jpg", "avatar3.jpg"]):
810 key = f"key_{filename}_{user1.id}"
811 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
812 session.add(upload)
813 keys.append(key)
814 session.commit()
816 # Add photos: avatar2 has lowest position, so it should be the avatar
817 with session_scope() as session:
818 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
819 gallery_id = user.profile_gallery_id
820 assert gallery_id is not None
822 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.5)) # avatar2
823 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=1.0)) # avatar1
824 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=2.0)) # avatar3
825 session.commit()
827 with api_session(token2) as api:
828 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
830 assert "avatar2.jpg" in user_pb.avatar_url
831 assert "avatar2.jpg" in user_pb.avatar_thumbnail_url
834def test_avatar_changes_after_reordering(db):
835 """Moving a photo to first position should make it the new avatar"""
836 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
837 user2, token2 = generate_user()
839 with session_scope() as session:
840 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
842 with galleries_session(token1) as api:
843 for key in keys:
844 api.AddPhotoToGallery(
845 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
846 )
848 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
849 item_ids = [photo.item_id for photo in res.photos]
851 # Check initial avatar (photo0)
852 with api_session(token2) as api:
853 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
854 assert "photo0.jpg" in user_pb.avatar_url
856 # Move photo2 to first position
857 with galleries_session(token1) as api:
858 api.MovePhoto(
859 galleries_pb2.MovePhotoReq(
860 gallery_id=user1.profile_gallery_id,
861 item_id=item_ids[2],
862 after_item_id=0, # 0 means first position
863 )
864 )
866 # Check avatar is now photo2
867 with api_session(token2) as api:
868 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
869 assert "photo2.jpg" in user_pb.avatar_url
872def test_avatar_with_negative_positions(db):
873 """Avatar selection should work correctly with negative position values"""
874 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
876 with session_scope() as session:
877 keys = []
878 for filename in ["neg.jpg", "zero.jpg", "pos.jpg"]:
879 key = f"key_{filename}_{user1.id}"
880 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
881 session.add(upload)
882 keys.append(key)
883 session.commit()
885 with session_scope() as session:
886 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
887 gallery_id = user.profile_gallery_id
888 assert gallery_id is not None
890 # neg.jpg has the lowest position
891 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=-5.0))
892 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.0))
893 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=5.0))
894 session.commit()
896 with session_scope() as session:
897 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
898 avatar = get_avatar_upload(session, user)
900 assert avatar is not None
901 assert avatar.filename == "neg.jpg"
904def test_avatar_with_fractional_positions(db):
905 """Avatar selection should work correctly with fractional position values"""
906 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
908 with session_scope() as session:
909 keys = []
910 for filename in ["a.jpg", "b.jpg", "c.jpg"]:
911 key = f"key_{filename}_{user1.id}"
912 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
913 session.add(upload)
914 keys.append(key)
915 session.commit()
917 with session_scope() as session:
918 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
919 gallery_id = user.profile_gallery_id
920 assert gallery_id is not None
922 # b.jpg has the lowest position (0.001)
923 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=0.5))
924 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.001))
925 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=0.999))
926 session.commit()
928 with session_scope() as session:
929 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
930 avatar = get_avatar_upload(session, user)
932 assert avatar is not None
933 assert avatar.filename == "b.jpg"