1import 'dart:convert'; 2 3import 'package:coves_flutter/models/coves_session.dart'; 4import 'package:flutter_test/flutter_test.dart'; 5 6void main() { 7 group('CovesSession.fromCallbackUri()', () { 8 test('should parse valid URI with all parameters', () { 9 final uri = Uri.parse( 10 'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=sess456&handle=test.user', 11 ); 12 13 final session = CovesSession.fromCallbackUri(uri); 14 15 expect(session.token, 'abc123'); 16 expect(session.did, 'did:plc:test123'); 17 expect(session.sessionId, 'sess456'); 18 expect(session.handle, 'test.user'); 19 }); 20 21 test('should parse valid URI without optional handle', () { 22 final uri = Uri.parse( 23 'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=sess456', 24 ); 25 26 final session = CovesSession.fromCallbackUri(uri); 27 28 expect(session.token, 'abc123'); 29 expect(session.did, 'did:plc:test123'); 30 expect(session.sessionId, 'sess456'); 31 expect(session.handle, null); 32 }); 33 34 test('should throw FormatException when token is missing', () { 35 final uri = Uri.parse( 36 'social.coves:/callback?did=did:plc:test123&session_id=sess456', 37 ); 38 39 expect( 40 () => CovesSession.fromCallbackUri(uri), 41 throwsA( 42 isA<FormatException>().having( 43 (e) => e.message, 44 'message', 45 'Missing required parameter: token', 46 ), 47 ), 48 ); 49 }); 50 51 test('should throw FormatException when did is missing', () { 52 final uri = Uri.parse( 53 'social.coves:/callback?token=abc123&session_id=sess456', 54 ); 55 56 expect( 57 () => CovesSession.fromCallbackUri(uri), 58 throwsA( 59 isA<FormatException>().having( 60 (e) => e.message, 61 'message', 62 'Missing required parameter: did', 63 ), 64 ), 65 ); 66 }); 67 68 test('should throw FormatException when session_id is missing', () { 69 final uri = Uri.parse( 70 'social.coves:/callback?token=abc123&did=did:plc:test123', 71 ); 72 73 expect( 74 () => CovesSession.fromCallbackUri(uri), 75 throwsA( 76 isA<FormatException>().having( 77 (e) => e.message, 78 'message', 79 'Missing required parameter: session_id', 80 ), 81 ), 82 ); 83 }); 84 85 test('should throw FormatException when token is empty', () { 86 final uri = Uri.parse( 87 'social.coves:/callback?token=&did=did:plc:test123&session_id=sess456', 88 ); 89 90 expect( 91 () => CovesSession.fromCallbackUri(uri), 92 throwsA( 93 isA<FormatException>().having( 94 (e) => e.message, 95 'message', 96 'Missing required parameter: token', 97 ), 98 ), 99 ); 100 }); 101 102 test('should throw FormatException when did is empty', () { 103 final uri = Uri.parse( 104 'social.coves:/callback?token=abc123&did=&session_id=sess456', 105 ); 106 107 expect( 108 () => CovesSession.fromCallbackUri(uri), 109 throwsA( 110 isA<FormatException>().having( 111 (e) => e.message, 112 'message', 113 'Missing required parameter: did', 114 ), 115 ), 116 ); 117 }); 118 119 test('should throw FormatException when session_id is empty', () { 120 final uri = Uri.parse( 121 'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=', 122 ); 123 124 expect( 125 () => CovesSession.fromCallbackUri(uri), 126 throwsA( 127 isA<FormatException>().having( 128 (e) => e.message, 129 'message', 130 'Missing required parameter: session_id', 131 ), 132 ), 133 ); 134 }); 135 136 test('should decode URL-encoded token values', () { 137 final uri = Uri.parse( 138 'social.coves:/callback?token=abc%2B123%2F456%3D&did=did:plc:test123&session_id=sess456', 139 ); 140 141 final session = CovesSession.fromCallbackUri(uri); 142 143 expect(session.token, 'abc+123/456='); 144 expect(session.did, 'did:plc:test123'); 145 expect(session.sessionId, 'sess456'); 146 }); 147 148 test('should handle URL-encoded spaces in token', () { 149 final uri = Uri.parse( 150 'social.coves:/callback?token=token%20with%20spaces&did=did:plc:test123&session_id=sess456', 151 ); 152 153 final session = CovesSession.fromCallbackUri(uri); 154 155 expect(session.token, 'token with spaces'); 156 }); 157 158 test('should ignore extra/unknown parameters', () { 159 final uri = Uri.parse( 160 'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=sess456&extra=ignored&unknown=also_ignored', 161 ); 162 163 final session = CovesSession.fromCallbackUri(uri); 164 165 expect(session.token, 'abc123'); 166 expect(session.did, 'did:plc:test123'); 167 expect(session.sessionId, 'sess456'); 168 }); 169 170 test('should handle complex token values', () { 171 final uri = Uri.parse( 172 'social.coves:/callback?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U&did=did:plc:test123&session_id=sess456', 173 ); 174 175 final session = CovesSession.fromCallbackUri(uri); 176 177 expect( 178 session.token, 179 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U', 180 ); 181 }); 182 }); 183 184 group('CovesSession.fromJson()', () { 185 test('should parse valid JSON with all fields', () { 186 final json = { 187 'token': 'abc123', 188 'did': 'did:plc:test123', 189 'session_id': 'sess456', 190 'handle': 'test.user', 191 }; 192 193 final session = CovesSession.fromJson(json); 194 195 expect(session.token, 'abc123'); 196 expect(session.did, 'did:plc:test123'); 197 expect(session.sessionId, 'sess456'); 198 expect(session.handle, 'test.user'); 199 }); 200 201 test('should parse valid JSON without optional handle', () { 202 final json = { 203 'token': 'abc123', 204 'did': 'did:plc:test123', 205 'session_id': 'sess456', 206 }; 207 208 final session = CovesSession.fromJson(json); 209 210 expect(session.token, 'abc123'); 211 expect(session.did, 'did:plc:test123'); 212 expect(session.sessionId, 'sess456'); 213 expect(session.handle, null); 214 }); 215 216 test('should parse JSON with null handle', () { 217 final json = { 218 'token': 'abc123', 219 'did': 'did:plc:test123', 220 'session_id': 'sess456', 221 'handle': null, 222 }; 223 224 final session = CovesSession.fromJson(json); 225 226 expect(session.handle, null); 227 }); 228 229 test('should throw when token has wrong type', () { 230 final json = { 231 'token': 123, // Should be String 232 'did': 'did:plc:test123', 233 'session_id': 'sess456', 234 }; 235 236 expect( 237 () => CovesSession.fromJson(json), 238 throwsA(isA<TypeError>()), 239 ); 240 }); 241 242 test('should throw when did has wrong type', () { 243 final json = { 244 'token': 'abc123', 245 'did': 123, // Should be String 246 'session_id': 'sess456', 247 }; 248 249 expect( 250 () => CovesSession.fromJson(json), 251 throwsA(isA<TypeError>()), 252 ); 253 }); 254 255 test('should throw when session_id has wrong type', () { 256 final json = { 257 'token': 'abc123', 258 'did': 'did:plc:test123', 259 'session_id': 123, // Should be String 260 }; 261 262 expect( 263 () => CovesSession.fromJson(json), 264 throwsA(isA<TypeError>()), 265 ); 266 }); 267 268 test('should throw when token field is missing', () { 269 final json = { 270 'did': 'did:plc:test123', 271 'session_id': 'sess456', 272 }; 273 274 expect( 275 () => CovesSession.fromJson(json), 276 throwsA(isA<TypeError>()), 277 ); 278 }); 279 280 test('should throw when did field is missing', () { 281 final json = { 282 'token': 'abc123', 283 'session_id': 'sess456', 284 }; 285 286 expect( 287 () => CovesSession.fromJson(json), 288 throwsA(isA<TypeError>()), 289 ); 290 }); 291 292 test('should throw when session_id field is missing', () { 293 final json = { 294 'token': 'abc123', 295 'did': 'did:plc:test123', 296 }; 297 298 expect( 299 () => CovesSession.fromJson(json), 300 throwsA(isA<TypeError>()), 301 ); 302 }); 303 304 test('should handle extra fields in JSON', () { 305 final json = { 306 'token': 'abc123', 307 'did': 'did:plc:test123', 308 'session_id': 'sess456', 309 'extra_field': 'ignored', 310 'another_field': 123, 311 }; 312 313 final session = CovesSession.fromJson(json); 314 315 expect(session.token, 'abc123'); 316 expect(session.did, 'did:plc:test123'); 317 expect(session.sessionId, 'sess456'); 318 }); 319 }); 320 321 group('CovesSession.fromJsonString()', () { 322 test('should parse valid JSON string', () { 323 final jsonString = jsonEncode({ 324 'token': 'abc123', 325 'did': 'did:plc:test123', 326 'session_id': 'sess456', 327 'handle': 'test.user', 328 }); 329 330 final session = CovesSession.fromJsonString(jsonString); 331 332 expect(session.token, 'abc123'); 333 expect(session.did, 'did:plc:test123'); 334 expect(session.sessionId, 'sess456'); 335 expect(session.handle, 'test.user'); 336 }); 337 338 test('should parse valid JSON string without handle', () { 339 final jsonString = jsonEncode({ 340 'token': 'abc123', 341 'did': 'did:plc:test123', 342 'session_id': 'sess456', 343 }); 344 345 final session = CovesSession.fromJsonString(jsonString); 346 347 expect(session.token, 'abc123'); 348 expect(session.did, 'did:plc:test123'); 349 expect(session.sessionId, 'sess456'); 350 expect(session.handle, null); 351 }); 352 353 test('should throw on invalid JSON string', () { 354 const invalidJson = '{invalid json}'; 355 356 expect( 357 () => CovesSession.fromJsonString(invalidJson), 358 throwsA(isA<FormatException>()), 359 ); 360 }); 361 362 test('should throw on empty string', () { 363 const emptyString = ''; 364 365 expect( 366 () => CovesSession.fromJsonString(emptyString), 367 throwsA(isA<FormatException>()), 368 ); 369 }); 370 371 test('should throw on non-JSON string', () { 372 const notJson = 'not a json string'; 373 374 expect( 375 () => CovesSession.fromJsonString(notJson), 376 throwsA(isA<FormatException>()), 377 ); 378 }); 379 380 test('should throw on JSON array instead of object', () { 381 const jsonArray = '["token", "did", "session_id"]'; 382 383 expect( 384 () => CovesSession.fromJsonString(jsonArray), 385 throwsA(isA<TypeError>()), 386 ); 387 }); 388 389 test('should throw on null JSON', () { 390 const nullJson = 'null'; 391 392 expect( 393 () => CovesSession.fromJsonString(nullJson), 394 throwsA(isA<TypeError>()), 395 ); 396 }); 397 }); 398 399 group('toJson() / toJsonString()', () { 400 test('should serialize to JSON with all fields', () { 401 const session = CovesSession( 402 token: 'abc123', 403 did: 'did:plc:test123', 404 sessionId: 'sess456', 405 handle: 'test.user', 406 ); 407 408 final json = session.toJson(); 409 410 expect(json['token'], 'abc123'); 411 expect(json['did'], 'did:plc:test123'); 412 expect(json['session_id'], 'sess456'); 413 expect(json['handle'], 'test.user'); 414 }); 415 416 test('should serialize to JSON without handle when null', () { 417 const session = CovesSession( 418 token: 'abc123', 419 did: 'did:plc:test123', 420 sessionId: 'sess456', 421 ); 422 423 final json = session.toJson(); 424 425 expect(json['token'], 'abc123'); 426 expect(json['did'], 'did:plc:test123'); 427 expect(json['session_id'], 'sess456'); 428 expect(json.containsKey('handle'), false); 429 }); 430 431 test('should serialize to JSON string', () { 432 const session = CovesSession( 433 token: 'abc123', 434 did: 'did:plc:test123', 435 sessionId: 'sess456', 436 handle: 'test.user', 437 ); 438 439 final jsonString = session.toJsonString(); 440 final decoded = jsonDecode(jsonString) as Map<String, dynamic>; 441 442 expect(decoded['token'], 'abc123'); 443 expect(decoded['did'], 'did:plc:test123'); 444 expect(decoded['session_id'], 'sess456'); 445 expect(decoded['handle'], 'test.user'); 446 }); 447 448 test('should round-trip: create, serialize, deserialize, compare', () { 449 const original = CovesSession( 450 token: 'abc123', 451 did: 'did:plc:test123', 452 sessionId: 'sess456', 453 handle: 'test.user', 454 ); 455 456 final json = original.toJson(); 457 final restored = CovesSession.fromJson(json); 458 459 expect(restored.token, original.token); 460 expect(restored.did, original.did); 461 expect(restored.sessionId, original.sessionId); 462 expect(restored.handle, original.handle); 463 }); 464 465 test('should round-trip with JSON string', () { 466 const original = CovesSession( 467 token: 'abc123', 468 did: 'did:plc:test123', 469 sessionId: 'sess456', 470 handle: 'test.user', 471 ); 472 473 final jsonString = original.toJsonString(); 474 final restored = CovesSession.fromJsonString(jsonString); 475 476 expect(restored.token, original.token); 477 expect(restored.did, original.did); 478 expect(restored.sessionId, original.sessionId); 479 expect(restored.handle, original.handle); 480 }); 481 482 test('should round-trip without handle', () { 483 const original = CovesSession( 484 token: 'abc123', 485 did: 'did:plc:test123', 486 sessionId: 'sess456', 487 ); 488 489 final json = original.toJson(); 490 final restored = CovesSession.fromJson(json); 491 492 expect(restored.token, original.token); 493 expect(restored.did, original.did); 494 expect(restored.sessionId, original.sessionId); 495 expect(restored.handle, null); 496 }); 497 498 test('should handle special characters in serialization', () { 499 const session = CovesSession( 500 token: 'token+with/special=chars', 501 did: 'did:plc:test123', 502 sessionId: 'sess456', 503 handle: 'user.with.dots', 504 ); 505 506 final jsonString = session.toJsonString(); 507 final restored = CovesSession.fromJsonString(jsonString); 508 509 expect(restored.token, session.token); 510 expect(restored.handle, session.handle); 511 }); 512 }); 513 514 group('copyWithToken()', () { 515 test('should create new session with updated token', () { 516 const original = CovesSession( 517 token: 'old_token', 518 did: 'did:plc:test123', 519 sessionId: 'sess456', 520 handle: 'test.user', 521 ); 522 523 final updated = original.copyWithToken('new_token'); 524 525 expect(updated.token, 'new_token'); 526 expect(updated.did, original.did); 527 expect(updated.sessionId, original.sessionId); 528 expect(updated.handle, original.handle); 529 }); 530 531 test('should preserve null handle when copying with new token', () { 532 const original = CovesSession( 533 token: 'old_token', 534 did: 'did:plc:test123', 535 sessionId: 'sess456', 536 ); 537 538 final updated = original.copyWithToken('new_token'); 539 540 expect(updated.token, 'new_token'); 541 expect(updated.did, original.did); 542 expect(updated.sessionId, original.sessionId); 543 expect(updated.handle, null); 544 }); 545 546 test('should not modify original session', () { 547 const original = CovesSession( 548 token: 'old_token', 549 did: 'did:plc:test123', 550 sessionId: 'sess456', 551 handle: 'test.user', 552 ); 553 554 final updated = original.copyWithToken('new_token'); 555 556 expect(original.token, 'old_token'); 557 expect(updated.token, 'new_token'); 558 }); 559 560 test('should handle empty string token', () { 561 const original = CovesSession( 562 token: 'old_token', 563 did: 'did:plc:test123', 564 sessionId: 'sess456', 565 ); 566 567 final updated = original.copyWithToken(''); 568 569 expect(updated.token, ''); 570 expect(updated.did, original.did); 571 }); 572 573 test('should handle complex token values', () { 574 const original = CovesSession( 575 token: 'old_token', 576 did: 'did:plc:test123', 577 sessionId: 'sess456', 578 ); 579 580 const newToken = 581 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U'; 582 final updated = original.copyWithToken(newToken); 583 584 expect(updated.token, newToken); 585 }); 586 }); 587 588 group('toString()', () { 589 test('should not expose token in string representation', () { 590 const session = CovesSession( 591 token: 'secret_token_abc123', 592 did: 'did:plc:test123', 593 sessionId: 'sess456', 594 handle: 'test.user', 595 ); 596 597 final stringRep = session.toString(); 598 599 expect(stringRep, isNot(contains('secret_token_abc123'))); 600 expect(stringRep, isNot(contains('token'))); 601 }); 602 603 test('should include did in string representation', () { 604 const session = CovesSession( 605 token: 'secret_token', 606 did: 'did:plc:test123', 607 sessionId: 'sess456', 608 handle: 'test.user', 609 ); 610 611 final stringRep = session.toString(); 612 613 expect(stringRep, contains('did:plc:test123')); 614 }); 615 616 test('should include handle in string representation', () { 617 const session = CovesSession( 618 token: 'secret_token', 619 did: 'did:plc:test123', 620 sessionId: 'sess456', 621 handle: 'test.user', 622 ); 623 624 final stringRep = session.toString(); 625 626 expect(stringRep, contains('test.user')); 627 }); 628 629 test('should include sessionId in string representation', () { 630 const session = CovesSession( 631 token: 'secret_token', 632 did: 'did:plc:test123', 633 sessionId: 'sess456', 634 handle: 'test.user', 635 ); 636 637 final stringRep = session.toString(); 638 639 expect(stringRep, contains('sess456')); 640 }); 641 642 test('should handle null handle in string representation', () { 643 const session = CovesSession( 644 token: 'secret_token', 645 did: 'did:plc:test123', 646 sessionId: 'sess456', 647 ); 648 649 final stringRep = session.toString(); 650 651 expect(stringRep, contains('did:plc:test123')); 652 expect(stringRep, contains('sess456')); 653 expect(stringRep, contains('null')); 654 }); 655 656 test('should follow expected format', () { 657 const session = CovesSession( 658 token: 'secret_token', 659 did: 'did:plc:test123', 660 sessionId: 'sess456', 661 handle: 'test.user', 662 ); 663 664 final stringRep = session.toString(); 665 666 expect( 667 stringRep, 668 'CovesSession(did: did:plc:test123, handle: test.user, sessionId: sess456)', 669 ); 670 }); 671 }); 672 673 group('Edge cases', () { 674 test('should handle very long token values', () { 675 final longToken = 'a' * 10000; 676 final session = CovesSession( 677 token: longToken, 678 did: 'did:plc:test123', 679 sessionId: 'sess456', 680 ); 681 682 expect(session.token.length, 10000); 683 684 final json = session.toJson(); 685 final restored = CovesSession.fromJson(json); 686 687 expect(restored.token, longToken); 688 }); 689 690 test('should handle unicode characters in handle', () { 691 const session = CovesSession( 692 token: 'abc123', 693 did: 'did:plc:test123', 694 sessionId: 'sess456', 695 handle: 'test.用户.bsky.social', 696 ); 697 698 final json = session.toJson(); 699 final restored = CovesSession.fromJson(json); 700 701 expect(restored.handle, 'test.用户.bsky.social'); 702 }); 703 704 test('should handle DID with different methods', () { 705 const session = CovesSession( 706 token: 'abc123', 707 did: 'did:web:example.com', 708 sessionId: 'sess456', 709 ); 710 711 final json = session.toJson(); 712 final restored = CovesSession.fromJson(json); 713 714 expect(restored.did, 'did:web:example.com'); 715 }); 716 717 test('should handle session with colons in sessionId', () { 718 const session = CovesSession( 719 token: 'abc123', 720 did: 'did:plc:test123', 721 sessionId: 'sess:456:789', 722 ); 723 724 final json = session.toJson(); 725 final restored = CovesSession.fromJson(json); 726 727 expect(restored.sessionId, 'sess:456:789'); 728 }); 729 730 test('should handle empty handle string', () { 731 const session = CovesSession( 732 token: 'abc123', 733 did: 'did:plc:test123', 734 sessionId: 'sess456', 735 handle: '', 736 ); 737 738 final json = session.toJson(); 739 740 expect(json['handle'], ''); 741 }); 742 743 test('should handle whitespace in token from callback URI', () { 744 final uri = Uri.parse( 745 'social.coves:/callback?token=%20abc123%20&did=did:plc:test123&session_id=sess456', 746 ); 747 748 final session = CovesSession.fromCallbackUri(uri); 749 750 expect(session.token, ' abc123 '); 751 }); 752 753 test('should handle multiple URL encoding passes', () { 754 // Token that's been double-encoded 755 final uri = Uri.parse( 756 'social.coves:/callback?token=abc%252B123&did=did:plc:test123&session_id=sess456', 757 ); 758 759 final session = CovesSession.fromCallbackUri(uri); 760 761 // Uri.queryParameters decodes once, Uri.decodeComponent decodes again 762 expect(session.token, 'abc+123'); 763 }); 764 }); 765 766 group('Security', () { 767 test('toString should not leak sensitive token data', () { 768 const session = CovesSession( 769 token: 'super_secret_encrypted_token_12345', 770 did: 'did:plc:test123', 771 sessionId: 'sess456', 772 handle: 'test.user', 773 ); 774 775 final stringRep = session.toString(); 776 777 // Verify the entire token is not present 778 expect(stringRep, isNot(contains('super_secret_encrypted_token_12345'))); 779 // Verify even partial token data is not present 780 expect(stringRep, isNot(contains('secret'))); 781 expect(stringRep, isNot(contains('encrypted'))); 782 expect(stringRep, isNot(contains('12345'))); 783 }); 784 785 test('toString should be safe for logging', () { 786 const session = CovesSession( 787 token: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', 788 did: 'did:plc:test123', 789 sessionId: 'sess456', 790 handle: 'test.user', 791 ); 792 793 final stringRep = session.toString(); 794 795 expect(stringRep, isNot(contains('Bearer'))); 796 expect(stringRep, isNot(contains('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))); 797 }); 798 }); 799}