Main coves client
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}