···
+
import 'package:coves_flutter/models/coves_session.dart';
+
import 'package:flutter_test/flutter_test.dart';
+
group('CovesSession.fromCallbackUri()', () {
+
test('should parse valid URI with all parameters', () {
+
'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=sess456&handle=test.user',
+
final session = CovesSession.fromCallbackUri(uri);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
expect(session.handle, 'test.user');
+
test('should parse valid URI without optional handle', () {
+
'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=sess456',
+
final session = CovesSession.fromCallbackUri(uri);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
expect(session.handle, null);
+
test('should throw FormatException when token is missing', () {
+
'social.coves:/callback?did=did:plc:test123&session_id=sess456',
+
() => CovesSession.fromCallbackUri(uri),
+
isA<FormatException>().having(
+
'Missing required parameter: token',
+
test('should throw FormatException when did is missing', () {
+
'social.coves:/callback?token=abc123&session_id=sess456',
+
() => CovesSession.fromCallbackUri(uri),
+
isA<FormatException>().having(
+
'Missing required parameter: did',
+
test('should throw FormatException when session_id is missing', () {
+
'social.coves:/callback?token=abc123&did=did:plc:test123',
+
() => CovesSession.fromCallbackUri(uri),
+
isA<FormatException>().having(
+
'Missing required parameter: session_id',
+
test('should throw FormatException when token is empty', () {
+
'social.coves:/callback?token=&did=did:plc:test123&session_id=sess456',
+
() => CovesSession.fromCallbackUri(uri),
+
isA<FormatException>().having(
+
'Missing required parameter: token',
+
test('should throw FormatException when did is empty', () {
+
'social.coves:/callback?token=abc123&did=&session_id=sess456',
+
() => CovesSession.fromCallbackUri(uri),
+
isA<FormatException>().having(
+
'Missing required parameter: did',
+
test('should throw FormatException when session_id is empty', () {
+
'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=',
+
() => CovesSession.fromCallbackUri(uri),
+
isA<FormatException>().having(
+
'Missing required parameter: session_id',
+
test('should decode URL-encoded token values', () {
+
'social.coves:/callback?token=abc%2B123%2F456%3D&did=did:plc:test123&session_id=sess456',
+
final session = CovesSession.fromCallbackUri(uri);
+
expect(session.token, 'abc+123/456=');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
test('should handle URL-encoded spaces in token', () {
+
'social.coves:/callback?token=token%20with%20spaces&did=did:plc:test123&session_id=sess456',
+
final session = CovesSession.fromCallbackUri(uri);
+
expect(session.token, 'token with spaces');
+
test('should ignore extra/unknown parameters', () {
+
'social.coves:/callback?token=abc123&did=did:plc:test123&session_id=sess456&extra=ignored&unknown=also_ignored',
+
final session = CovesSession.fromCallbackUri(uri);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
test('should handle complex token values', () {
+
'social.coves:/callback?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U&did=did:plc:test123&session_id=sess456',
+
final session = CovesSession.fromCallbackUri(uri);
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U',
+
group('CovesSession.fromJson()', () {
+
test('should parse valid JSON with all fields', () {
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
final session = CovesSession.fromJson(json);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
expect(session.handle, 'test.user');
+
test('should parse valid JSON without optional handle', () {
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
final session = CovesSession.fromJson(json);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
expect(session.handle, null);
+
test('should parse JSON with null handle', () {
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
final session = CovesSession.fromJson(json);
+
expect(session.handle, null);
+
test('should throw when token has wrong type', () {
+
'token': 123, // Should be String
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
() => CovesSession.fromJson(json),
+
throwsA(isA<TypeError>()),
+
test('should throw when did has wrong type', () {
+
'did': 123, // Should be String
+
'session_id': 'sess456',
+
() => CovesSession.fromJson(json),
+
throwsA(isA<TypeError>()),
+
test('should throw when session_id has wrong type', () {
+
'did': 'did:plc:test123',
+
'session_id': 123, // Should be String
+
() => CovesSession.fromJson(json),
+
throwsA(isA<TypeError>()),
+
test('should throw when token field is missing', () {
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
() => CovesSession.fromJson(json),
+
throwsA(isA<TypeError>()),
+
test('should throw when did field is missing', () {
+
'session_id': 'sess456',
+
() => CovesSession.fromJson(json),
+
throwsA(isA<TypeError>()),
+
test('should throw when session_id field is missing', () {
+
'did': 'did:plc:test123',
+
() => CovesSession.fromJson(json),
+
throwsA(isA<TypeError>()),
+
test('should handle extra fields in JSON', () {
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
'extra_field': 'ignored',
+
final session = CovesSession.fromJson(json);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
group('CovesSession.fromJsonString()', () {
+
test('should parse valid JSON string', () {
+
final jsonString = jsonEncode({
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
final session = CovesSession.fromJsonString(jsonString);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
expect(session.handle, 'test.user');
+
test('should parse valid JSON string without handle', () {
+
final jsonString = jsonEncode({
+
'did': 'did:plc:test123',
+
'session_id': 'sess456',
+
final session = CovesSession.fromJsonString(jsonString);
+
expect(session.token, 'abc123');
+
expect(session.did, 'did:plc:test123');
+
expect(session.sessionId, 'sess456');
+
expect(session.handle, null);
+
test('should throw on invalid JSON string', () {
+
const invalidJson = '{invalid json}';
+
() => CovesSession.fromJsonString(invalidJson),
+
throwsA(isA<FormatException>()),
+
test('should throw on empty string', () {
+
const emptyString = '';
+
() => CovesSession.fromJsonString(emptyString),
+
throwsA(isA<FormatException>()),
+
test('should throw on non-JSON string', () {
+
const notJson = 'not a json string';
+
() => CovesSession.fromJsonString(notJson),
+
throwsA(isA<FormatException>()),
+
test('should throw on JSON array instead of object', () {
+
const jsonArray = '["token", "did", "session_id"]';
+
() => CovesSession.fromJsonString(jsonArray),
+
throwsA(isA<TypeError>()),
+
test('should throw on null JSON', () {
+
const nullJson = 'null';
+
() => CovesSession.fromJsonString(nullJson),
+
throwsA(isA<TypeError>()),
+
group('toJson() / toJsonString()', () {
+
test('should serialize to JSON with all fields', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final json = session.toJson();
+
expect(json['token'], 'abc123');
+
expect(json['did'], 'did:plc:test123');
+
expect(json['session_id'], 'sess456');
+
expect(json['handle'], 'test.user');
+
test('should serialize to JSON without handle when null', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final json = session.toJson();
+
expect(json['token'], 'abc123');
+
expect(json['did'], 'did:plc:test123');
+
expect(json['session_id'], 'sess456');
+
expect(json.containsKey('handle'), false);
+
test('should serialize to JSON string', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final jsonString = session.toJsonString();
+
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
+
expect(decoded['token'], 'abc123');
+
expect(decoded['did'], 'did:plc:test123');
+
expect(decoded['session_id'], 'sess456');
+
expect(decoded['handle'], 'test.user');
+
test('should round-trip: create, serialize, deserialize, compare', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
final json = original.toJson();
+
final restored = CovesSession.fromJson(json);
+
expect(restored.token, original.token);
+
expect(restored.did, original.did);
+
expect(restored.sessionId, original.sessionId);
+
expect(restored.handle, original.handle);
+
test('should round-trip with JSON string', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
final jsonString = original.toJsonString();
+
final restored = CovesSession.fromJsonString(jsonString);
+
expect(restored.token, original.token);
+
expect(restored.did, original.did);
+
expect(restored.sessionId, original.sessionId);
+
expect(restored.handle, original.handle);
+
test('should round-trip without handle', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
final json = original.toJson();
+
final restored = CovesSession.fromJson(json);
+
expect(restored.token, original.token);
+
expect(restored.did, original.did);
+
expect(restored.sessionId, original.sessionId);
+
expect(restored.handle, null);
+
test('should handle special characters in serialization', () {
+
const session = CovesSession(
+
token: 'token+with/special=chars',
+
did: 'did:plc:test123',
+
handle: 'user.with.dots',
+
final jsonString = session.toJsonString();
+
final restored = CovesSession.fromJsonString(jsonString);
+
expect(restored.token, session.token);
+
expect(restored.handle, session.handle);
+
group('copyWithToken()', () {
+
test('should create new session with updated token', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
final updated = original.copyWithToken('new_token');
+
expect(updated.token, 'new_token');
+
expect(updated.did, original.did);
+
expect(updated.sessionId, original.sessionId);
+
expect(updated.handle, original.handle);
+
test('should preserve null handle when copying with new token', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
final updated = original.copyWithToken('new_token');
+
expect(updated.token, 'new_token');
+
expect(updated.did, original.did);
+
expect(updated.sessionId, original.sessionId);
+
expect(updated.handle, null);
+
test('should not modify original session', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
final updated = original.copyWithToken('new_token');
+
expect(original.token, 'old_token');
+
expect(updated.token, 'new_token');
+
test('should handle empty string token', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
final updated = original.copyWithToken('');
+
expect(updated.token, '');
+
expect(updated.did, original.did);
+
test('should handle complex token values', () {
+
const original = CovesSession(
+
did: 'did:plc:test123',
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U';
+
final updated = original.copyWithToken(newToken);
+
expect(updated.token, newToken);
+
group('toString()', () {
+
test('should not expose token in string representation', () {
+
const session = CovesSession(
+
token: 'secret_token_abc123',
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
expect(stringRep, isNot(contains('secret_token_abc123')));
+
expect(stringRep, isNot(contains('token')));
+
test('should include did in string representation', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
expect(stringRep, contains('did:plc:test123'));
+
test('should include handle in string representation', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
expect(stringRep, contains('test.user'));
+
test('should include sessionId in string representation', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
expect(stringRep, contains('sess456'));
+
test('should handle null handle in string representation', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
expect(stringRep, contains('did:plc:test123'));
+
expect(stringRep, contains('sess456'));
+
expect(stringRep, contains('null'));
+
test('should follow expected format', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
'CovesSession(did: did:plc:test123, handle: test.user, sessionId: sess456)',
+
group('Edge cases', () {
+
test('should handle very long token values', () {
+
final longToken = 'a' * 10000;
+
final session = CovesSession(
+
did: 'did:plc:test123',
+
expect(session.token.length, 10000);
+
final json = session.toJson();
+
final restored = CovesSession.fromJson(json);
+
expect(restored.token, longToken);
+
test('should handle unicode characters in handle', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
handle: 'test.用户.bsky.social',
+
final json = session.toJson();
+
final restored = CovesSession.fromJson(json);
+
expect(restored.handle, 'test.用户.bsky.social');
+
test('should handle DID with different methods', () {
+
const session = CovesSession(
+
did: 'did:web:example.com',
+
final json = session.toJson();
+
final restored = CovesSession.fromJson(json);
+
expect(restored.did, 'did:web:example.com');
+
test('should handle session with colons in sessionId', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
sessionId: 'sess:456:789',
+
final json = session.toJson();
+
final restored = CovesSession.fromJson(json);
+
expect(restored.sessionId, 'sess:456:789');
+
test('should handle empty handle string', () {
+
const session = CovesSession(
+
did: 'did:plc:test123',
+
final json = session.toJson();
+
expect(json['handle'], '');
+
test('should handle whitespace in token from callback URI', () {
+
'social.coves:/callback?token=%20abc123%20&did=did:plc:test123&session_id=sess456',
+
final session = CovesSession.fromCallbackUri(uri);
+
expect(session.token, ' abc123 ');
+
test('should handle multiple URL encoding passes', () {
+
// Token that's been double-encoded
+
'social.coves:/callback?token=abc%252B123&did=did:plc:test123&session_id=sess456',
+
final session = CovesSession.fromCallbackUri(uri);
+
// Uri.queryParameters decodes once, Uri.decodeComponent decodes again
+
expect(session.token, 'abc+123');
+
test('toString should not leak sensitive token data', () {
+
const session = CovesSession(
+
token: 'super_secret_encrypted_token_12345',
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
// Verify the entire token is not present
+
expect(stringRep, isNot(contains('super_secret_encrypted_token_12345')));
+
// Verify even partial token data is not present
+
expect(stringRep, isNot(contains('secret')));
+
expect(stringRep, isNot(contains('encrypted')));
+
expect(stringRep, isNot(contains('12345')));
+
test('toString should be safe for logging', () {
+
const session = CovesSession(
+
token: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
+
did: 'did:plc:test123',
+
final stringRep = session.toString();
+
expect(stringRep, isNot(contains('Bearer')));
+
expect(stringRep, isNot(contains('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')));