···
1
+
import 'dart:convert';
3
+
import 'package:coves_flutter/models/coves_session.dart';
4
+
import 'package:flutter_test/flutter_test.dart';
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',
13
+
final session = CovesSession.fromCallbackUri(uri);
15
+
expect(session.token, 'abc123');
16
+
expect(session.did, 'did:plc:test123');
17
+
expect(session.sessionId, 'sess456');
18
+
expect(session.handle, 'test.user');
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',
26
+
final session = CovesSession.fromCallbackUri(uri);
28
+
expect(session.token, 'abc123');
29
+
expect(session.did, 'did:plc:test123');
30
+
expect(session.sessionId, 'sess456');
31
+
expect(session.handle, null);
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',
40
+
() => CovesSession.fromCallbackUri(uri),
42
+
isA<FormatException>().having(
45
+
'Missing required parameter: token',
51
+
test('should throw FormatException when did is missing', () {
52
+
final uri = Uri.parse(
53
+
'social.coves:/callback?token=abc123&session_id=sess456',
57
+
() => CovesSession.fromCallbackUri(uri),
59
+
isA<FormatException>().having(
62
+
'Missing required parameter: did',
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',
74
+
() => CovesSession.fromCallbackUri(uri),
76
+
isA<FormatException>().having(
79
+
'Missing required parameter: session_id',
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',
91
+
() => CovesSession.fromCallbackUri(uri),
93
+
isA<FormatException>().having(
96
+
'Missing required parameter: token',
102
+
test('should throw FormatException when did is empty', () {
103
+
final uri = Uri.parse(
104
+
'social.coves:/callback?token=abc123&did=&session_id=sess456',
108
+
() => CovesSession.fromCallbackUri(uri),
110
+
isA<FormatException>().having(
113
+
'Missing required parameter: did',
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=',
125
+
() => CovesSession.fromCallbackUri(uri),
127
+
isA<FormatException>().having(
130
+
'Missing required parameter: session_id',
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',
141
+
final session = CovesSession.fromCallbackUri(uri);
143
+
expect(session.token, 'abc+123/456=');
144
+
expect(session.did, 'did:plc:test123');
145
+
expect(session.sessionId, 'sess456');
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',
153
+
final session = CovesSession.fromCallbackUri(uri);
155
+
expect(session.token, 'token with spaces');
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',
163
+
final session = CovesSession.fromCallbackUri(uri);
165
+
expect(session.token, 'abc123');
166
+
expect(session.did, 'did:plc:test123');
167
+
expect(session.sessionId, 'sess456');
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',
175
+
final session = CovesSession.fromCallbackUri(uri);
179
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U',
184
+
group('CovesSession.fromJson()', () {
185
+
test('should parse valid JSON with all fields', () {
188
+
'did': 'did:plc:test123',
189
+
'session_id': 'sess456',
190
+
'handle': 'test.user',
193
+
final session = CovesSession.fromJson(json);
195
+
expect(session.token, 'abc123');
196
+
expect(session.did, 'did:plc:test123');
197
+
expect(session.sessionId, 'sess456');
198
+
expect(session.handle, 'test.user');
201
+
test('should parse valid JSON without optional handle', () {
204
+
'did': 'did:plc:test123',
205
+
'session_id': 'sess456',
208
+
final session = CovesSession.fromJson(json);
210
+
expect(session.token, 'abc123');
211
+
expect(session.did, 'did:plc:test123');
212
+
expect(session.sessionId, 'sess456');
213
+
expect(session.handle, null);
216
+
test('should parse JSON with null handle', () {
219
+
'did': 'did:plc:test123',
220
+
'session_id': 'sess456',
224
+
final session = CovesSession.fromJson(json);
226
+
expect(session.handle, null);
229
+
test('should throw when token has wrong type', () {
231
+
'token': 123, // Should be String
232
+
'did': 'did:plc:test123',
233
+
'session_id': 'sess456',
237
+
() => CovesSession.fromJson(json),
238
+
throwsA(isA<TypeError>()),
242
+
test('should throw when did has wrong type', () {
245
+
'did': 123, // Should be String
246
+
'session_id': 'sess456',
250
+
() => CovesSession.fromJson(json),
251
+
throwsA(isA<TypeError>()),
255
+
test('should throw when session_id has wrong type', () {
258
+
'did': 'did:plc:test123',
259
+
'session_id': 123, // Should be String
263
+
() => CovesSession.fromJson(json),
264
+
throwsA(isA<TypeError>()),
268
+
test('should throw when token field is missing', () {
270
+
'did': 'did:plc:test123',
271
+
'session_id': 'sess456',
275
+
() => CovesSession.fromJson(json),
276
+
throwsA(isA<TypeError>()),
280
+
test('should throw when did field is missing', () {
283
+
'session_id': 'sess456',
287
+
() => CovesSession.fromJson(json),
288
+
throwsA(isA<TypeError>()),
292
+
test('should throw when session_id field is missing', () {
295
+
'did': 'did:plc:test123',
299
+
() => CovesSession.fromJson(json),
300
+
throwsA(isA<TypeError>()),
304
+
test('should handle extra fields in JSON', () {
307
+
'did': 'did:plc:test123',
308
+
'session_id': 'sess456',
309
+
'extra_field': 'ignored',
310
+
'another_field': 123,
313
+
final session = CovesSession.fromJson(json);
315
+
expect(session.token, 'abc123');
316
+
expect(session.did, 'did:plc:test123');
317
+
expect(session.sessionId, 'sess456');
321
+
group('CovesSession.fromJsonString()', () {
322
+
test('should parse valid JSON string', () {
323
+
final jsonString = jsonEncode({
325
+
'did': 'did:plc:test123',
326
+
'session_id': 'sess456',
327
+
'handle': 'test.user',
330
+
final session = CovesSession.fromJsonString(jsonString);
332
+
expect(session.token, 'abc123');
333
+
expect(session.did, 'did:plc:test123');
334
+
expect(session.sessionId, 'sess456');
335
+
expect(session.handle, 'test.user');
338
+
test('should parse valid JSON string without handle', () {
339
+
final jsonString = jsonEncode({
341
+
'did': 'did:plc:test123',
342
+
'session_id': 'sess456',
345
+
final session = CovesSession.fromJsonString(jsonString);
347
+
expect(session.token, 'abc123');
348
+
expect(session.did, 'did:plc:test123');
349
+
expect(session.sessionId, 'sess456');
350
+
expect(session.handle, null);
353
+
test('should throw on invalid JSON string', () {
354
+
const invalidJson = '{invalid json}';
357
+
() => CovesSession.fromJsonString(invalidJson),
358
+
throwsA(isA<FormatException>()),
362
+
test('should throw on empty string', () {
363
+
const emptyString = '';
366
+
() => CovesSession.fromJsonString(emptyString),
367
+
throwsA(isA<FormatException>()),
371
+
test('should throw on non-JSON string', () {
372
+
const notJson = 'not a json string';
375
+
() => CovesSession.fromJsonString(notJson),
376
+
throwsA(isA<FormatException>()),
380
+
test('should throw on JSON array instead of object', () {
381
+
const jsonArray = '["token", "did", "session_id"]';
384
+
() => CovesSession.fromJsonString(jsonArray),
385
+
throwsA(isA<TypeError>()),
389
+
test('should throw on null JSON', () {
390
+
const nullJson = 'null';
393
+
() => CovesSession.fromJsonString(nullJson),
394
+
throwsA(isA<TypeError>()),
399
+
group('toJson() / toJsonString()', () {
400
+
test('should serialize to JSON with all fields', () {
401
+
const session = CovesSession(
403
+
did: 'did:plc:test123',
404
+
sessionId: 'sess456',
405
+
handle: 'test.user',
408
+
final json = session.toJson();
410
+
expect(json['token'], 'abc123');
411
+
expect(json['did'], 'did:plc:test123');
412
+
expect(json['session_id'], 'sess456');
413
+
expect(json['handle'], 'test.user');
416
+
test('should serialize to JSON without handle when null', () {
417
+
const session = CovesSession(
419
+
did: 'did:plc:test123',
420
+
sessionId: 'sess456',
423
+
final json = session.toJson();
425
+
expect(json['token'], 'abc123');
426
+
expect(json['did'], 'did:plc:test123');
427
+
expect(json['session_id'], 'sess456');
428
+
expect(json.containsKey('handle'), false);
431
+
test('should serialize to JSON string', () {
432
+
const session = CovesSession(
434
+
did: 'did:plc:test123',
435
+
sessionId: 'sess456',
436
+
handle: 'test.user',
439
+
final jsonString = session.toJsonString();
440
+
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
442
+
expect(decoded['token'], 'abc123');
443
+
expect(decoded['did'], 'did:plc:test123');
444
+
expect(decoded['session_id'], 'sess456');
445
+
expect(decoded['handle'], 'test.user');
448
+
test('should round-trip: create, serialize, deserialize, compare', () {
449
+
const original = CovesSession(
451
+
did: 'did:plc:test123',
452
+
sessionId: 'sess456',
453
+
handle: 'test.user',
456
+
final json = original.toJson();
457
+
final restored = CovesSession.fromJson(json);
459
+
expect(restored.token, original.token);
460
+
expect(restored.did, original.did);
461
+
expect(restored.sessionId, original.sessionId);
462
+
expect(restored.handle, original.handle);
465
+
test('should round-trip with JSON string', () {
466
+
const original = CovesSession(
468
+
did: 'did:plc:test123',
469
+
sessionId: 'sess456',
470
+
handle: 'test.user',
473
+
final jsonString = original.toJsonString();
474
+
final restored = CovesSession.fromJsonString(jsonString);
476
+
expect(restored.token, original.token);
477
+
expect(restored.did, original.did);
478
+
expect(restored.sessionId, original.sessionId);
479
+
expect(restored.handle, original.handle);
482
+
test('should round-trip without handle', () {
483
+
const original = CovesSession(
485
+
did: 'did:plc:test123',
486
+
sessionId: 'sess456',
489
+
final json = original.toJson();
490
+
final restored = CovesSession.fromJson(json);
492
+
expect(restored.token, original.token);
493
+
expect(restored.did, original.did);
494
+
expect(restored.sessionId, original.sessionId);
495
+
expect(restored.handle, null);
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',
506
+
final jsonString = session.toJsonString();
507
+
final restored = CovesSession.fromJsonString(jsonString);
509
+
expect(restored.token, session.token);
510
+
expect(restored.handle, session.handle);
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',
523
+
final updated = original.copyWithToken('new_token');
525
+
expect(updated.token, 'new_token');
526
+
expect(updated.did, original.did);
527
+
expect(updated.sessionId, original.sessionId);
528
+
expect(updated.handle, original.handle);
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',
538
+
final updated = original.copyWithToken('new_token');
540
+
expect(updated.token, 'new_token');
541
+
expect(updated.did, original.did);
542
+
expect(updated.sessionId, original.sessionId);
543
+
expect(updated.handle, null);
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',
554
+
final updated = original.copyWithToken('new_token');
556
+
expect(original.token, 'old_token');
557
+
expect(updated.token, 'new_token');
560
+
test('should handle empty string token', () {
561
+
const original = CovesSession(
562
+
token: 'old_token',
563
+
did: 'did:plc:test123',
564
+
sessionId: 'sess456',
567
+
final updated = original.copyWithToken('');
569
+
expect(updated.token, '');
570
+
expect(updated.did, original.did);
573
+
test('should handle complex token values', () {
574
+
const original = CovesSession(
575
+
token: 'old_token',
576
+
did: 'did:plc:test123',
577
+
sessionId: 'sess456',
581
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U';
582
+
final updated = original.copyWithToken(newToken);
584
+
expect(updated.token, newToken);
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',
597
+
final stringRep = session.toString();
599
+
expect(stringRep, isNot(contains('secret_token_abc123')));
600
+
expect(stringRep, isNot(contains('token')));
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',
611
+
final stringRep = session.toString();
613
+
expect(stringRep, contains('did:plc:test123'));
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',
624
+
final stringRep = session.toString();
626
+
expect(stringRep, contains('test.user'));
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',
637
+
final stringRep = session.toString();
639
+
expect(stringRep, contains('sess456'));
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',
649
+
final stringRep = session.toString();
651
+
expect(stringRep, contains('did:plc:test123'));
652
+
expect(stringRep, contains('sess456'));
653
+
expect(stringRep, contains('null'));
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',
664
+
final stringRep = session.toString();
668
+
'CovesSession(did: did:plc:test123, handle: test.user, sessionId: sess456)',
673
+
group('Edge cases', () {
674
+
test('should handle very long token values', () {
675
+
final longToken = 'a' * 10000;
676
+
final session = CovesSession(
678
+
did: 'did:plc:test123',
679
+
sessionId: 'sess456',
682
+
expect(session.token.length, 10000);
684
+
final json = session.toJson();
685
+
final restored = CovesSession.fromJson(json);
687
+
expect(restored.token, longToken);
690
+
test('should handle unicode characters in handle', () {
691
+
const session = CovesSession(
693
+
did: 'did:plc:test123',
694
+
sessionId: 'sess456',
695
+
handle: 'test.用户.bsky.social',
698
+
final json = session.toJson();
699
+
final restored = CovesSession.fromJson(json);
701
+
expect(restored.handle, 'test.用户.bsky.social');
704
+
test('should handle DID with different methods', () {
705
+
const session = CovesSession(
707
+
did: 'did:web:example.com',
708
+
sessionId: 'sess456',
711
+
final json = session.toJson();
712
+
final restored = CovesSession.fromJson(json);
714
+
expect(restored.did, 'did:web:example.com');
717
+
test('should handle session with colons in sessionId', () {
718
+
const session = CovesSession(
720
+
did: 'did:plc:test123',
721
+
sessionId: 'sess:456:789',
724
+
final json = session.toJson();
725
+
final restored = CovesSession.fromJson(json);
727
+
expect(restored.sessionId, 'sess:456:789');
730
+
test('should handle empty handle string', () {
731
+
const session = CovesSession(
733
+
did: 'did:plc:test123',
734
+
sessionId: 'sess456',
738
+
final json = session.toJson();
740
+
expect(json['handle'], '');
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',
748
+
final session = CovesSession.fromCallbackUri(uri);
750
+
expect(session.token, ' abc123 ');
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',
759
+
final session = CovesSession.fromCallbackUri(uri);
761
+
// Uri.queryParameters decodes once, Uri.decodeComponent decodes again
762
+
expect(session.token, 'abc+123');
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',
775
+
final stringRep = session.toString();
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')));
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',
793
+
final stringRep = session.toString();
795
+
expect(stringRep, isNot(contains('Bearer')));
796
+
expect(stringRep, isNot(contains('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')));