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

1import grpc 

2import pytest 

3from sqlalchemy import select 

4from sqlalchemy.exc import IntegrityError 

5 

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 

12 

13 

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 

24 

25 

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) 

29 

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 

33 

34 gallery = session.execute(select(PhotoGallery).where(PhotoGallery.id == user.profile_gallery_id)).scalar_one() 

35 assert gallery.owner_user_id == user1.id 

36 

37 

38def test_GetGalleryEditInfo(db): 

39 user1, token1 = generate_user(complete_profile=False) 

40 

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 

46 

47 

48def test_GetGalleryEditInfo_verified_user(db): 

49 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

50 

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 

56 

57 

58def test_GetGalleryEditInfo_not_owner(db): 

59 user1, token1 = generate_user(complete_profile=False) 

60 user2, token2 = generate_user(complete_profile=False) 

61 

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." 

67 

68 

69def test_GetGalleryEditInfo_not_found(db): 

70 user1, token1 = generate_user(complete_profile=False) 

71 

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." 

77 

78 

79def test_GetGalleryEditInfo_with_photos(db): 

80 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

81 

82 with session_scope() as session: 

83 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)] 

84 

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 ) 

90 

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 

94 

95 

96def test_GetGallery_as_owner(db): 

97 user1, token1 = generate_user(complete_profile=False) 

98 

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 

104 

105 

106def test_GetGallery_as_non_owner(db): 

107 user1, token1 = generate_user(complete_profile=False) 

108 user2, token2 = generate_user(complete_profile=False) 

109 

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 

115 

116 

117def test_GetGallery_not_found(db): 

118 user1, token1 = generate_user(complete_profile=False) 

119 

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." 

125 

126 

127def test_AddPhotoToGallery_success(db): 

128 user1, token1 = generate_user(complete_profile=False) 

129 

130 with session_scope() as session: 

131 upload_key = create_upload(session, user1.id) 

132 

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 ) 

140 

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 == "" 

146 

147 

148def test_AddPhotoToGallery_with_caption(db): 

149 user1, token1 = generate_user(complete_profile=False) 

150 

151 with session_scope() as session: 

152 upload_key = create_upload(session, user1.id) 

153 

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 ) 

162 

163 assert len(res.photos) == 1 

164 assert res.photos[0].caption == "Test caption" 

165 

166 

167def test_AddPhotoToGallery_multiple_photos(db): 

168 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

169 

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") 

174 

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 

180 

181 res = api.AddPhotoToGallery( 

182 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key2) 

183 ) 

184 assert len(res.photos) == 2 

185 

186 res = api.AddPhotoToGallery( 

187 galleries_pb2.AddPhotoToGalleryReq(gallery_id=user1.profile_gallery_id, upload_key=key3) 

188 ) 

189 assert len(res.photos) == 3 

190 

191 

192def test_AddPhotoToGallery_not_owner(db): 

193 user1, token1 = generate_user(complete_profile=False) 

194 user2, token2 = generate_user(complete_profile=False) 

195 

196 with session_scope() as session: 

197 upload_key = create_upload(session, user2.id) 

198 

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." 

209 

210 

211def test_AddPhotoToGallery_upload_not_owned(db): 

212 user1, token1 = generate_user(complete_profile=False) 

213 user2, token2 = generate_user(complete_profile=False) 

214 

215 with session_scope() as session: 

216 upload_key = create_upload(session, user2.id) 

217 

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." 

228 

229 

230def test_AddPhotoToGallery_max_capacity(db): 

231 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

232 

233 with session_scope() as session: 

234 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(6)] 

235 

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 ) 

244 

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." 

254 

255 

256def test_AddPhotoToGallery_duplicate_photo(db): 

257 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

258 

259 with session_scope() as session: 

260 upload_key = create_upload(session, user1.id) 

261 

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 ) 

269 

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." 

279 

280 

281def test_AddPhotoToGallery_gallery_not_found(db): 

282 user1, token1 = generate_user(complete_profile=False) 

283 

284 with session_scope() as session: 

285 upload_key = create_upload(session, user1.id) 

286 

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." 

297 

298 

299def test_RemovePhotoFromGallery_success(db): 

300 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

301 

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") 

306 

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 ) 

313 

314 item_id = res.photos[1].item_id 

315 

316 res = api.RemovePhotoFromGallery( 

317 galleries_pb2.RemovePhotoFromGalleryReq( 

318 gallery_id=user1.profile_gallery_id, 

319 item_id=item_id, 

320 ) 

321 ) 

322 

323 assert len(res.photos) == 2 

324 

325 

326def test_RemovePhotoFromGallery_not_owner(db): 

327 user1, token1 = generate_user(complete_profile=False) 

328 user2, token2 = generate_user(complete_profile=False) 

329 

330 with session_scope() as session: 

331 upload_key = create_upload(session, user1.id) 

332 

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 

338 

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." 

349 

350 

351def test_RemovePhotoFromGallery_item_not_found(db): 

352 user1, token1 = generate_user(complete_profile=False) 

353 

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." 

364 

365 

366def test_MovePhoto_to_first(db): 

367 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

368 

369 with session_scope() as session: 

370 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)] 

371 

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 ) 

377 

378 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id)) 

379 item_ids = [photo.item_id for photo in res.photos] 

380 

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 ) 

389 

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] 

394 

395 

396def test_MovePhoto_to_middle(db): 

397 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

398 

399 with session_scope() as session: 

400 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)] 

401 

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 ) 

407 

408 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id)) 

409 item_ids = [photo.item_id for photo in res.photos] 

410 

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 ) 

419 

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] 

424 

425 

426def test_MovePhoto_to_end(db): 

427 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

428 

429 with session_scope() as session: 

430 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)] 

431 

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 ) 

437 

438 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id)) 

439 item_ids = [photo.item_id for photo in res.photos] 

440 

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 ) 

449 

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] 

454 

455 

456def test_MovePhoto_noop(db): 

457 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

458 

459 with session_scope() as session: 

460 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)] 

461 

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 ) 

467 

468 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id)) 

469 item_ids = [photo.item_id for photo in res.photos] 

470 

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 ) 

479 

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] 

484 

485 

486def test_MovePhoto_not_owner(db): 

487 user1, token1 = generate_user(complete_profile=False) 

488 user2, token2 = generate_user(complete_profile=False) 

489 

490 with session_scope() as session: 

491 upload_key = create_upload(session, user1.id) 

492 

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 

498 

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." 

510 

511 

512def test_MovePhoto_item_not_found(db): 

513 user1, token1 = generate_user(complete_profile=False) 

514 

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." 

526 

527 

528def test_MovePhoto_after_item_not_found(db): 

529 user1, token1 = generate_user(complete_profile=False) 

530 

531 with session_scope() as session: 

532 upload_key = create_upload(session, user1.id) 

533 

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 

539 

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." 

550 

551 

552def test_UpdatePhotoCaption_success(db): 

553 user1, token1 = generate_user(complete_profile=False) 

554 

555 with session_scope() as session: 

556 upload_key = create_upload(session, user1.id) 

557 

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 

563 

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 ) 

571 

572 assert len(res.photos) == 1 

573 assert res.photos[0].caption == "New caption" 

574 

575 

576def test_UpdatePhotoCaption_clear_caption(db): 

577 user1, token1 = generate_user(complete_profile=False) 

578 

579 with session_scope() as session: 

580 upload_key = create_upload(session, user1.id) 

581 

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 

591 

592 res = api.UpdatePhotoCaption( 

593 galleries_pb2.UpdatePhotoCaptionReq( 

594 gallery_id=user1.profile_gallery_id, 

595 item_id=item_id, 

596 caption="", 

597 ) 

598 ) 

599 

600 assert len(res.photos) == 1 

601 assert res.photos[0].caption == "" 

602 

603 

604def test_UpdatePhotoCaption_not_owner(db): 

605 user1, token1 = generate_user(complete_profile=False) 

606 user2, token2 = generate_user(complete_profile=False) 

607 

608 with session_scope() as session: 

609 upload_key = create_upload(session, user1.id) 

610 

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 

616 

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." 

628 

629 

630def test_UpdatePhotoCaption_item_not_found(db): 

631 user1, token1 = generate_user(complete_profile=False) 

632 

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." 

644 

645 

646def test_remove_and_readd_photo(db): 

647 user1, token1 = generate_user(complete_profile=False) 

648 

649 with session_scope() as session: 

650 upload_key = create_upload(session, user1.id) 

651 

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 

657 

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 

662 

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 

667 

668 

669def test_gallery_photo_ordering_preserved(db): 

670 user1, token1 = generate_user(complete_profile=False, strong_verification=True) 

671 

672 with session_scope() as session: 

673 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(4)] 

674 

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) 

682 

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] 

687 

688 

689def test_database_constraints_upload_uniqueness(db): 

690 user1, token1 = generate_user(complete_profile=False) 

691 

692 with session_scope() as session: 

693 user = session.execute(select(User).where(User.id == user1.id)).scalar_one() 

694 

695 upload = Upload(key="key1", filename="test.jpg", creator_user_id=user.id) 

696 session.add(upload) 

697 session.flush() 

698 

699 gallery_id = user.profile_gallery_id 

700 assert gallery_id 

701 

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]) 

705 

706 with pytest.raises(IntegrityError): 

707 session.flush() 

708 

709 session.rollback() 

710 

711 

712# Avatar photo selection tests 

713 

714 

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) 

718 

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() 

728 

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 

734 

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() 

740 

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) 

744 

745 assert avatar is not None 

746 assert avatar.filename == "third.jpg" 

747 

748 

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) 

752 

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) 

756 

757 assert avatar is None 

758 

759 

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) 

763 

764 with session_scope() as session: 

765 upload_key = create_upload(session, user1.id) 

766 

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 ) 

771 

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 

778 

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 

783 

784 

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) 

788 

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 

795 

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 

800 

801 

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() 

806 

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() 

815 

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 

821 

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() 

826 

827 with api_session(token2) as api: 

828 user_pb = api.GetUser(api_pb2.GetUserReq(user=user1.username)) 

829 

830 assert "avatar2.jpg" in user_pb.avatar_url 

831 assert "avatar2.jpg" in user_pb.avatar_thumbnail_url 

832 

833 

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() 

838 

839 with session_scope() as session: 

840 keys = [create_upload(session, user1.id, f"photo{i}.jpg") for i in range(3)] 

841 

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 ) 

847 

848 res = api.GetGallery(galleries_pb2.GetGalleryReq(gallery_id=user1.profile_gallery_id)) 

849 item_ids = [photo.item_id for photo in res.photos] 

850 

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 

855 

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 ) 

865 

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 

870 

871 

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) 

875 

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() 

884 

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 

889 

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() 

895 

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) 

899 

900 assert avatar is not None 

901 assert avatar.filename == "neg.jpg" 

902 

903 

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) 

907 

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() 

916 

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 

921 

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() 

927 

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) 

931 

932 assert avatar is not None 

933 assert avatar.filename == "b.jpg"