Coverage for app / backend / src / tests / test_galleries.py: 100%
508 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +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.proto import galleries_pb2
9from tests.fixtures.db import generate_user
10from tests.fixtures.sessions import galleries_session
13@pytest.fixture(autouse=True)
14def _(testconfig):
15 pass
18def create_upload(session, user_id, filename="test.jpg"):
19 """Helper to create an upload for testing"""
20 upload = Upload(
21 key=f"test_key_{filename}_{user_id}",
22 filename=filename,
23 creator_user_id=user_id,
24 )
25 session.add(upload)
26 session.commit()
27 return upload.key
30def test_user_has_profile_gallery(db):
31 """Each user should have a profile gallery created automatically"""
32 user1, token1 = generate_user(complete_profile=False)
34 with session_scope() as session:
35 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
36 assert user.profile_gallery_id is not None
38 gallery = session.execute(select(PhotoGallery).where(PhotoGallery.id == user.profile_gallery_id)).scalar_one()
39 assert gallery.owner_user_id == user1.id
42def test_GetGalleryEditInfo(db):
43 user1, token1 = generate_user(complete_profile=False)
45 with galleries_session(token1) as api:
46 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
47 assert res.gallery_id == user1.profile_gallery_id
48 assert res.max_photos == 1
49 assert res.current_photo_count == 0
52def test_GetGalleryEditInfo_verified_user(db):
53 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
55 with galleries_session(token1) as api:
56 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
57 assert res.gallery_id == user1.profile_gallery_id
58 assert res.max_photos == 4
59 assert res.current_photo_count == 0
62def test_GetGalleryEditInfo_not_owner(db):
63 user1, token1 = generate_user(complete_profile=False)
64 user2, token2 = generate_user(complete_profile=False)
66 with galleries_session(token2) as api:
67 with pytest.raises(grpc.RpcError) as e:
68 api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
69 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
70 assert e.value.details() == "You do not have permission to edit this gallery."
73def test_GetGalleryEditInfo_not_found(db):
74 user1, token1 = generate_user(complete_profile=False)
76 with galleries_session(token1) as api:
77 with pytest.raises(grpc.RpcError) as e:
78 api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=999999))
79 assert e.value.code() == grpc.StatusCode.NOT_FOUND
80 assert e.value.details() == "Gallery not found."
83def test_GetGalleryEditInfo_with_photos(db):
84 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
86 with session_scope() as session:
87 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
89 with galleries_session(token1) as api:
90 for key in keys:
91 api.AddPhotoToGallery(
92 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
93 )
95 res = api.GetGalleryEditInfo(galleries_pb2.GetGalleryEditInfoReq(gallery_id=user1.profile_gallery_id))
96 assert res.max_photos == 4
97 assert res.current_photo_count == 3
100def test_GetGallery_as_owner(db):
101 user1, token1 = generate_user(complete_profile=False)
103 with galleries_session(token1) as api:
104 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
105 assert res.gallery_id == user1.profile_gallery_id
106 assert res.can_edit is True
107 assert len(res.photos) == 0
110def test_GetGallery_as_non_owner(db):
111 user1, token1 = generate_user(complete_profile=False)
112 user2, token2 = generate_user(complete_profile=False)
114 with galleries_session(token2) as api:
115 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
116 assert res.gallery_id == user1.profile_gallery_id
117 assert res.can_edit is False
118 assert len(res.photos) == 0
121def test_GetGallery_not_found(db):
122 user1, token1 = generate_user(complete_profile=False)
124 with galleries_session(token1) as api:
125 with pytest.raises(grpc.RpcError) as e:
126 api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=999999))
127 assert e.value.code() == grpc.StatusCode.NOT_FOUND
128 assert e.value.details() == "Gallery not found."
131def test_AddPhotoToGallery_success(db):
132 user1, token1 = generate_user(complete_profile=False)
134 with session_scope() as session:
135 upload_key = create_upload(session, user1.id)
137 with galleries_session(token1) as api:
138 res = api.AddPhotoToGallery(
139 galleries_pb2.AddPhotoToGalleryReq(
140 gallery_id=user1.profile_gallery_id,
141 upload_key=upload_key,
142 )
143 )
145 assert len(res.photos) == 1
146 photo = res.photos[0]
147 assert photo.full_url
148 assert photo.thumbnail_url
149 assert photo.caption == ""
152def test_AddPhotoToGallery_with_caption(db):
153 user1, token1 = generate_user(complete_profile=False)
155 with session_scope() as session:
156 upload_key = create_upload(session, user1.id)
158 with galleries_session(token1) as api:
159 res = api.AddPhotoToGallery(
160 galleries_pb2.AddPhotoToGalleryReq(
161 gallery_id=user1.profile_gallery_id,
162 upload_key=upload_key,
163 caption="Test caption",
164 )
165 )
167 assert len(res.photos) == 1
168 assert res.photos[0].caption == "Test caption"
171def test_AddPhotoToGallery_multiple_photos(db):
172 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
174 with session_scope() as session:
175 key1 = create_upload(session, user1.id, "photo1.jpg")
176 key2 = create_upload(session, user1.id, "photo2.jpg")
177 key3 = create_upload(session, user1.id, "photo3.jpg")
179 with galleries_session(token1) as api:
180 res = api.AddPhotoToGallery(
181 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key1)
182 )
183 assert len(res.photos) == 1
185 res = api.AddPhotoToGallery(
186 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key2)
187 )
188 assert len(res.photos) == 2
190 res = api.AddPhotoToGallery(
191 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key3)
192 )
193 assert len(res.photos) == 3
196def test_AddPhotoToGallery_not_owner(db):
197 user1, token1 = generate_user(complete_profile=False)
198 user2, token2 = generate_user(complete_profile=False)
200 with session_scope() as session:
201 upload_key = create_upload(session, user2.id)
203 with galleries_session(token2) as api:
204 with pytest.raises(grpc.RpcError) as e:
205 api.AddPhotoToGallery(
206 galleries_pb2.AddPhotoToGalleryReq(
207 gallery_id=user1.profile_gallery_id,
208 upload_key=upload_key,
209 )
210 )
211 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
212 assert e.value.details() == "You do not have permission to edit this gallery."
215def test_AddPhotoToGallery_upload_not_owned(db):
216 user1, token1 = generate_user(complete_profile=False)
217 user2, token2 = generate_user(complete_profile=False)
219 with session_scope() as session:
220 upload_key = create_upload(session, user2.id)
222 with galleries_session(token1) as api:
223 with pytest.raises(grpc.RpcError) as e:
224 api.AddPhotoToGallery(
225 galleries_pb2.AddPhotoToGalleryReq(
226 gallery_id=user1.profile_gallery_id,
227 upload_key=upload_key,
228 )
229 )
230 assert e.value.code() == grpc.StatusCode.NOT_FOUND
231 assert e.value.details() == "Upload not found or you don't own it."
234def test_AddPhotoToGallery_max_capacity(db):
235 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
237 with session_scope() as session:
238 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(5)]
240 with galleries_session(token1) as api:
241 for i in range(4):
242 api.AddPhotoToGallery(
243 galleries_pb2.AddPhotoToGalleryReq(
244 gallery_id=user1.profile_gallery_id,
245 upload_key=keys[i],
246 )
247 )
249 with pytest.raises(grpc.RpcError) as e:
250 api.AddPhotoToGallery(
251 galleries_pb2.AddPhotoToGalleryReq(
252 gallery_id=user1.profile_gallery_id,
253 upload_key=keys[4],
254 )
255 )
256 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
257 assert e.value.details() == "The gallery is at maximum capacity and cannot accept more photos."
260def test_AddPhotoToGallery_duplicate_photo(db):
261 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
263 with session_scope() as session:
264 upload_key = create_upload(session, user1.id)
266 with galleries_session(token1) as api:
267 api.AddPhotoToGallery(
268 galleries_pb2.AddPhotoToGalleryReq(
269 gallery_id=user1.profile_gallery_id,
270 upload_key=upload_key,
271 )
272 )
274 with pytest.raises(grpc.RpcError) as e:
275 api.AddPhotoToGallery(
276 galleries_pb2.AddPhotoToGalleryReq(
277 gallery_id=user1.profile_gallery_id,
278 upload_key=upload_key,
279 )
280 )
281 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
282 assert e.value.details() == "This photo is already in the gallery."
285def test_AddPhotoToGallery_gallery_not_found(db):
286 user1, token1 = generate_user(complete_profile=False)
288 with session_scope() as session:
289 upload_key = create_upload(session, user1.id)
291 with galleries_session(token1) as api:
292 with pytest.raises(grpc.RpcError) as e:
293 api.AddPhotoToGallery(
294 galleries_pb2.AddPhotoToGalleryReq(
295 gallery_id=999999,
296 upload_key=upload_key,
297 )
298 )
299 assert e.value.code() == grpc.StatusCode.NOT_FOUND
300 assert e.value.details() == "Gallery not found."
303def test_RemovePhotoFromGallery_success(db):
304 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
306 with session_scope() as session:
307 key1 = create_upload(session, user1.id, "photo1.jpg")
308 key2 = create_upload(session, user1.id, "photo2.jpg")
309 key3 = create_upload(session, user1.id, "photo3.jpg")
311 with galleries_session(token1) as api:
312 api.AddPhotoToGallery(galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key1))
313 api.AddPhotoToGallery(galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key2))
314 res = api.AddPhotoToGallery(
315 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key3)
316 )
318 item_id = res.photos[1].item_id
320 res = api.RemovePhotoFromGallery(
321 galleries_pb2.RemovePhotoFromGalleryReq(
322 gallery_id=user1.profile_gallery_id,
323 item_id=item_id,
324 )
325 )
327 assert len(res.photos) == 2
330def test_RemovePhotoFromGallery_not_owner(db):
331 user1, token1 = generate_user(complete_profile=False)
332 user2, token2 = generate_user(complete_profile=False)
334 with session_scope() as session:
335 upload_key = create_upload(session, user1.id)
337 with galleries_session(token1) as api:
338 res = api.AddPhotoToGallery(
339 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
340 )
341 item_id = res.photos[0].item_id
343 with galleries_session(token2) as api:
344 with pytest.raises(grpc.RpcError) as e:
345 api.RemovePhotoFromGallery(
346 galleries_pb2.RemovePhotoFromGalleryReq(
347 gallery_id=user1.profile_gallery_id,
348 item_id=item_id,
349 )
350 )
351 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
352 assert e.value.details() == "You do not have permission to edit this gallery."
355def test_RemovePhotoFromGallery_item_not_found(db):
356 user1, token1 = generate_user(complete_profile=False)
358 with galleries_session(token1) as api:
359 with pytest.raises(grpc.RpcError) as e:
360 api.RemovePhotoFromGallery(
361 galleries_pb2.RemovePhotoFromGalleryReq(
362 gallery_id=user1.profile_gallery_id,
363 item_id=999999,
364 )
365 )
366 assert e.value.code() == grpc.StatusCode.NOT_FOUND
367 assert e.value.details() == "Gallery item not found."
370def test_MovePhoto_to_first(db):
371 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
373 with session_scope() as session:
374 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
376 with galleries_session(token1) as api:
377 for key in keys:
378 api.AddPhotoToGallery(
379 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
380 )
382 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
383 item_ids = [photo.item_id for photo in res.photos]
385 # Move last photo to first position
386 res = api.MovePhoto(
387 galleries_pb2.MovePhotoReq(
388 gallery_id=user1.profile_gallery_id,
389 item_id=item_ids[2],
390 after_item_id=0, # 0 means first position
391 )
392 )
394 # Last photo should now be first
395 assert res.photos[0].item_id == item_ids[2]
396 assert res.photos[1].item_id == item_ids[0]
397 assert res.photos[2].item_id == item_ids[1]
400def test_MovePhoto_to_middle(db):
401 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
403 with session_scope() as session:
404 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
406 with galleries_session(token1) as api:
407 for key in keys:
408 api.AddPhotoToGallery(
409 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
410 )
412 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
413 item_ids = [photo.item_id for photo in res.photos]
415 # Move first photo after second (to middle)
416 res = api.MovePhoto(
417 galleries_pb2.MovePhotoReq(
418 gallery_id=user1.profile_gallery_id,
419 item_id=item_ids[0],
420 after_item_id=item_ids[1],
421 )
422 )
424 # Order should be: [1, 0, 2]
425 assert res.photos[0].item_id == item_ids[1]
426 assert res.photos[1].item_id == item_ids[0]
427 assert res.photos[2].item_id == item_ids[2]
430def test_MovePhoto_to_end(db):
431 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
433 with session_scope() as session:
434 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
436 with galleries_session(token1) as api:
437 for key in keys:
438 api.AddPhotoToGallery(
439 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
440 )
442 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
443 item_ids = [photo.item_id for photo in res.photos]
445 # Move first photo to end (after last)
446 res = api.MovePhoto(
447 galleries_pb2.MovePhotoReq(
448 gallery_id=user1.profile_gallery_id,
449 item_id=item_ids[0],
450 after_item_id=item_ids[2],
451 )
452 )
454 # Order should be: [1, 2, 0]
455 assert res.photos[0].item_id == item_ids[1]
456 assert res.photos[1].item_id == item_ids[2]
457 assert res.photos[2].item_id == item_ids[0]
460def test_MovePhoto_noop(db):
461 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
463 with session_scope() as session:
464 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
466 with galleries_session(token1) as api:
467 for key in keys:
468 api.AddPhotoToGallery(
469 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
470 )
472 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
473 item_ids = [photo.item_id for photo in res.photos]
475 # Move photo after itself - should be a no-op
476 res = api.MovePhoto(
477 galleries_pb2.MovePhotoReq(
478 gallery_id=user1.profile_gallery_id,
479 item_id=item_ids[1],
480 after_item_id=item_ids[1],
481 )
482 )
484 # Order should be unchanged
485 assert res.photos[0].item_id == item_ids[0]
486 assert res.photos[1].item_id == item_ids[1]
487 assert res.photos[2].item_id == item_ids[2]
490def test_MovePhoto_not_owner(db):
491 user1, token1 = generate_user(complete_profile=False)
492 user2, token2 = generate_user(complete_profile=False)
494 with session_scope() as session:
495 upload_key = create_upload(session, user1.id)
497 with galleries_session(token1) as api:
498 res = api.AddPhotoToGallery(
499 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
500 )
501 item_id = res.photos[0].item_id
503 with galleries_session(token2) as api:
504 with pytest.raises(grpc.RpcError) as e:
505 api.MovePhoto(
506 galleries_pb2.MovePhotoReq(
507 gallery_id=user1.profile_gallery_id,
508 item_id=item_id,
509 after_item_id=0,
510 )
511 )
512 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
513 assert e.value.details() == "You do not have permission to edit this gallery."
516def test_MovePhoto_item_not_found(db):
517 user1, token1 = generate_user(complete_profile=False)
519 with galleries_session(token1) as api:
520 with pytest.raises(grpc.RpcError) as e:
521 api.MovePhoto(
522 galleries_pb2.MovePhotoReq(
523 gallery_id=user1.profile_gallery_id,
524 item_id=999999,
525 after_item_id=0,
526 )
527 )
528 assert e.value.code() == grpc.StatusCode.NOT_FOUND
529 assert e.value.details() == "Gallery item not found."
532def test_MovePhoto_after_item_not_found(db):
533 user1, token1 = generate_user(complete_profile=False)
535 with session_scope() as session:
536 upload_key = create_upload(session, user1.id)
538 with galleries_session(token1) as api:
539 res = api.AddPhotoToGallery(
540 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
541 )
542 item_id = res.photos[0].item_id
544 with pytest.raises(grpc.RpcError) as e:
545 api.MovePhoto(
546 galleries_pb2.MovePhotoReq(
547 gallery_id=user1.profile_gallery_id,
548 item_id=item_id,
549 after_item_id=999999,
550 )
551 )
552 assert e.value.code() == grpc.StatusCode.NOT_FOUND
553 assert e.value.details() == "The item to place after was not found."
556def test_UpdatePhotoCaption_success(db):
557 user1, token1 = generate_user(complete_profile=False)
559 with session_scope() as session:
560 upload_key = create_upload(session, user1.id)
562 with galleries_session(token1) as api:
563 res = api.AddPhotoToGallery(
564 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
565 )
566 item_id = res.photos[0].item_id
568 res = api.UpdatePhotoCaption(
569 galleries_pb2.UpdatePhotoCaptionReq(
570 gallery_id=user1.profile_gallery_id,
571 item_id=item_id,
572 caption="New caption",
573 )
574 )
576 assert len(res.photos) == 1
577 assert res.photos[0].caption == "New caption"
580def test_UpdatePhotoCaption_clear_caption(db):
581 user1, token1 = generate_user(complete_profile=False)
583 with session_scope() as session:
584 upload_key = create_upload(session, user1.id)
586 with galleries_session(token1) as api:
587 res = api.AddPhotoToGallery(
588 galleries_pb2.AddPhotoToGalleryReq(
589 gallery_id=user1.profile_gallery_id,
590 upload_key=upload_key,
591 caption="Initial caption",
592 )
593 )
594 item_id = res.photos[0].item_id
596 res = api.UpdatePhotoCaption(
597 galleries_pb2.UpdatePhotoCaptionReq(
598 gallery_id=user1.profile_gallery_id,
599 item_id=item_id,
600 caption="",
601 )
602 )
604 assert len(res.photos) == 1
605 assert res.photos[0].caption == ""
608def test_UpdatePhotoCaption_not_owner(db):
609 user1, token1 = generate_user(complete_profile=False)
610 user2, token2 = generate_user(complete_profile=False)
612 with session_scope() as session:
613 upload_key = create_upload(session, user1.id)
615 with galleries_session(token1) as api:
616 res = api.AddPhotoToGallery(
617 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
618 )
619 item_id = res.photos[0].item_id
621 with galleries_session(token2) as api:
622 with pytest.raises(grpc.RpcError) as e:
623 api.UpdatePhotoCaption(
624 galleries_pb2.UpdatePhotoCaptionReq(
625 gallery_id=user1.profile_gallery_id,
626 item_id=item_id,
627 caption="Hacked!",
628 )
629 )
630 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
631 assert e.value.details() == "You do not have permission to edit this gallery."
634def test_UpdatePhotoCaption_item_not_found(db):
635 user1, token1 = generate_user(complete_profile=False)
637 with galleries_session(token1) as api:
638 with pytest.raises(grpc.RpcError) as e:
639 api.UpdatePhotoCaption(
640 galleries_pb2.UpdatePhotoCaptionReq(
641 gallery_id=user1.profile_gallery_id,
642 item_id=999999,
643 caption="Test",
644 )
645 )
646 assert e.value.code() == grpc.StatusCode.NOT_FOUND
647 assert e.value.details() == "Gallery item not found."
650def test_remove_and_readd_photo(db):
651 user1, token1 = generate_user(complete_profile=False)
653 with session_scope() as session:
654 upload_key = create_upload(session, user1.id)
656 with galleries_session(token1) as api:
657 res = api.AddPhotoToGallery(
658 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
659 )
660 item_id = res.photos[0].item_id
662 res = api.RemovePhotoFromGallery(
663 galleries_pb2.RemovePhotoFromGalleryReq(gallery_id=user1.profile_gallery_id, item_id=item_id)
664 )
665 assert len(res.photos) == 0
667 res = api.AddPhotoToGallery(
668 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
669 )
670 assert len(res.photos) == 1
673def test_gallery_photo_ordering_preserved(db):
674 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
676 with session_scope() as session:
677 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(4)]
679 with galleries_session(token1) as api:
680 item_ids = []
681 for key in keys:
682 res = api.AddPhotoToGallery(
683 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
684 )
685 item_ids.append(res.photos[-1].item_id)
687 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
688 assert len(res.photos) == 4
689 for i, photo in enumerate(res.photos):
690 assert photo.item_id == item_ids[i]
693def test_database_constraints_upload_uniqueness(db):
694 user1, token1 = generate_user(complete_profile=False)
696 with session_scope() as session:
697 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
699 upload = Upload(key="key1", filename="test.jpg", creator_user_id=user.id)
700 session.add(upload)
701 session.flush()
703 gallery_id = user.profile_gallery_id
704 assert gallery_id
706 item1 = PhotoGalleryItem(gallery_id=gallery_id, upload_key="key1", position=0.0)
707 item2 = PhotoGalleryItem(gallery_id=gallery_id, upload_key="key1", position=1.0)
708 session.add_all([item1, item2])
710 with pytest.raises(IntegrityError):
711 session.flush()
713 session.rollback()
716# Avatar photo selection tests
719def test_get_avatar_upload_returns_first_by_position(db):
720 """get_avatar_upload should return the upload with the lowest position value"""
721 from couchers.models.uploads import get_avatar_upload
723 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
725 with session_scope() as session:
726 # Create uploads with specific filenames so we can identify them
727 keys = []
728 for i, filename in enumerate(["first.jpg", "second.jpg", "third.jpg"]):
729 key = f"key_{filename}_{user1.id}"
730 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
731 session.add(upload)
732 keys.append(key)
733 session.commit()
735 # Add photos in reverse position order (third has lowest position)
736 with session_scope() as session:
737 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
738 gallery_id = user.profile_gallery_id
739 assert gallery_id is not None
741 # Add with positions: third=0.5, first=1.0, second=2.0
742 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=0.5))
743 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=1.0))
744 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=2.0))
745 session.commit()
747 with session_scope() as session:
748 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
749 avatar = get_avatar_upload(session, user)
751 assert avatar is not None
752 assert avatar.filename == "third.jpg"
755def test_get_avatar_upload_no_photos(db):
756 """get_avatar_upload should return None when user has no photos"""
757 from couchers.models.uploads import get_avatar_upload
759 user1, token1 = generate_user(complete_profile=False)
761 with session_scope() as session:
762 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
763 avatar = get_avatar_upload(session, user)
765 assert avatar is None
768def test_has_avatar_photo_expression_with_photos(db):
769 """has_avatar_photo_expression should return True when user has photos"""
770 from couchers.models.uploads import has_avatar_photo_expression
772 user1, token1 = generate_user(complete_profile=False)
774 with session_scope() as session:
775 upload_key = create_upload(session, user1.id)
777 with galleries_session(token1) as api:
778 api.AddPhotoToGallery(
779 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=upload_key)
780 )
782 with session_scope() as session:
783 # Test with User class (SQL expression)
784 result = session.execute(
785 select(User.id).where(User.id == user1.id).where(has_avatar_photo_expression(User))
786 ).scalar_one_or_none()
787 assert result == user1.id
789 # Test with User instance
790 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
791 has_photo = session.execute(select(has_avatar_photo_expression(user))).scalar()
792 assert has_photo is True
795def test_has_avatar_photo_expression_no_photos(db):
796 """has_avatar_photo_expression should return False when user has no photos"""
797 from couchers.models.uploads import has_avatar_photo_expression
799 user1, token1 = generate_user(complete_profile=False)
801 with session_scope() as session:
802 # Test with User class (SQL expression) - should not match
803 result = session.execute(
804 select(User.id).where(User.id == user1.id).where(has_avatar_photo_expression(User))
805 ).scalar_one_or_none()
806 assert result is None
808 # Test with User instance
809 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
810 has_photo = session.execute(select(has_avatar_photo_expression(user))).scalar()
811 assert has_photo is False
814def test_avatar_url_via_api_reflects_first_photo(db):
815 """GetUser should return avatar URL matching the first photo by position"""
816 from couchers.proto import api_pb2
817 from tests.fixtures.sessions import api_session
819 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
820 user2, token2 = generate_user()
822 with session_scope() as session:
823 keys = []
824 for i, filename in enumerate(["avatar1.jpg", "avatar2.jpg", "avatar3.jpg"]):
825 key = f"key_{filename}_{user1.id}"
826 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
827 session.add(upload)
828 keys.append(key)
829 session.commit()
831 # Add photos: avatar2 has lowest position, so it should be the avatar
832 with session_scope() as session:
833 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
834 gallery_id = user.profile_gallery_id
835 assert gallery_id is not None
837 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.5)) # avatar2
838 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=1.0)) # avatar1
839 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=2.0)) # avatar3
840 session.commit()
842 with api_session(token2) as api:
843 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
845 assert "avatar2.jpg" in user_pb.avatar_url
846 assert "avatar2.jpg" in user_pb.avatar_thumbnail_url
849def test_avatar_changes_after_reordering(db):
850 """Moving a photo to first position should make it the new avatar"""
851 from couchers.proto import api_pb2
852 from tests.fixtures.sessions import api_session
854 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
855 user2, token2 = generate_user()
857 with session_scope() as session:
858 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)]
860 with galleries_session(token1) as api:
861 for key in keys:
862 api.AddPhotoToGallery(
863 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key)
864 )
866 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id))
867 item_ids = [photo.item_id for photo in res.photos]
869 # Check initial avatar (photo0)
870 with api_session(token2) as api:
871 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
872 assert "photo0.jpg" in user_pb.avatar_url
874 # Move photo2 to first position
875 with galleries_session(token1) as api:
876 api.MovePhoto(
877 galleries_pb2.MovePhotoReq(
878 gallery_id=user1.profile_gallery_id,
879 item_id=item_ids[2],
880 after_item_id=0, # 0 means first position
881 )
882 )
884 # Check avatar is now photo2
885 with api_session(token2) as api:
886 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username))
887 assert "photo2.jpg" in user_pb.avatar_url
890def test_avatar_with_negative_positions(db):
891 """Avatar selection should work correctly with negative position values"""
892 from couchers.models.uploads import get_avatar_upload
894 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
896 with session_scope() as session:
897 keys = []
898 for filename in ["neg.jpg", "zero.jpg", "pos.jpg"]:
899 key = f"key_{filename}_{user1.id}"
900 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
901 session.add(upload)
902 keys.append(key)
903 session.commit()
905 with session_scope() as session:
906 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
907 gallery_id = user.profile_gallery_id
908 assert gallery_id is not None
910 # neg.jpg has the lowest position
911 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=-5.0))
912 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.0))
913 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=5.0))
914 session.commit()
916 with session_scope() as session:
917 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
918 avatar = get_avatar_upload(session, user)
920 assert avatar is not None
921 assert avatar.filename == "neg.jpg"
924def test_avatar_with_fractional_positions(db):
925 """Avatar selection should work correctly with fractional position values"""
926 from couchers.models.uploads import get_avatar_upload
928 user1, token1 = generate_user(complete_profile=False, strong_verification=True)
930 with session_scope() as session:
931 keys = []
932 for filename in ["a.jpg", "b.jpg", "c.jpg"]:
933 key = f"key_{filename}_{user1.id}"
934 upload = Upload(key=key, filename=filename, creator_user_id=user1.id)
935 session.add(upload)
936 keys.append(key)
937 session.commit()
939 with session_scope() as session:
940 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
941 gallery_id = user.profile_gallery_id
942 assert gallery_id is not None
944 # b.jpg has the lowest position (0.001)
945 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[0], position=0.5))
946 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[1], position=0.001))
947 session.add(PhotoGalleryItem(gallery_id=gallery_id, upload_key=keys[2], position=0.999))
948 session.commit()
950 with session_scope() as session:
951 user = session.execute(select(User).where(User.id == user1.id)).scalar_one()
952 avatar = get_avatar_upload(session, user)
954 assert avatar is not None
955 assert avatar.filename == "b.jpg"