Coverage for app / backend / src / tests / test_galleries.py: 100%
499 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +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
14@pytest.fixture(autouse=True)
15def _(testconfig):
16 pass
19def create_upload(session, user_id, filename="test.jpg"):
20 """Helper to create an upload for testing"""
21 upload = Upload(
22 key=f"test_key_{filename}_{user_id}",
23 filename=filename,
24 creator_user_id=user_id,
25 )
26 session.add(upload)
27 session.commit()
28 return upload.key
31def test_user_has_profile_gallery(db):
32 """Each user should have a profile gallery created automatically"""
33 user1, token1 = generate_user(complete_profile=False)
35 with session_scope() as session:
36 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
37 assert user.profile_gallery_id is not None
39 gallery = session.execute(select(PhotoGallery).where(PhotoGallery.id == user.profile_gallery_id)).scalar_one()
40 assert gallery.owner_user_id == user1.id
43def test_GetGalleryEditInfo(db):
44 user1, token1 = generate_user(complete_profile=False)
46 with galleries_session(token1) as api:
47 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
48 assert res.gallery_id == user1.profile_gallery_id
49 assert res.max_photos == 1
50 assert res.current_photo_count == 0
53def test_GetGalleryEditInfo_verified_user(db):
54 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
56 with galleries_session(token1) as api:
57 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
58 assert res.gallery_id == user1.profile_gallery_id
59 assert res.max_photos == 4
60 assert res.current_photo_count == 0
63def test_GetGalleryEditInfo_not_owner(db):
64 user1, token1 = generate_user(complete_profile=False)
65 user2, token2 = generate_user(complete_profile=False)
67 with galleries_session(token2) as api:
68 with pytest.raises(grpc.RpcError) as e:
69 api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
70 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
71 assert e.value.details() == "You do not have permission to edit this gallery."
74def test_GetGalleryEditInfo_not_found(db):
75 user1, token1 = generate_user(complete_profile=False)
77 with galleries_session(token1) as api:
78 with pytest.raises(grpc.RpcError) as e:
79 api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=999999))
80 assert e.value.code() == grpc.StatusCode.NOT_FOUND
81 assert e.value.details() == "Gallery not found."
84def test_GetGalleryEditInfo_with_photos(db):
85 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
87 with session_scope() as session:
88 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
90 with galleries_session(token1) as api:
91 for key in keys:
92 api.AddPhotoToGallery(
93 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
94 )
96 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
97 assert res.max_photos == 4
98 assert res.current_photo_count == 3
101def test_GetGallery_as_owner(db):
102 user1, token1 = generate_user(complete_profile=False)
104 with galleries_session(token1) as api:
105 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
106 assert res.gallery_id == user1.profile_gallery_id
107 assert res.can_edit is True
108 assert len(res.photos) == 0
111def test_GetGallery_as_non_owner(db):
112 user1, token1 = generate_user(complete_profile=False)
113 user2, token2 = generate_user(complete_profile=False)
115 with galleries_session(token2) as api:
116 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
117 assert res.gallery_id == user1.profile_gallery_id
118 assert res.can_edit is False
119 assert len(res.photos) == 0
122def test_GetGallery_not_found(db):
123 user1, token1 = generate_user(complete_profile=False)
125 with galleries_session(token1) as api:
126 with pytest.raises(grpc.RpcError) as e:
127 api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=999999))
128 assert e.value.code() == grpc.StatusCode.NOT_FOUND
129 assert e.value.details() == "Gallery not found."
132def test_AddPhotoToGallery_success(db):
133 user1, token1 = generate_user(complete_profile=False)
135 with session_scope() as session:
136 upload_key = create_upload(session, user1.id)
138 with galleries_session(token1) as api:
139 res = api.AddPhotoToGallery(
140 galleries_pb2.AddPhotoToGalleryReq(
141 gallery_id=user1.profile_gallery_id,
142 upload_key=upload_key,
143 )
144 )
146 assert len(res.photos) == 1
147 photo = res.photos[0]
148 assert photo.full_url
149 assert photo.thumbnail_url
150 assert photo.caption == ""
153def test_AddPhotoToGallery_with_caption(db):
154 user1, token1 = generate_user(complete_profile=False)
156 with session_scope() as session:
157 upload_key = create_upload(session, user1.id)
159 with galleries_session(token1) as api:
160 res = api.AddPhotoToGallery(
161 galleries_pb2.AddPhotoToGalleryReq(
162 gallery_id=user1.profile_gallery_id,
163 upload_key=upload_key,
164 caption="Test caption",
165 )
166 )
168 assert len(res.photos) == 1
169 assert res.photos[0].caption == "Test caption"
172def test_AddPhotoToGallery_multiple_photos(db):
173 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
175 with session_scope() as session:
176 key1 = create_upload(session, user1.id, "photo1.jpg")
177 key2 = create_upload(session, user1.id, "photo2.jpg")
178 key3 = create_upload(session, user1.id, "photo3.jpg")
180 with galleries_session(token1) as api:
181 res = api.AddPhotoToGallery(
182 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key1)
183 )
184 assert len(res.photos) == 1
186 res = api.AddPhotoToGallery(
187 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key2)
188 )
189 assert len(res.photos) == 2
191 res = api.AddPhotoToGallery(
192 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key3)
193 )
194 assert len(res.photos) == 3
197def test_AddPhotoToGallery_not_owner(db):
198 user1, token1 = generate_user(complete_profile=False)
199 user2, token2 = generate_user(complete_profile=False)
201 with session_scope() as session:
202 upload_key = create_upload(session, user2.id)
204 with galleries_session(token2) as api:
205 with pytest.raises(grpc.RpcError) as e:
206 api.AddPhotoToGallery(
207 galleries_pb2.AddPhotoToGalleryReq(
208 gallery_id=user1.profile_gallery_id,
209 upload_key=upload_key,
210 )
211 )
212 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
213 assert e.value.details() == "You do not have permission to edit this gallery."
216def test_AddPhotoToGallery_upload_not_owned(db):
217 user1, token1 = generate_user(complete_profile=False)
218 user2, token2 = generate_user(complete_profile=False)
220 with session_scope() as session:
221 upload_key = create_upload(session, user2.id)
223 with galleries_session(token1) as api:
224 with pytest.raises(grpc.RpcError) as e:
225 api.AddPhotoToGallery(
226 galleries_pb2.AddPhotoToGalleryReq(
227 gallery_id=user1.profile_gallery_id,
228 upload_key=upload_key,
229 )
230 )
231 assert e.value.code() == grpc.StatusCode.NOT_FOUND
232 assert e.value.details() == "Upload not found or you don't own it."
235def test_AddPhotoToGallery_max_capacity(db):
236 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
238 with session_scope() as session:
239 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(5)]
241 with galleries_session(token1) as api:
242 for i in range(4):
243 api.AddPhotoToGallery(
244 galleries_pb2.AddPhotoToGalleryReq(
245 gallery_id=user1.profile_gallery_id,
246 upload_key=keys[i],
247 )
248 )
250 with pytest.raises(grpc.RpcError) as e:
251 api.AddPhotoToGallery(
252 galleries_pb2.AddPhotoToGalleryReq(
253 gallery_id=user1.profile_gallery_id,
254 upload_key=keys[4],
255 )
256 )
257 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
258 assert e.value.details() == "The gallery is at maximum capacity and cannot accept more photos."
261def test_AddPhotoToGallery_duplicate_photo(db):
262 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
264 with session_scope() as session:
265 upload_key = create_upload(session, user1.id)
267 with galleries_session(token1) as api:
268 api.AddPhotoToGallery(
269 galleries_pb2.AddPhotoToGalleryReq(
270 gallery_id=user1.profile_gallery_id,
271 upload_key=upload_key,
272 )
273 )
275 with pytest.raises(grpc.RpcError) as e:
276 api.AddPhotoToGallery(
277 galleries_pb2.AddPhotoToGalleryReq(
278 gallery_id=user1.profile_gallery_id,
279 upload_key=upload_key,
280 )
281 )
282 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
283 assert e.value.details() == "This photo is already in the gallery."
286def test_AddPhotoToGallery_gallery_not_found(db):
287 user1, token1 = generate_user(complete_profile=False)
289 with session_scope() as session:
290 upload_key = create_upload(session, user1.id)
292 with galleries_session(token1) as api:
293 with pytest.raises(grpc.RpcError) as e:
294 api.AddPhotoToGallery(
295 galleries_pb2.AddPhotoToGalleryReq(
296 gallery_id=999999,
297 upload_key=upload_key,
298 )
299 )
300 assert e.value.code() == grpc.StatusCode.NOT_FOUND
301 assert e.value.details() == "Gallery not found."
304def test_RemovePhotoFromGallery_success(db):
305 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
307 with session_scope() as session:
308 key1 = create_upload(session, user1.id, "photo1.jpg")
309 key2 = create_upload(session, user1.id, "photo2.jpg")
310 key3 = create_upload(session, user1.id, "photo3.jpg")
312 with galleries_session(token1) as api:
313 api.AddPhotoToGallery(galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key1))
314 api.AddPhotoToGallery(galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key2))
315 res = api.AddPhotoToGallery(
316 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key3)
317 )
319 item_id = res.photos[1].item_id
321 res = api.RemovePhotoFromGallery(
322 galleries_pb2.RemovePhotoFromGalleryReq(
323 gallery_id=user1.profile_gallery_id,
324 item_id=item_id,
325 )
326 )
328 assert len(res.photos) == 2
331def test_RemovePhotoFromGallery_not_owner(db):
332 user1, token1 = generate_user(complete_profile=False)
333 user2, token2 = generate_user(complete_profile=False)
335 with session_scope() as session:
336 upload_key = create_upload(session, user1.id)
338 with galleries_session(token1) as api:
339 res = api.AddPhotoToGallery(
340 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
341 )
342 item_id = res.photos[0].item_id
344 with galleries_session(token2) as api:
345 with pytest.raises(grpc.RpcError) as e:
346 api.RemovePhotoFromGallery(
347 galleries_pb2.RemovePhotoFromGalleryReq(
348 gallery_id=user1.profile_gallery_id,
349 item_id=item_id,
350 )
351 )
352 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
353 assert e.value.details() == "You do not have permission to edit this gallery."
356def test_RemovePhotoFromGallery_item_not_found(db):
357 user1, token1 = generate_user(complete_profile=False)
359 with galleries_session(token1) as api:
360 with pytest.raises(grpc.RpcError) as e:
361 api.RemovePhotoFromGallery(
362 galleries_pb2.RemovePhotoFromGalleryReq(
363 gallery_id=user1.profile_gallery_id,
364 item_id=999999,
365 )
366 )
367 assert e.value.code() == grpc.StatusCode.NOT_FOUND
368 assert e.value.details() == "Gallery item not found."
371def test_MovePhoto_to_first(db):
372 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
374 with session_scope() as session:
375 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
377 with galleries_session(token1) as api:
378 for key in keys:
379 api.AddPhotoToGallery(
380 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
381 )
383 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
384 item_ids = [photo.item_id for photo in res.photos]
386 # Move last photo to first position
387 res = api.MovePhoto(
388 galleries_pb2.MovePhotoReq(
389 gallery_id=user1.profile_gallery_id,
390 item_id=item_ids[2],
391 after_item_id=0, # 0 means first position
392 )
393 )
395 # Last photo should now be first
396 assert res.photos[0].item_id == item_ids[2]
397 assert res.photos[1].item_id == item_ids[0]
398 assert res.photos[2].item_id == item_ids[1]
401def test_MovePhoto_to_middle(db):
402 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
404 with session_scope() as session:
405 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
407 with galleries_session(token1) as api:
408 for key in keys:
409 api.AddPhotoToGallery(
410 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
411 )
413 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
414 item_ids = [photo.item_id for photo in res.photos]
416 # Move first photo after second (to middle)
417 res = api.MovePhoto(
418 galleries_pb2.MovePhotoReq(
419 gallery_id=user1.profile_gallery_id,
420 item_id=item_ids[0],
421 after_item_id=item_ids[1],
422 )
423 )
425 # Order should be: [1, 0, 2]
426 assert res.photos[0].item_id == item_ids[1]
427 assert res.photos[1].item_id == item_ids[0]
428 assert res.photos[2].item_id == item_ids[2]
431def test_MovePhoto_to_end(db):
432 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
434 with session_scope() as session:
435 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
437 with galleries_session(token1) as api:
438 for key in keys:
439 api.AddPhotoToGallery(
440 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
441 )
443 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
444 item_ids = [photo.item_id for photo in res.photos]
446 # Move first photo to end (after last)
447 res = api.MovePhoto(
448 galleries_pb2.MovePhotoReq(
449 gallery_id=user1.profile_gallery_id,
450 item_id=item_ids[0],
451 after_item_id=item_ids[2],
452 )
453 )
455 # Order should be: [1, 2, 0]
456 assert res.photos[0].item_id == item_ids[1]
457 assert res.photos[1].item_id == item_ids[2]
458 assert res.photos[2].item_id == item_ids[0]
461def test_MovePhoto_noop(db):
462 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
464 with session_scope() as session:
465 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
467 with galleries_session(token1) as api:
468 for key in keys:
469 api.AddPhotoToGallery(
470 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
471 )
473 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
474 item_ids = [photo.item_id for photo in res.photos]
476 # Move photo after itself - should be a no-op
477 res = api.MovePhoto(
478 galleries_pb2.MovePhotoReq(
479 gallery_id=user1.profile_gallery_id,
480 item_id=item_ids[1],
481 after_item_id=item_ids[1],
482 )
483 )
485 # Order should be unchanged
486 assert res.photos[0].item_id == item_ids[0]
487 assert res.photos[1].item_id == item_ids[1]
488 assert res.photos[2].item_id == item_ids[2]
491def test_MovePhoto_not_owner(db):
492 user1, token1 = generate_user(complete_profile=False)
493 user2, token2 = generate_user(complete_profile=False)
495 with session_scope() as session:
496 upload_key = create_upload(session, user1.id)
498 with galleries_session(token1) as api:
499 res = api.AddPhotoToGallery(
500 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
501 )
502 item_id = res.photos[0].item_id
504 with galleries_session(token2) as api:
505 with pytest.raises(grpc.RpcError) as e:
506 api.MovePhoto(
507 galleries_pb2.MovePhotoReq(
508 gallery_id=user1.profile_gallery_id,
509 item_id=item_id,
510 after_item_id=0,
511 )
512 )
513 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
514 assert e.value.details() == "You do not have permission to edit this gallery."
517def test_MovePhoto_item_not_found(db):
518 user1, token1 = generate_user(complete_profile=False)
520 with galleries_session(token1) as api:
521 with pytest.raises(grpc.RpcError) as e:
522 api.MovePhoto(
523 galleries_pb2.MovePhotoReq(
524 gallery_id=user1.profile_gallery_id,
525 item_id=999999,
526 after_item_id=0,
527 )
528 )
529 assert e.value.code() == grpc.StatusCode.NOT_FOUND
530 assert e.value.details() == "Gallery item not found."
533def test_MovePhoto_after_item_not_found(db):
534 user1, token1 = generate_user(complete_profile=False)
536 with session_scope() as session:
537 upload_key = create_upload(session, user1.id)
539 with galleries_session(token1) as api:
540 res = api.AddPhotoToGallery(
541 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
542 )
543 item_id = res.photos[0].item_id
545 with pytest.raises(grpc.RpcError) as e:
546 api.MovePhoto(
547 galleries_pb2.MovePhotoReq(
548 gallery_id=user1.profile_gallery_id,
549 item_id=item_id,
550 after_item_id=999999,
551 )
552 )
553 assert e.value.code() == grpc.StatusCode.NOT_FOUND
554 assert e.value.details() == "The item to place after was not found."
557def test_UpdatePhotoCaption_success(db):
558 user1, token1 = generate_user(complete_profile=False)
560 with session_scope() as session:
561 upload_key = create_upload(session, user1.id)
563 with galleries_session(token1) as api:
564 res = api.AddPhotoToGallery(
565 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
566 )
567 item_id = res.photos[0].item_id
569 res = api.UpdatePhotoCaption(
570 galleries_pb2.UpdatePhotoCaptionReq(
571 gallery_id=user1.profile_gallery_id,
572 item_id=item_id,
573 caption="New caption",
574 )
575 )
577 assert len(res.photos) == 1
578 assert res.photos[0].caption == "New caption"
581def test_UpdatePhotoCaption_clear_caption(db):
582 user1, token1 = generate_user(complete_profile=False)
584 with session_scope() as session:
585 upload_key = create_upload(session, user1.id)
587 with galleries_session(token1) as api:
588 res = api.AddPhotoToGallery(
589 galleries_pb2.AddPhotoToGalleryReq(
590 gallery_id=user1.profile_gallery_id,
591 upload_key=upload_key,
592 caption="Initial caption",
593 )
594 )
595 item_id = res.photos[0].item_id
597 res = api.UpdatePhotoCaption(
598 galleries_pb2.UpdatePhotoCaptionReq(
599 gallery_id=user1.profile_gallery_id,
600 item_id=item_id,
601 caption="",
602 )
603 )
605 assert len(res.photos) == 1
606 assert res.photos[0].caption == ""
609def test_UpdatePhotoCaption_not_owner(db):
610 user1, token1 = generate_user(complete_profile=False)
611 user2, token2 = generate_user(complete_profile=False)
613 with session_scope() as session:
614 upload_key = create_upload(session, user1.id)
616 with galleries_session(token1) as api:
617 res = api.AddPhotoToGallery(
618 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
619 )
620 item_id = res.photos[0].item_id
622 with galleries_session(token2) as api:
623 with pytest.raises(grpc.RpcError) as e:
624 api.UpdatePhotoCaption(
625 galleries_pb2.UpdatePhotoCaptionReq(
626 gallery_id=user1.profile_gallery_id,
627 item_id=item_id,
628 caption="Hacked!",
629 )
630 )
631 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
632 assert e.value.details() == "You do not have permission to edit this gallery."
635def test_UpdatePhotoCaption_item_not_found(db):
636 user1, token1 = generate_user(complete_profile=False)
638 with galleries_session(token1) as api:
639 with pytest.raises(grpc.RpcError) as e:
640 api.UpdatePhotoCaption(
641 galleries_pb2.UpdatePhotoCaptionReq(
642 gallery_id=user1.profile_gallery_id,
643 item_id=999999,
644 caption="Test",
645 )
646 )
647 assert e.value.code() == grpc.StatusCode.NOT_FOUND
648 assert e.value.details() == "Gallery item not found."
651def test_remove_and_readd_photo(db):
652 user1, token1 = generate_user(complete_profile=False)
654 with session_scope() as session:
655 upload_key = create_upload(session, user1.id)
657 with galleries_session(token1) as api:
658 res = api.AddPhotoToGallery(
659 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
660 )
661 item_id = res.photos[0].item_id
663 res = api.RemovePhotoFromGallery(
664 galleries_pb2.RemovePhotoFromGalleryReq(gallery_id=user1.profile_gallery_id, item_id=item_id)
665 )
666 assert len(res.photos) == 0
668 res = api.AddPhotoToGallery(
669 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
670 )
671 assert len(res.photos) == 1
674def test_gallery_photo_ordering_preserved(db):
675 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
677 with session_scope() as session:
678 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(4)]
680 with galleries_session(token1) as api:
681 item_ids = []
682 for key in keys:
683 res = api.AddPhotoToGallery(
684 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
685 )
686 item_ids.append(res.photos[-1].item_id)
688 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
689 assert len(res.photos) == 4
690 for i, photo in enumerate(res.photos):
691 assert photo.item_id == item_ids[i]
694def test_database_constraints_upload_uniqueness(db):
695 user1, token1 = generate_user(complete_profile=False)
697 with session_scope() as session:
698 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
700 upload = Upload(key="key1", filename="test.jpg", creator_user_id=user.id)
701 session.add(upload)
702 session.flush()
704 gallery_id = user.profile_gallery_id
705 assert gallery_id
707 item1 = PhotoGalleryItem(gallery_id=gallery_id, upload_key="key1", position=0.0)
708 item2 = PhotoGalleryItem(gallery_id=gallery_id, upload_key="key1", position=1.0)
709 session.add_all([item1, item2])
711 with pytest.raises(IntegrityError):
712 session.flush()
714 session.rollback()
717# Avatar photo selection tests
720def test_get_avatar_upload_returns_first_by_position(db):
721 """get_avatar_upload should return the upload with the lowest position value"""
722 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
724 with session_scope() as session:
725 # Create uploads with specific filenames so we can identify them
726 keys = []
727 for i, filename in enumerate(["first.jpg", "second.jpg", "third.jpg"]):
728 key = f"key_{filename}_{user1.id}"
729 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
730 session.add(upload)
731 keys.append(key)
732 session.commit()
734 # Add photos in reverse position order (third has lowest position)
735 with session_scope() as session:
736 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
737 gallery_id = user.profile_gallery_id
738 assert gallery_id is not None
740 # Add with positions: third=0.5, first=1.0, second=2.0
741 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=0.5))
742 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=1.0))
743 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=2.0))
744 session.commit()
746 with session_scope() as session:
747 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
748 avatar = get_avatar_upload(session, user)
750 assert avatar is not None
751 assert avatar.filename == "third.jpg"
754def test_get_avatar_upload_no_photos(db):
755 """get_avatar_upload should return None when user has no photos"""
756 user1, token1 = generate_user(complete_profile=False)
758 with session_scope() as session:
759 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
760 avatar = get_avatar_upload(session, user)
762 assert avatar is None
765def test_has_avatar_photo_expression_with_photos(db):
766 """has_avatar_photo_expression should return True when user has photos"""
767 user1, token1 = generate_user(complete_profile=False)
769 with session_scope() as session:
770 upload_key = create_upload(session, user1.id)
772 with galleries_session(token1) as api:
773 api.AddPhotoToGallery(
774 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
775 )
777 with session_scope() as session:
778 # Test with User class (SQL expression)
779 result = session.execute(
780 select(User.id).where(User.id == user1.id).where(has_avatar_photo_expression(User))
781 ).scalar_one_or_none()
782 assert result == user1.id
784 # Test with User instance
785 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
786 has_photo = session.execute(select(has_avatar_photo_expression(user))).scalar()
787 assert has_photo is True
790def test_has_avatar_photo_expression_no_photos(db):
791 """has_avatar_photo_expression should return False when user has no photos"""
792 user1, token1 = generate_user(complete_profile=False)
794 with session_scope() as session:
795 # Test with User class (SQL expression) - should not match
796 result = session.execute(
797 select(User.id).where(User.id == user1.id).where(has_avatar_photo_expression(User))
798 ).scalar_one_or_none()
799 assert result is None
801 # Test with User instance
802 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
803 has_photo = session.execute(select(has_avatar_photo_expression(user))).scalar()
804 assert has_photo is False
807def test_avatar_url_via_api_reflects_first_photo(db):
808 """GetUser should return avatar URL matching the first photo by position"""
809 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
810 user2, token2 = generate_user()
812 with session_scope() as session:
813 keys = []
814 for i, filename in enumerate(["avatar1.jpg", "avatar2.jpg", "avatar3.jpg"]):
815 key = f"key_{filename}_{user1.id}"
816 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
817 session.add(upload)
818 keys.append(key)
819 session.commit()
821 # Add photos: avatar2 has lowest position, so it should be the avatar
822 with session_scope() as session:
823 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
824 gallery_id = user.profile_gallery_id
825 assert gallery_id is not None
827 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.5)) # avatar2
828 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=1.0)) # avatar1
829 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=2.0)) # avatar3
830 session.commit()
832 with api_session(token2) as api:
833 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
835 assert "avatar2.jpg" in user_pb.avatar_url
836 assert "avatar2.jpg" in user_pb.avatar_thumbnail_url
839def test_avatar_changes_after_reordering(db):
840 """Moving a photo to first position should make it the new avatar"""
841 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
842 user2, token2 = generate_user()
844 with session_scope() as session:
845 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
847 with galleries_session(token1) as api:
848 for key in keys:
849 api.AddPhotoToGallery(
850 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
851 )
853 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
854 item_ids = [photo.item_id for photo in res.photos]
856 # Check initial avatar (photo0)
857 with api_session(token2) as api:
858 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
859 assert "photo0.jpg" in user_pb.avatar_url
861 # Move photo2 to first position
862 with galleries_session(token1) as api:
863 api.MovePhoto(
864 galleries_pb2.MovePhotoReq(
865 gallery_id=user1.profile_gallery_id,
866 item_id=item_ids[2],
867 after_item_id=0, # 0 means first position
868 )
869 )
871 # Check avatar is now photo2
872 with api_session(token2) as api:
873 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
874 assert "photo2.jpg" in user_pb.avatar_url
877def test_avatar_with_negative_positions(db):
878 """Avatar selection should work correctly with negative position values"""
879 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
881 with session_scope() as session:
882 keys = []
883 for filename in ["neg.jpg", "zero.jpg", "pos.jpg"]:
884 key = f"key_{filename}_{user1.id}"
885 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
886 session.add(upload)
887 keys.append(key)
888 session.commit()
890 with session_scope() as session:
891 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
892 gallery_id = user.profile_gallery_id
893 assert gallery_id is not None
895 # neg.jpg has the lowest position
896 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=-5.0))
897 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.0))
898 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=5.0))
899 session.commit()
901 with session_scope() as session:
902 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
903 avatar = get_avatar_upload(session, user)
905 assert avatar is not None
906 assert avatar.filename == "neg.jpg"
909def test_avatar_with_fractional_positions(db):
910 """Avatar selection should work correctly with fractional position values"""
911 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
913 with session_scope() as session:
914 keys = []
915 for filename in ["a.jpg", "b.jpg", "c.jpg"]:
916 key = f"key_{filename}_{user1.id}"
917 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
918 session.add(upload)
919 keys.append(key)
920 session.commit()
922 with session_scope() as session:
923 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
924 gallery_id = user.profile_gallery_id
925 assert gallery_id is not None
927 # b.jpg has the lowest position (0.001)
928 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=0.5))
929 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.001))
930 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=0.999))
931 session.commit()
933 with session_scope() as session:
934 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
935 avatar = get_avatar_upload(session, user)
937 assert avatar is not None
938 assert avatar.filename == "b.jpg"