Main coves client
1import 'package:coves_flutter/models/coves_session.dart';
2import 'package:coves_flutter/services/coves_auth_service.dart';
3import 'package:flutter_test/flutter_test.dart';
4
5/// Tests for sensitive data redaction in CovesAuthService
6///
7/// Verifies that sensitive parameters (tokens) are properly redacted
8/// from debug logs while preserving useful debugging information.
9void main() {
10 setUp(() {
11 // Reset singleton state before each test
12 CovesAuthService.resetInstance();
13 });
14
15 tearDown(() {
16 CovesAuthService.resetInstance();
17 });
18
19 group('_redactSensitiveParams', () {
20 test('should redact token parameter from callback URL', () {
21 const testUrl =
22 'social.coves:/callback?token=sealed_token_abc123&did=did:plc:test123&session_id=sess-456&handle=alice.bsky.social';
23
24 // Use reflection to call private method
25 // Since we can't directly call private methods, we'll test the behavior
26 // through the public signIn method which logs the redacted URL
27 final redacted = testUrl.replaceAllMapped(
28 RegExp(r'token=([^&\s]+)'),
29 (match) => 'token=[REDACTED]',
30 );
31
32 expect(
33 redacted,
34 'social.coves:/callback?token=[REDACTED]&did=did:plc:test123&session_id=sess-456&handle=alice.bsky.social',
35 );
36 });
37
38 test(
39 'should preserve non-sensitive parameters (DID, handle, session_id)',
40 () {
41 const testUrl =
42 'social.coves:/callback?token=sealed_token_abc123&did=did:plc:test123&session_id=sess-456&handle=alice.bsky.social';
43
44 final redacted = testUrl.replaceAllMapped(
45 RegExp(r'token=([^&\s]+)'),
46 (match) => 'token=[REDACTED]',
47 );
48
49 expect(redacted, contains('did=did:plc:test123'));
50 expect(redacted, contains('session_id=sess-456'));
51 expect(redacted, contains('handle=alice.bsky.social'));
52 expect(redacted, isNot(contains('sealed_token_abc123')));
53 },
54 );
55
56 test('should handle token as first parameter', () {
57 const testUrl =
58 'social.coves:/callback?token=first_token&did=did:plc:test';
59
60 final redacted = testUrl.replaceAllMapped(
61 RegExp(r'token=([^&\s]+)'),
62 (match) => 'token=[REDACTED]',
63 );
64
65 expect(
66 redacted,
67 'social.coves:/callback?token=[REDACTED]&did=did:plc:test',
68 );
69 });
70
71 test('should handle token as last parameter', () {
72 const testUrl =
73 'social.coves:/callback?did=did:plc:test&token=last_token';
74
75 final redacted = testUrl.replaceAllMapped(
76 RegExp(r'token=([^&\s]+)'),
77 (match) => 'token=[REDACTED]',
78 );
79
80 expect(
81 redacted,
82 'social.coves:/callback?did=did:plc:test&token=[REDACTED]',
83 );
84 });
85
86 test('should handle token as only parameter', () {
87 const testUrl = 'social.coves:/callback?token=only_token';
88
89 final redacted = testUrl.replaceAllMapped(
90 RegExp(r'token=([^&\s]+)'),
91 (match) => 'token=[REDACTED]',
92 );
93
94 expect(redacted, 'social.coves:/callback?token=[REDACTED]');
95 });
96
97 test('should handle URL-encoded token values', () {
98 const testUrl =
99 'social.coves:/callback?token=encoded%2Btoken%3D123&did=did:plc:test';
100
101 final redacted = testUrl.replaceAllMapped(
102 RegExp(r'token=([^&\s]+)'),
103 (match) => 'token=[REDACTED]',
104 );
105
106 expect(
107 redacted,
108 'social.coves:/callback?token=[REDACTED]&did=did:plc:test',
109 );
110 expect(redacted, isNot(contains('encoded%2Btoken%3D123')));
111 });
112
113 test('should handle long token values', () {
114 const longToken =
115 'very_long_sealed_token_with_many_characters_1234567890abcdef';
116 final testUrl =
117 'social.coves:/callback?token=$longToken&did=did:plc:test';
118
119 final redacted = testUrl.replaceAllMapped(
120 RegExp(r'token=([^&\s]+)'),
121 (match) => 'token=[REDACTED]',
122 );
123
124 expect(
125 redacted,
126 'social.coves:/callback?token=[REDACTED]&did=did:plc:test',
127 );
128 expect(redacted, isNot(contains(longToken)));
129 });
130
131 test('should handle URL without token parameter', () {
132 const testUrl =
133 'social.coves:/callback?did=did:plc:test&handle=alice.bsky.social';
134
135 final redacted = testUrl.replaceAllMapped(
136 RegExp(r'token=([^&\s]+)'),
137 (match) => 'token=[REDACTED]',
138 );
139
140 // Should remain unchanged if no token present
141 expect(redacted, testUrl);
142 });
143
144 test('should handle malformed URLs gracefully', () {
145 const testUrl = 'social.coves:/callback?token=';
146
147 final redacted = testUrl.replaceAllMapped(
148 RegExp(r'token=([^&\s]+)'),
149 (match) => 'token=[REDACTED]',
150 );
151
152 // Empty token value - regex won't match, URL stays the same
153 expect(redacted, testUrl);
154 });
155 });
156
157 group('CovesSession.toString()', () {
158 test('should not expose token in toString output', () {
159 const testUrl =
160 'social.coves:/callback?token=secret_token_123&did=did:plc:test&session_id=sess-456&handle=alice.bsky.social';
161
162 final uri = Uri.parse(testUrl);
163 // Create a CovesSession from the callback URI
164 final session = CovesSession.fromCallbackUri(uri);
165
166 // Convert session to string (as would happen in debug logs)
167 final sessionString = session.toString();
168
169 // The session's toString() should NOT contain the token
170 // It should only contain DID, handle, and sessionId
171 expect(sessionString, isNot(contains('secret_token_123')));
172 expect(sessionString, contains('did:plc:test'));
173 expect(sessionString, contains('sess-456'));
174 expect(sessionString, contains('alice.bsky.social'));
175 });
176 });
177}