at main 5.7 kB view raw
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}