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(() => CovesSession.fromJson(json), throwsA(isA<TypeError>())); 237 }); 238 239 test('should throw when did has wrong type', () { 240 final json = { 241 'token': 'abc123', 242 'did': 123, // Should be String 243 'session_id': 'sess456', 244 }; 245 246 expect(() => CovesSession.fromJson(json), throwsA(isA<TypeError>())); 247 }); 248 249 test('should throw when session_id has wrong type', () { 250 final json = { 251 'token': 'abc123', 252 'did': 'did:plc:test123', 253 'session_id': 123, // Should be String 254 }; 255 256 expect(() => CovesSession.fromJson(json), throwsA(isA<TypeError>())); 257 }); 258 259 test('should throw when token field is missing', () { 260 final json = {'did': 'did:plc:test123', 'session_id': 'sess456'}; 261 262 expect(() => CovesSession.fromJson(json), throwsA(isA<TypeError>())); 263 }); 264 265 test('should throw when did field is missing', () { 266 final json = {'token': 'abc123', 'session_id': 'sess456'}; 267 268 expect(() => CovesSession.fromJson(json), throwsA(isA<TypeError>())); 269 }); 270 271 test('should throw when session_id field is missing', () { 272 final json = {'token': 'abc123', 'did': 'did:plc:test123'}; 273 274 expect(() => CovesSession.fromJson(json), throwsA(isA<TypeError>())); 275 }); 276 277 test('should handle extra fields in JSON', () { 278 final json = { 279 'token': 'abc123', 280 'did': 'did:plc:test123', 281 'session_id': 'sess456', 282 'extra_field': 'ignored', 283 'another_field': 123, 284 }; 285 286 final session = CovesSession.fromJson(json); 287 288 expect(session.token, 'abc123'); 289 expect(session.did, 'did:plc:test123'); 290 expect(session.sessionId, 'sess456'); 291 }); 292 }); 293 294 group('CovesSession.fromJsonString()', () { 295 test('should parse valid JSON string', () { 296 final jsonString = jsonEncode({ 297 'token': 'abc123', 298 'did': 'did:plc:test123', 299 'session_id': 'sess456', 300 'handle': 'test.user', 301 }); 302 303 final session = CovesSession.fromJsonString(jsonString); 304 305 expect(session.token, 'abc123'); 306 expect(session.did, 'did:plc:test123'); 307 expect(session.sessionId, 'sess456'); 308 expect(session.handle, 'test.user'); 309 }); 310 311 test('should parse valid JSON string without handle', () { 312 final jsonString = jsonEncode({ 313 'token': 'abc123', 314 'did': 'did:plc:test123', 315 'session_id': 'sess456', 316 }); 317 318 final session = CovesSession.fromJsonString(jsonString); 319 320 expect(session.token, 'abc123'); 321 expect(session.did, 'did:plc:test123'); 322 expect(session.sessionId, 'sess456'); 323 expect(session.handle, null); 324 }); 325 326 test('should throw on invalid JSON string', () { 327 const invalidJson = '{invalid json}'; 328 329 expect( 330 () => CovesSession.fromJsonString(invalidJson), 331 throwsA(isA<FormatException>()), 332 ); 333 }); 334 335 test('should throw on empty string', () { 336 const emptyString = ''; 337 338 expect( 339 () => CovesSession.fromJsonString(emptyString), 340 throwsA(isA<FormatException>()), 341 ); 342 }); 343 344 test('should throw on non-JSON string', () { 345 const notJson = 'not a json string'; 346 347 expect( 348 () => CovesSession.fromJsonString(notJson), 349 throwsA(isA<FormatException>()), 350 ); 351 }); 352 353 test('should throw on JSON array instead of object', () { 354 const jsonArray = '["token", "did", "session_id"]'; 355 356 expect( 357 () => CovesSession.fromJsonString(jsonArray), 358 throwsA(isA<TypeError>()), 359 ); 360 }); 361 362 test('should throw on null JSON', () { 363 const nullJson = 'null'; 364 365 expect( 366 () => CovesSession.fromJsonString(nullJson), 367 throwsA(isA<TypeError>()), 368 ); 369 }); 370 }); 371 372 group('toJson() / toJsonString()', () { 373 test('should serialize to JSON with all fields', () { 374 const session = CovesSession( 375 token: 'abc123', 376 did: 'did:plc:test123', 377 sessionId: 'sess456', 378 handle: 'test.user', 379 ); 380 381 final json = session.toJson(); 382 383 expect(json['token'], 'abc123'); 384 expect(json['did'], 'did:plc:test123'); 385 expect(json['session_id'], 'sess456'); 386 expect(json['handle'], 'test.user'); 387 }); 388 389 test('should serialize to JSON without handle when null', () { 390 const session = CovesSession( 391 token: 'abc123', 392 did: 'did:plc:test123', 393 sessionId: 'sess456', 394 ); 395 396 final json = session.toJson(); 397 398 expect(json['token'], 'abc123'); 399 expect(json['did'], 'did:plc:test123'); 400 expect(json['session_id'], 'sess456'); 401 expect(json.containsKey('handle'), false); 402 }); 403 404 test('should serialize to JSON string', () { 405 const session = CovesSession( 406 token: 'abc123', 407 did: 'did:plc:test123', 408 sessionId: 'sess456', 409 handle: 'test.user', 410 ); 411 412 final jsonString = session.toJsonString(); 413 final decoded = jsonDecode(jsonString) as Map<String, dynamic>; 414 415 expect(decoded['token'], 'abc123'); 416 expect(decoded['did'], 'did:plc:test123'); 417 expect(decoded['session_id'], 'sess456'); 418 expect(decoded['handle'], 'test.user'); 419 }); 420 421 test('should round-trip: create, serialize, deserialize, compare', () { 422 const original = CovesSession( 423 token: 'abc123', 424 did: 'did:plc:test123', 425 sessionId: 'sess456', 426 handle: 'test.user', 427 ); 428 429 final json = original.toJson(); 430 final restored = CovesSession.fromJson(json); 431 432 expect(restored.token, original.token); 433 expect(restored.did, original.did); 434 expect(restored.sessionId, original.sessionId); 435 expect(restored.handle, original.handle); 436 }); 437 438 test('should round-trip with JSON string', () { 439 const original = CovesSession( 440 token: 'abc123', 441 did: 'did:plc:test123', 442 sessionId: 'sess456', 443 handle: 'test.user', 444 ); 445 446 final jsonString = original.toJsonString(); 447 final restored = CovesSession.fromJsonString(jsonString); 448 449 expect(restored.token, original.token); 450 expect(restored.did, original.did); 451 expect(restored.sessionId, original.sessionId); 452 expect(restored.handle, original.handle); 453 }); 454 455 test('should round-trip without handle', () { 456 const original = CovesSession( 457 token: 'abc123', 458 did: 'did:plc:test123', 459 sessionId: 'sess456', 460 ); 461 462 final json = original.toJson(); 463 final restored = CovesSession.fromJson(json); 464 465 expect(restored.token, original.token); 466 expect(restored.did, original.did); 467 expect(restored.sessionId, original.sessionId); 468 expect(restored.handle, null); 469 }); 470 471 test('should handle special characters in serialization', () { 472 const session = CovesSession( 473 token: 'token+with/special=chars', 474 did: 'did:plc:test123', 475 sessionId: 'sess456', 476 handle: 'user.with.dots', 477 ); 478 479 final jsonString = session.toJsonString(); 480 final restored = CovesSession.fromJsonString(jsonString); 481 482 expect(restored.token, session.token); 483 expect(restored.handle, session.handle); 484 }); 485 }); 486 487 group('copyWithToken()', () { 488 test('should create new session with updated token', () { 489 const original = CovesSession( 490 token: 'old_token', 491 did: 'did:plc:test123', 492 sessionId: 'sess456', 493 handle: 'test.user', 494 ); 495 496 final updated = original.copyWithToken('new_token'); 497 498 expect(updated.token, 'new_token'); 499 expect(updated.did, original.did); 500 expect(updated.sessionId, original.sessionId); 501 expect(updated.handle, original.handle); 502 }); 503 504 test('should preserve null handle when copying with new token', () { 505 const original = CovesSession( 506 token: 'old_token', 507 did: 'did:plc:test123', 508 sessionId: 'sess456', 509 ); 510 511 final updated = original.copyWithToken('new_token'); 512 513 expect(updated.token, 'new_token'); 514 expect(updated.did, original.did); 515 expect(updated.sessionId, original.sessionId); 516 expect(updated.handle, null); 517 }); 518 519 test('should not modify original session', () { 520 const original = CovesSession( 521 token: 'old_token', 522 did: 'did:plc:test123', 523 sessionId: 'sess456', 524 handle: 'test.user', 525 ); 526 527 final updated = original.copyWithToken('new_token'); 528 529 expect(original.token, 'old_token'); 530 expect(updated.token, 'new_token'); 531 }); 532 533 test('should handle empty string token', () { 534 const original = CovesSession( 535 token: 'old_token', 536 did: 'did:plc:test123', 537 sessionId: 'sess456', 538 ); 539 540 final updated = original.copyWithToken(''); 541 542 expect(updated.token, ''); 543 expect(updated.did, original.did); 544 }); 545 546 test('should handle complex token values', () { 547 const original = CovesSession( 548 token: 'old_token', 549 did: 'did:plc:test123', 550 sessionId: 'sess456', 551 ); 552 553 const newToken = 554 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U'; 555 final updated = original.copyWithToken(newToken); 556 557 expect(updated.token, newToken); 558 }); 559 }); 560 561 group('toString()', () { 562 test('should not expose token in string representation', () { 563 const session = CovesSession( 564 token: 'secret_token_abc123', 565 did: 'did:plc:test123', 566 sessionId: 'sess456', 567 handle: 'test.user', 568 ); 569 570 final stringRep = session.toString(); 571 572 expect(stringRep, isNot(contains('secret_token_abc123'))); 573 expect(stringRep, isNot(contains('token'))); 574 }); 575 576 test('should include did in string representation', () { 577 const session = CovesSession( 578 token: 'secret_token', 579 did: 'did:plc:test123', 580 sessionId: 'sess456', 581 handle: 'test.user', 582 ); 583 584 final stringRep = session.toString(); 585 586 expect(stringRep, contains('did:plc:test123')); 587 }); 588 589 test('should include handle in string representation', () { 590 const session = CovesSession( 591 token: 'secret_token', 592 did: 'did:plc:test123', 593 sessionId: 'sess456', 594 handle: 'test.user', 595 ); 596 597 final stringRep = session.toString(); 598 599 expect(stringRep, contains('test.user')); 600 }); 601 602 test('should include sessionId in string representation', () { 603 const session = CovesSession( 604 token: 'secret_token', 605 did: 'did:plc:test123', 606 sessionId: 'sess456', 607 handle: 'test.user', 608 ); 609 610 final stringRep = session.toString(); 611 612 expect(stringRep, contains('sess456')); 613 }); 614 615 test('should handle null handle in string representation', () { 616 const session = CovesSession( 617 token: 'secret_token', 618 did: 'did:plc:test123', 619 sessionId: 'sess456', 620 ); 621 622 final stringRep = session.toString(); 623 624 expect(stringRep, contains('did:plc:test123')); 625 expect(stringRep, contains('sess456')); 626 expect(stringRep, contains('null')); 627 }); 628 629 test('should follow expected format', () { 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( 640 stringRep, 641 'CovesSession(did: did:plc:test123, handle: test.user, sessionId: sess456)', 642 ); 643 }); 644 }); 645 646 group('Edge cases', () { 647 test('should handle very long token values', () { 648 final longToken = 'a' * 10000; 649 final session = CovesSession( 650 token: longToken, 651 did: 'did:plc:test123', 652 sessionId: 'sess456', 653 ); 654 655 expect(session.token.length, 10000); 656 657 final json = session.toJson(); 658 final restored = CovesSession.fromJson(json); 659 660 expect(restored.token, longToken); 661 }); 662 663 test('should handle unicode characters in handle', () { 664 const session = CovesSession( 665 token: 'abc123', 666 did: 'did:plc:test123', 667 sessionId: 'sess456', 668 handle: 'test.用户.bsky.social', 669 ); 670 671 final json = session.toJson(); 672 final restored = CovesSession.fromJson(json); 673 674 expect(restored.handle, 'test.用户.bsky.social'); 675 }); 676 677 test('should handle DID with different methods', () { 678 const session = CovesSession( 679 token: 'abc123', 680 did: 'did:web:example.com', 681 sessionId: 'sess456', 682 ); 683 684 final json = session.toJson(); 685 final restored = CovesSession.fromJson(json); 686 687 expect(restored.did, 'did:web:example.com'); 688 }); 689 690 test('should handle session with colons in sessionId', () { 691 const session = CovesSession( 692 token: 'abc123', 693 did: 'did:plc:test123', 694 sessionId: 'sess:456:789', 695 ); 696 697 final json = session.toJson(); 698 final restored = CovesSession.fromJson(json); 699 700 expect(restored.sessionId, 'sess:456:789'); 701 }); 702 703 test('should handle empty handle string', () { 704 const session = CovesSession( 705 token: 'abc123', 706 did: 'did:plc:test123', 707 sessionId: 'sess456', 708 handle: '', 709 ); 710 711 final json = session.toJson(); 712 713 expect(json['handle'], ''); 714 }); 715 716 test('should handle whitespace in token from callback URI', () { 717 final uri = Uri.parse( 718 'social.coves:/callback?token=%20abc123%20&did=did:plc:test123&session_id=sess456', 719 ); 720 721 final session = CovesSession.fromCallbackUri(uri); 722 723 expect(session.token, ' abc123 '); 724 }); 725 726 test('should handle multiple URL encoding passes', () { 727 // Token that's been double-encoded 728 final uri = Uri.parse( 729 'social.coves:/callback?token=abc%252B123&did=did:plc:test123&session_id=sess456', 730 ); 731 732 final session = CovesSession.fromCallbackUri(uri); 733 734 // Uri.queryParameters decodes once, Uri.decodeComponent decodes again 735 expect(session.token, 'abc+123'); 736 }); 737 }); 738 739 group('Security', () { 740 test('toString should not leak sensitive token data', () { 741 const session = CovesSession( 742 token: 'super_secret_encrypted_token_12345', 743 did: 'did:plc:test123', 744 sessionId: 'sess456', 745 handle: 'test.user', 746 ); 747 748 final stringRep = session.toString(); 749 750 // Verify the entire token is not present 751 expect(stringRep, isNot(contains('super_secret_encrypted_token_12345'))); 752 // Verify even partial token data is not present 753 expect(stringRep, isNot(contains('secret'))); 754 expect(stringRep, isNot(contains('encrypted'))); 755 expect(stringRep, isNot(contains('12345'))); 756 }); 757 758 test('toString should be safe for logging', () { 759 const session = CovesSession( 760 token: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', 761 did: 'did:plc:test123', 762 sessionId: 'sess456', 763 handle: 'test.user', 764 ); 765 766 final stringRep = session.toString(); 767 768 expect(stringRep, isNot(contains('Bearer'))); 769 expect( 770 stringRep, 771 isNot(contains('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')), 772 ); 773 }); 774 }); 775}