at main 26 kB view raw
1import 'package:coves_flutter/providers/auth_provider.dart'; 2import 'package:coves_flutter/providers/vote_provider.dart'; 3import 'package:coves_flutter/services/api_exceptions.dart'; 4import 'package:coves_flutter/services/vote_service.dart'; 5import 'package:flutter_test/flutter_test.dart'; 6import 'package:mockito/annotations.dart'; 7import 'package:mockito/mockito.dart'; 8 9import 'vote_provider_test.mocks.dart'; 10 11// Generate mocks for VoteService and AuthProvider 12@GenerateMocks([VoteService, AuthProvider]) 13void main() { 14 TestWidgetsFlutterBinding.ensureInitialized(); 15 16 group('VoteProvider', () { 17 late VoteProvider voteProvider; 18 late MockVoteService mockVoteService; 19 late MockAuthProvider mockAuthProvider; 20 21 setUp(() { 22 mockVoteService = MockVoteService(); 23 mockAuthProvider = MockAuthProvider(); 24 25 // Default: user is authenticated 26 when(mockAuthProvider.isAuthenticated).thenReturn(true); 27 28 voteProvider = VoteProvider( 29 voteService: mockVoteService, 30 authProvider: mockAuthProvider, 31 ); 32 }); 33 34 tearDown(() { 35 voteProvider.dispose(); 36 }); 37 38 group('toggleVote', () { 39 const testPostUri = 'at://did:plc:test/social.coves.post.record/123'; 40 const testPostCid = 'bafy2bzacepostcid123'; 41 42 test('should create vote with optimistic update', () async { 43 // Mock successful API response 44 when( 45 mockVoteService.createVote( 46 postUri: anyNamed('postUri'), 47 postCid: anyNamed('postCid'), 48 direction: anyNamed('direction'), 49 ), 50 ).thenAnswer( 51 (_) async => const VoteResponse( 52 uri: 'at://did:plc:test/social.coves.feed.vote/456', 53 cid: 'bafy123', 54 rkey: '456', 55 deleted: false, 56 ), 57 ); 58 59 var notificationCount = 0; 60 voteProvider.addListener(() { 61 notificationCount++; 62 }); 63 64 // Initially not liked 65 expect(voteProvider.isLiked(testPostUri), false); 66 67 // Toggle vote 68 final wasLiked = await voteProvider.toggleVote( 69 postUri: testPostUri, 70 postCid: testPostCid, 71 ); 72 73 // Should return true (vote created) 74 expect(wasLiked, true); 75 76 // Should be liked now 77 expect(voteProvider.isLiked(testPostUri), true); 78 79 // Should have notified listeners twice (optimistic + server response) 80 expect(notificationCount, greaterThanOrEqualTo(2)); 81 82 // Vote state should be correct 83 final voteState = voteProvider.getVoteState(testPostUri); 84 expect(voteState?.direction, 'up'); 85 expect(voteState?.uri, 'at://did:plc:test/social.coves.feed.vote/456'); 86 expect(voteState?.deleted, false); 87 }); 88 89 test('should remove vote when toggled off', () async { 90 // First, set up initial vote state 91 voteProvider.setInitialVoteState( 92 postUri: testPostUri, 93 voteDirection: 'up', 94 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 95 ); 96 97 expect(voteProvider.isLiked(testPostUri), true); 98 99 // Mock API response for toggling off 100 when( 101 mockVoteService.createVote( 102 postUri: anyNamed('postUri'), 103 postCid: anyNamed('postCid'), 104 direction: anyNamed('direction'), 105 ), 106 ).thenAnswer((_) async => const VoteResponse(deleted: true)); 107 108 // Toggle vote off 109 final wasLiked = await voteProvider.toggleVote( 110 postUri: testPostUri, 111 postCid: testPostCid, 112 ); 113 114 // Should return false (vote removed) 115 expect(wasLiked, false); 116 117 // Should not be liked anymore 118 expect(voteProvider.isLiked(testPostUri), false); 119 120 // Vote state should be marked as deleted 121 final voteState = voteProvider.getVoteState(testPostUri); 122 expect(voteState?.deleted, true); 123 }); 124 125 test('should rollback on API error', () async { 126 // Set up initial state (not voted) 127 expect(voteProvider.isLiked(testPostUri), false); 128 129 // Mock API failure 130 when( 131 mockVoteService.createVote( 132 postUri: anyNamed('postUri'), 133 postCid: anyNamed('postCid'), 134 direction: anyNamed('direction'), 135 ), 136 ).thenThrow(ApiException('Network error', statusCode: 500)); 137 138 var notificationCount = 0; 139 voteProvider.addListener(() { 140 notificationCount++; 141 }); 142 143 // Try to toggle vote 144 expect( 145 () => voteProvider.toggleVote( 146 postUri: testPostUri, 147 postCid: testPostCid, 148 ), 149 throwsA(isA<ApiException>()), 150 ); 151 152 // Should rollback to initial state (not liked) 153 await Future.delayed(Duration.zero); // Wait for async completion 154 expect(voteProvider.isLiked(testPostUri), false); 155 expect(voteProvider.getVoteState(testPostUri), null); 156 157 // Should have notified listeners (optimistic + rollback) 158 expect(notificationCount, greaterThanOrEqualTo(2)); 159 }); 160 161 test('should rollback to previous state on error', () async { 162 // Set up initial voted state 163 voteProvider.setInitialVoteState( 164 postUri: testPostUri, 165 voteDirection: 'up', 166 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 167 ); 168 169 final initialState = voteProvider.getVoteState(testPostUri); 170 expect(voteProvider.isLiked(testPostUri), true); 171 172 // Mock API failure when trying to toggle off 173 when( 174 mockVoteService.createVote( 175 postUri: anyNamed('postUri'), 176 postCid: anyNamed('postCid'), 177 direction: anyNamed('direction'), 178 ), 179 ).thenThrow(NetworkException('Connection failed')); 180 181 // Try to toggle vote off 182 expect( 183 () => voteProvider.toggleVote( 184 postUri: testPostUri, 185 postCid: testPostCid, 186 ), 187 throwsA(isA<ApiException>()), 188 ); 189 190 // Should rollback to initial liked state 191 await Future.delayed(Duration.zero); // Wait for async completion 192 expect(voteProvider.isLiked(testPostUri), true); 193 expect(voteProvider.getVoteState(testPostUri)?.uri, initialState?.uri); 194 }); 195 196 test('should prevent concurrent requests for same post', () async { 197 // Mock slow API response 198 when( 199 mockVoteService.createVote( 200 postUri: anyNamed('postUri'), 201 postCid: anyNamed('postCid'), 202 direction: anyNamed('direction'), 203 ), 204 ).thenAnswer((_) async { 205 await Future.delayed(const Duration(milliseconds: 100)); 206 return const VoteResponse( 207 uri: 'at://did:plc:test/social.coves.feed.vote/456', 208 cid: 'bafy123', 209 rkey: '456', 210 deleted: false, 211 ); 212 }); 213 214 // Start first request 215 final future1 = voteProvider.toggleVote( 216 postUri: testPostUri, 217 postCid: testPostCid, 218 ); 219 220 // Try to start second request before first completes 221 final result2 = await voteProvider.toggleVote( 222 postUri: testPostUri, 223 postCid: testPostCid, 224 ); 225 226 // Second request should be ignored 227 expect(result2, false); 228 229 // First request should complete normally 230 final result1 = await future1; 231 expect(result1, true); 232 233 // Should have only called API once 234 verify( 235 mockVoteService.createVote( 236 postUri: anyNamed('postUri'), 237 postCid: anyNamed('postCid'), 238 direction: anyNamed('direction'), 239 ), 240 ).called(1); 241 }); 242 243 test('should handle downvote direction', () async { 244 when( 245 mockVoteService.createVote( 246 postUri: anyNamed('postUri'), 247 postCid: anyNamed('postCid'), 248 direction: anyNamed('direction'), 249 ), 250 ).thenAnswer( 251 (_) async => const VoteResponse( 252 uri: 'at://did:plc:test/social.coves.feed.vote/456', 253 cid: 'bafy123', 254 rkey: '456', 255 deleted: false, 256 ), 257 ); 258 259 await voteProvider.toggleVote( 260 postUri: testPostUri, 261 postCid: testPostCid, 262 direction: 'down', 263 ); 264 265 final voteState = voteProvider.getVoteState(testPostUri); 266 expect(voteState?.direction, 'down'); 267 expect(voteState?.deleted, false); 268 269 // Should not be "liked" (isLiked checks for 'up' direction) 270 expect(voteProvider.isLiked(testPostUri), false); 271 }); 272 }); 273 274 group('setInitialVoteState', () { 275 const testPostUri = 'at://did:plc:test/social.coves.post.record/123'; 276 277 test('should set initial vote state from API data', () { 278 voteProvider.setInitialVoteState( 279 postUri: testPostUri, 280 voteDirection: 'up', 281 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 282 ); 283 284 expect(voteProvider.isLiked(testPostUri), true); 285 286 final voteState = voteProvider.getVoteState(testPostUri); 287 expect(voteState?.direction, 'up'); 288 expect(voteState?.uri, 'at://did:plc:test/social.coves.feed.vote/456'); 289 expect(voteState?.deleted, false); 290 }); 291 292 test('should set initial vote state with "down" direction', () { 293 voteProvider.setInitialVoteState( 294 postUri: testPostUri, 295 voteDirection: 'down', 296 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 297 ); 298 299 // Should not be "liked" (isLiked checks for 'up' direction) 300 expect(voteProvider.isLiked(testPostUri), false); 301 302 final voteState = voteProvider.getVoteState(testPostUri); 303 expect(voteState?.direction, 'down'); 304 expect(voteState?.uri, 'at://did:plc:test/social.coves.feed.vote/456'); 305 expect(voteState?.deleted, false); 306 }); 307 308 test('should extract rkey from voteUri', () { 309 voteProvider.setInitialVoteState( 310 postUri: testPostUri, 311 voteDirection: 'up', 312 voteUri: 'at://did:plc:test/social.coves.feed.vote/3kbyxyz123', 313 ); 314 315 final voteState = voteProvider.getVoteState(testPostUri); 316 expect(voteState?.rkey, '3kbyxyz123'); 317 }); 318 319 test('should handle voteUri being null', () { 320 voteProvider.setInitialVoteState( 321 postUri: testPostUri, 322 voteDirection: 'up', 323 ); 324 325 final voteState = voteProvider.getVoteState(testPostUri); 326 expect(voteState?.direction, 'up'); 327 expect(voteState?.uri, null); 328 expect(voteState?.rkey, null); 329 expect(voteState?.deleted, false); 330 }); 331 332 test('should remove vote state when voteDirection is null', () { 333 // First set a vote 334 voteProvider.setInitialVoteState( 335 postUri: testPostUri, 336 voteDirection: 'up', 337 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 338 ); 339 340 expect(voteProvider.isLiked(testPostUri), true); 341 342 // Then clear it 343 voteProvider.setInitialVoteState(postUri: testPostUri); 344 345 expect(voteProvider.isLiked(testPostUri), false); 346 expect(voteProvider.getVoteState(testPostUri), null); 347 }); 348 349 test('should clear stale vote state when refreshing with null vote', () { 350 // Simulate initial state from previous session 351 voteProvider.setInitialVoteState( 352 postUri: testPostUri, 353 voteDirection: 'up', 354 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 355 ); 356 357 expect(voteProvider.isLiked(testPostUri), true); 358 359 // Simulate refresh where server returns viewer.vote = null 360 // (user removed vote on another device) 361 voteProvider.setInitialVoteState( 362 postUri: testPostUri, 363 voteDirection: null, 364 ); 365 366 // Vote should be cleared 367 expect(voteProvider.isLiked(testPostUri), false); 368 expect(voteProvider.getVoteState(testPostUri), null); 369 }); 370 371 test('should clear stale score adjustment on refresh', () async { 372 // Simulate optimistic upvote that created a +1 adjustment 373 when( 374 mockVoteService.createVote( 375 postUri: anyNamed('postUri'), 376 postCid: anyNamed('postCid'), 377 direction: anyNamed('direction'), 378 ), 379 ).thenAnswer( 380 (_) async => const VoteResponse( 381 uri: 'at://did:plc:test/social.coves.feed.vote/456', 382 cid: 'bafy123', 383 rkey: '456', 384 deleted: false, 385 ), 386 ); 387 388 // Create vote - this sets _scoreAdjustments[testPostUri] = +1 389 await voteProvider.toggleVote( 390 postUri: testPostUri, 391 postCid: 'bafy2bzacepostcid123', 392 ); 393 394 // Verify adjustment exists 395 const serverScore = 10; 396 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 11); 397 398 // Now simulate a feed refresh - server returns fresh score (11) 399 // which already includes the vote. The adjustment should be cleared. 400 voteProvider.setInitialVoteState( 401 postUri: testPostUri, 402 voteDirection: 'up', 403 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 404 ); 405 406 // After refresh, adjustment should be cleared (server score is truth) 407 // If we pass the NEW server score (11), we should get 11, not 12 408 const freshServerScore = 11; 409 expect( 410 voteProvider.getAdjustedScore(testPostUri, freshServerScore), 411 11, 412 ); 413 }); 414 415 test('should not notify listeners when setting initial state', () { 416 var notificationCount = 0; 417 voteProvider 418 ..addListener(() { 419 notificationCount++; 420 }) 421 ..setInitialVoteState( 422 postUri: testPostUri, 423 voteDirection: 'up', 424 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 425 ); 426 427 // Should NOT notify listeners (silent initialization) 428 expect(notificationCount, 0); 429 }); 430 }); 431 432 group('VoteState.extractRkeyFromUri', () { 433 test('should extract rkey from valid AT-URI', () { 434 expect( 435 VoteState.extractRkeyFromUri( 436 'at://did:plc:test/social.coves.feed.vote/3kbyxyz123', 437 ), 438 '3kbyxyz123', 439 ); 440 }); 441 442 test('should return null for null uri', () { 443 expect(VoteState.extractRkeyFromUri(null), null); 444 }); 445 446 test('should handle URI with no path segments', () { 447 expect(VoteState.extractRkeyFromUri(''), ''); 448 }); 449 450 test('should handle complex rkey values', () { 451 expect( 452 VoteState.extractRkeyFromUri( 453 'at://did:plc:abc123xyz/social.coves.feed.vote/3lbp7kw2abc', 454 ), 455 '3lbp7kw2abc', 456 ); 457 }); 458 }); 459 460 group('clear', () { 461 test('should clear all vote state', () { 462 const post1 = 'at://did:plc:test/social.coves.post.record/1'; 463 const post2 = 'at://did:plc:test/social.coves.post.record/2'; 464 465 // Set up multiple votes 466 voteProvider 467 ..setInitialVoteState( 468 postUri: post1, 469 voteDirection: 'up', 470 voteUri: 'at://did:plc:test/social.coves.feed.vote/1', 471 ) 472 ..setInitialVoteState( 473 postUri: post2, 474 voteDirection: 'up', 475 voteUri: 'at://did:plc:test/social.coves.feed.vote/2', 476 ); 477 478 expect(voteProvider.isLiked(post1), true); 479 expect(voteProvider.isLiked(post2), true); 480 481 // Clear all 482 voteProvider.clear(); 483 484 // Should have no votes 485 expect(voteProvider.isLiked(post1), false); 486 expect(voteProvider.isLiked(post2), false); 487 expect(voteProvider.getVoteState(post1), null); 488 expect(voteProvider.getVoteState(post2), null); 489 }); 490 491 test('should notify listeners when cleared', () { 492 var notificationCount = 0; 493 voteProvider 494 ..addListener(() { 495 notificationCount++; 496 }) 497 ..clear(); 498 499 expect(notificationCount, 1); 500 }); 501 }); 502 503 group('isPending', () { 504 const testPostUri = 'at://did:plc:test/social.coves.post.record/123'; 505 const testPostCid = 'bafy2bzacepostcid123'; 506 507 test('should return true while request is in progress', () async { 508 // Mock slow API response 509 when( 510 mockVoteService.createVote( 511 postUri: anyNamed('postUri'), 512 postCid: anyNamed('postCid'), 513 direction: anyNamed('direction'), 514 ), 515 ).thenAnswer((_) async { 516 await Future.delayed(const Duration(milliseconds: 50)); 517 return const VoteResponse( 518 uri: 'at://did:plc:test/social.coves.feed.vote/456', 519 cid: 'bafy123', 520 rkey: '456', 521 deleted: false, 522 ); 523 }); 524 525 expect(voteProvider.isPending(testPostUri), false); 526 527 // Start request 528 final future = voteProvider.toggleVote( 529 postUri: testPostUri, 530 postCid: testPostCid, 531 ); 532 533 // Give it time to set pending flag 534 await Future.delayed(const Duration(milliseconds: 10)); 535 536 // Should be pending now 537 expect(voteProvider.isPending(testPostUri), true); 538 539 // Wait for completion 540 await future; 541 542 // Should not be pending anymore 543 expect(voteProvider.isPending(testPostUri), false); 544 }); 545 546 test('should return false for posts with no pending request', () { 547 const testPostUri = 'at://did:plc:test/social.coves.post.record/123'; 548 expect(voteProvider.isPending(testPostUri), false); 549 }); 550 }); 551 552 group('Score adjustments', () { 553 const testPostUri = 'at://did:plc:test/social.coves.post.record/123'; 554 const testPostCid = 'bafy2bzacepostcid123'; 555 556 test('should adjust score when creating upvote', () async { 557 when( 558 mockVoteService.createVote( 559 postUri: anyNamed('postUri'), 560 postCid: anyNamed('postCid'), 561 direction: anyNamed('direction'), 562 ), 563 ).thenAnswer( 564 (_) async => const VoteResponse( 565 uri: 'at://did:plc:test/social.coves.feed.vote/456', 566 cid: 'bafy123', 567 rkey: '456', 568 deleted: false, 569 ), 570 ); 571 572 // Initial score from server 573 const serverScore = 10; 574 575 // Before vote, adjustment should be 0 576 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 10); 577 578 // Create upvote 579 await voteProvider.toggleVote( 580 postUri: testPostUri, 581 postCid: testPostCid, 582 ); 583 584 // Should have +1 adjustment (upvote added) 585 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 11); 586 }); 587 588 test('should adjust score when removing upvote', () async { 589 // Set initial state with upvote 590 voteProvider.setInitialVoteState( 591 postUri: testPostUri, 592 voteDirection: 'up', 593 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 594 ); 595 596 when( 597 mockVoteService.createVote( 598 postUri: anyNamed('postUri'), 599 postCid: anyNamed('postCid'), 600 direction: anyNamed('direction'), 601 ), 602 ).thenAnswer((_) async => const VoteResponse(deleted: true)); 603 604 const serverScore = 10; 605 606 // Before removing, adjustment should be 0 (server knows about upvote) 607 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 10); 608 609 // Remove upvote 610 await voteProvider.toggleVote( 611 postUri: testPostUri, 612 postCid: testPostCid, 613 ); 614 615 // Should have -1 adjustment (upvote removed) 616 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 9); 617 }); 618 619 test('should adjust score when creating downvote', () async { 620 when( 621 mockVoteService.createVote( 622 postUri: anyNamed('postUri'), 623 postCid: anyNamed('postCid'), 624 direction: anyNamed('direction'), 625 ), 626 ).thenAnswer( 627 (_) async => const VoteResponse( 628 uri: 'at://did:plc:test/social.coves.feed.vote/456', 629 cid: 'bafy123', 630 rkey: '456', 631 deleted: false, 632 ), 633 ); 634 635 const serverScore = 10; 636 637 // Create downvote 638 await voteProvider.toggleVote( 639 postUri: testPostUri, 640 postCid: testPostCid, 641 direction: 'down', 642 ); 643 644 // Should have -1 adjustment (downvote added) 645 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 9); 646 }); 647 648 test( 649 'should adjust score when switching from upvote to downvote', 650 () async { 651 // Set initial state with upvote 652 voteProvider.setInitialVoteState( 653 postUri: testPostUri, 654 voteDirection: 'up', 655 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 656 ); 657 658 when( 659 mockVoteService.createVote( 660 postUri: anyNamed('postUri'), 661 postCid: anyNamed('postCid'), 662 direction: anyNamed('direction'), 663 ), 664 ).thenAnswer( 665 (_) async => const VoteResponse( 666 uri: 'at://did:plc:test/social.coves.feed.vote/789', 667 cid: 'bafy789', 668 rkey: '789', 669 deleted: false, 670 ), 671 ); 672 673 const serverScore = 10; 674 675 // Switch to downvote 676 await voteProvider.toggleVote( 677 postUri: testPostUri, 678 postCid: testPostCid, 679 direction: 'down', 680 ); 681 682 // Should have -2 adjustment (remove +1, add -1) 683 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 8); 684 }, 685 ); 686 687 test( 688 'should adjust score when switching from downvote to upvote', 689 () async { 690 // Set initial state with downvote 691 voteProvider.setInitialVoteState( 692 postUri: testPostUri, 693 voteDirection: 'down', 694 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 695 ); 696 697 when( 698 mockVoteService.createVote( 699 postUri: anyNamed('postUri'), 700 postCid: anyNamed('postCid'), 701 direction: anyNamed('direction'), 702 ), 703 ).thenAnswer( 704 (_) async => const VoteResponse( 705 uri: 'at://did:plc:test/social.coves.feed.vote/789', 706 cid: 'bafy789', 707 rkey: '789', 708 deleted: false, 709 ), 710 ); 711 712 const serverScore = 10; 713 714 // Switch to upvote 715 await voteProvider.toggleVote( 716 postUri: testPostUri, 717 postCid: testPostCid, 718 ); 719 720 // Should have +2 adjustment (remove -1, add +1) 721 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 12); 722 }, 723 ); 724 725 test('should rollback score adjustment on error', () async { 726 const serverScore = 10; 727 728 when( 729 mockVoteService.createVote( 730 postUri: anyNamed('postUri'), 731 postCid: anyNamed('postCid'), 732 direction: anyNamed('direction'), 733 ), 734 ).thenThrow(ApiException('Network error', statusCode: 500)); 735 736 // Try to vote (will fail) 737 expect( 738 () => voteProvider.toggleVote( 739 postUri: testPostUri, 740 postCid: testPostCid, 741 ), 742 throwsA(isA<ApiException>()), 743 ); 744 745 await Future.delayed(Duration.zero); 746 747 // Adjustment should be rolled back to 0 748 expect(voteProvider.getAdjustedScore(testPostUri, serverScore), 10); 749 }); 750 751 test('should clear score adjustments when clearing all state', () { 752 const testPostUri1 = 'at://did:plc:test/social.coves.post.record/1'; 753 const testPostUri2 = 'at://did:plc:test/social.coves.post.record/2'; 754 755 // Manually set some adjustments (simulating votes) 756 voteProvider 757 ..setInitialVoteState( 758 postUri: testPostUri1, 759 voteDirection: 'up', 760 voteUri: 'at://did:plc:test/social.coves.feed.vote/1', 761 ) 762 ..clear(); 763 764 // Adjustments should be cleared (back to 0) 765 expect(voteProvider.getAdjustedScore(testPostUri1, 10), 10); 766 expect(voteProvider.getAdjustedScore(testPostUri2, 5), 5); 767 }); 768 }); 769 770 group('Auth state listener', () { 771 test('should clear votes when user signs out', () { 772 const testPostUri = 'at://did:plc:test/social.coves.post.record/123'; 773 774 // Set up vote state 775 voteProvider.setInitialVoteState( 776 postUri: testPostUri, 777 voteDirection: 'up', 778 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 779 ); 780 781 expect(voteProvider.isLiked(testPostUri), true); 782 783 // Simulate sign out by changing auth state 784 when(mockAuthProvider.isAuthenticated).thenReturn(false); 785 786 // Trigger the auth listener by calling it directly 787 // (In real app, this would be triggered by 788 // AuthProvider.notifyListeners) 789 voteProvider.clear(); 790 791 // Votes should be cleared 792 expect(voteProvider.isLiked(testPostUri), false); 793 expect(voteProvider.getVoteState(testPostUri), null); 794 }); 795 796 test('should not clear votes when user is still authenticated', () { 797 const testPostUri = 'at://did:plc:test/social.coves.post.record/123'; 798 799 // Set up vote state 800 voteProvider.setInitialVoteState( 801 postUri: testPostUri, 802 voteDirection: 'up', 803 voteUri: 'at://did:plc:test/social.coves.feed.vote/456', 804 ); 805 806 expect(voteProvider.isLiked(testPostUri), true); 807 808 // Auth state remains authenticated 809 when(mockAuthProvider.isAuthenticated).thenReturn(true); 810 811 // Votes should NOT be cleared 812 expect(voteProvider.isLiked(testPostUri), true); 813 }); 814 }); 815 }); 816}