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

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 

14@pytest.fixture(autouse=True) 

15def _(testconfig): 

16 pass 

17 

18 

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 

29 

30 

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) 

34 

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 

38 

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

40 assert gallery.owner_user_id == user1.id 

41 

42 

43def test_GetGalleryEditInfo(db): 

44 user1, token1 = generate_user(complete_profile=False) 

45 

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 

51 

52 

53def test_GetGalleryEditInfo_verified_user(db): 

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

55 

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 

61 

62 

63def test_GetGalleryEditInfo_not_owner(db): 

64 user1, token1 = generate_user(complete_profile=False) 

65 user2, token2 = generate_user(complete_profile=False) 

66 

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

72 

73 

74def test_GetGalleryEditInfo_not_found(db): 

75 user1, token1 = generate_user(complete_profile=False) 

76 

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

82 

83 

84def test_GetGalleryEditInfo_with_photos(db): 

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

86 

87 with session_scope() as session: 

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

89 

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 ) 

95 

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 

99 

100 

101def test_GetGallery_as_owner(db): 

102 user1, token1 = generate_user(complete_profile=False) 

103 

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 

109 

110 

111def test_GetGallery_as_non_owner(db): 

112 user1, token1 = generate_user(complete_profile=False) 

113 user2, token2 = generate_user(complete_profile=False) 

114 

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 

120 

121 

122def test_GetGallery_not_found(db): 

123 user1, token1 = generate_user(complete_profile=False) 

124 

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

130 

131 

132def test_AddPhotoToGallery_success(db): 

133 user1, token1 = generate_user(complete_profile=False) 

134 

135 with session_scope() as session: 

136 upload_key = create_upload(session, user1.id) 

137 

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 ) 

145 

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

151 

152 

153def test_AddPhotoToGallery_with_caption(db): 

154 user1, token1 = generate_user(complete_profile=False) 

155 

156 with session_scope() as session: 

157 upload_key = create_upload(session, user1.id) 

158 

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 ) 

167 

168 assert len(res.photos) == 1 

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

170 

171 

172def test_AddPhotoToGallery_multiple_photos(db): 

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

174 

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

179 

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 

185 

186 res = api.AddPhotoToGallery( 

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

188 ) 

189 assert len(res.photos) == 2 

190 

191 res = api.AddPhotoToGallery( 

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

193 ) 

194 assert len(res.photos) == 3 

195 

196 

197def test_AddPhotoToGallery_not_owner(db): 

198 user1, token1 = generate_user(complete_profile=False) 

199 user2, token2 = generate_user(complete_profile=False) 

200 

201 with session_scope() as session: 

202 upload_key = create_upload(session, user2.id) 

203 

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

214 

215 

216def test_AddPhotoToGallery_upload_not_owned(db): 

217 user1, token1 = generate_user(complete_profile=False) 

218 user2, token2 = generate_user(complete_profile=False) 

219 

220 with session_scope() as session: 

221 upload_key = create_upload(session, user2.id) 

222 

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

233 

234 

235def test_AddPhotoToGallery_max_capacity(db): 

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

237 

238 with session_scope() as session: 

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

240 

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 ) 

249 

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

259 

260 

261def test_AddPhotoToGallery_duplicate_photo(db): 

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

263 

264 with session_scope() as session: 

265 upload_key = create_upload(session, user1.id) 

266 

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 ) 

274 

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

284 

285 

286def test_AddPhotoToGallery_gallery_not_found(db): 

287 user1, token1 = generate_user(complete_profile=False) 

288 

289 with session_scope() as session: 

290 upload_key = create_upload(session, user1.id) 

291 

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

302 

303 

304def test_RemovePhotoFromGallery_success(db): 

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

306 

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

311 

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 ) 

318 

319 item_id = res.photos[1].item_id 

320 

321 res = api.RemovePhotoFromGallery( 

322 galleries_pb2.RemovePhotoFromGalleryReq( 

323 gallery_id=user1.profile_gallery_id, 

324 item_id=item_id, 

325 ) 

326 ) 

327 

328 assert len(res.photos) == 2 

329 

330 

331def test_RemovePhotoFromGallery_not_owner(db): 

332 user1, token1 = generate_user(complete_profile=False) 

333 user2, token2 = generate_user(complete_profile=False) 

334 

335 with session_scope() as session: 

336 upload_key = create_upload(session, user1.id) 

337 

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 

343 

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

354 

355 

356def test_RemovePhotoFromGallery_item_not_found(db): 

357 user1, token1 = generate_user(complete_profile=False) 

358 

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

369 

370 

371def test_MovePhoto_to_first(db): 

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

373 

374 with session_scope() as session: 

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

376 

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 ) 

382 

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

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

385 

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 ) 

394 

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] 

399 

400 

401def test_MovePhoto_to_middle(db): 

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

403 

404 with session_scope() as session: 

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

406 

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 ) 

412 

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

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

415 

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 ) 

424 

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] 

429 

430 

431def test_MovePhoto_to_end(db): 

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

433 

434 with session_scope() as session: 

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

436 

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 ) 

442 

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

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

445 

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 ) 

454 

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] 

459 

460 

461def test_MovePhoto_noop(db): 

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

463 

464 with session_scope() as session: 

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

466 

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 ) 

472 

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

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

475 

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 ) 

484 

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] 

489 

490 

491def test_MovePhoto_not_owner(db): 

492 user1, token1 = generate_user(complete_profile=False) 

493 user2, token2 = generate_user(complete_profile=False) 

494 

495 with session_scope() as session: 

496 upload_key = create_upload(session, user1.id) 

497 

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 

503 

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

515 

516 

517def test_MovePhoto_item_not_found(db): 

518 user1, token1 = generate_user(complete_profile=False) 

519 

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

531 

532 

533def test_MovePhoto_after_item_not_found(db): 

534 user1, token1 = generate_user(complete_profile=False) 

535 

536 with session_scope() as session: 

537 upload_key = create_upload(session, user1.id) 

538 

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 

544 

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

555 

556 

557def test_UpdatePhotoCaption_success(db): 

558 user1, token1 = generate_user(complete_profile=False) 

559 

560 with session_scope() as session: 

561 upload_key = create_upload(session, user1.id) 

562 

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 

568 

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 ) 

576 

577 assert len(res.photos) == 1 

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

579 

580 

581def test_UpdatePhotoCaption_clear_caption(db): 

582 user1, token1 = generate_user(complete_profile=False) 

583 

584 with session_scope() as session: 

585 upload_key = create_upload(session, user1.id) 

586 

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 

596 

597 res = api.UpdatePhotoCaption( 

598 galleries_pb2.UpdatePhotoCaptionReq( 

599 gallery_id=user1.profile_gallery_id, 

600 item_id=item_id, 

601 caption="", 

602 ) 

603 ) 

604 

605 assert len(res.photos) == 1 

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

607 

608 

609def test_UpdatePhotoCaption_not_owner(db): 

610 user1, token1 = generate_user(complete_profile=False) 

611 user2, token2 = generate_user(complete_profile=False) 

612 

613 with session_scope() as session: 

614 upload_key = create_upload(session, user1.id) 

615 

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 

621 

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

633 

634 

635def test_UpdatePhotoCaption_item_not_found(db): 

636 user1, token1 = generate_user(complete_profile=False) 

637 

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

649 

650 

651def test_remove_and_readd_photo(db): 

652 user1, token1 = generate_user(complete_profile=False) 

653 

654 with session_scope() as session: 

655 upload_key = create_upload(session, user1.id) 

656 

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 

662 

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 

667 

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 

672 

673 

674def test_gallery_photo_ordering_preserved(db): 

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

676 

677 with session_scope() as session: 

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

679 

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) 

687 

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] 

692 

693 

694def test_database_constraints_upload_uniqueness(db): 

695 user1, token1 = generate_user(complete_profile=False) 

696 

697 with session_scope() as session: 

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

699 

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

701 session.add(upload) 

702 session.flush() 

703 

704 gallery_id = user.profile_gallery_id 

705 assert gallery_id 

706 

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

710 

711 with pytest.raises(IntegrityError): 

712 session.flush() 

713 

714 session.rollback() 

715 

716 

717# Avatar photo selection tests 

718 

719 

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) 

723 

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

733 

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 

739 

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

745 

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) 

749 

750 assert avatar is not None 

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

752 

753 

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) 

757 

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) 

761 

762 assert avatar is None 

763 

764 

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) 

768 

769 with session_scope() as session: 

770 upload_key = create_upload(session, user1.id) 

771 

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 ) 

776 

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 

783 

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 

788 

789 

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) 

793 

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 

800 

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 

805 

806 

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

811 

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

820 

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 

826 

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

831 

832 with api_session(token2) as api: 

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

834 

835 assert "avatar2.jpg" in user_pb.avatar_url 

836 assert "avatar2.jpg" in user_pb.avatar_thumbnail_url 

837 

838 

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

843 

844 with session_scope() as session: 

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

846 

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 ) 

852 

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

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

855 

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 

860 

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 ) 

870 

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 

875 

876 

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) 

880 

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

889 

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 

894 

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

900 

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) 

904 

905 assert avatar is not None 

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

907 

908 

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) 

912 

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

921 

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 

926 

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

932 

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) 

936 

937 assert avatar is not None 

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