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