1import 'package:coves_flutter/services/coves_auth_service.dart'; 2import 'package:dio/dio.dart'; 3import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 4import 'package:flutter_test/flutter_test.dart'; 5import 'package:mockito/annotations.dart'; 6import 'package:mockito/mockito.dart'; 7 8import 'coves_auth_service_test.mocks.dart'; 9 10@GenerateMocks([Dio, FlutterSecureStorage]) 11void main() { 12 late MockDio mockDio; 13 late MockFlutterSecureStorage mockStorage; 14 late CovesAuthService authService; 15 16 setUp(() { 17 CovesAuthService.resetInstance(); 18 mockDio = MockDio(); 19 mockStorage = MockFlutterSecureStorage(); 20 authService = CovesAuthService.createTestInstance( 21 dio: mockDio, 22 storage: mockStorage, 23 ); 24 }); 25 26 tearDown(() { 27 CovesAuthService.resetInstance(); 28 }); 29 30 group('Handle Validation', () { 31 group('Valid inputs', () { 32 test('should accept standard handle format', () { 33 final result = 34 authService.validateAndNormalizeHandle('alice.bsky.social'); 35 expect(result, 'alice.bsky.social'); 36 }); 37 38 test('should accept handle with @ prefix and strip it', () { 39 final result = 40 authService.validateAndNormalizeHandle('@alice.bsky.social'); 41 expect(result, 'alice.bsky.social'); 42 }); 43 44 test('should accept handle with leading/trailing whitespace and trim', () { 45 final result = 46 authService.validateAndNormalizeHandle(' alice.bsky.social '); 47 expect(result, 'alice.bsky.social'); 48 }); 49 50 test('should accept handle with hyphen in segment', () { 51 final result = 52 authService.validateAndNormalizeHandle('alice-bob.bsky.social'); 53 expect(result, 'alice-bob.bsky.social'); 54 }); 55 56 test('should accept handle with multiple hyphens', () { 57 final result = authService 58 .validateAndNormalizeHandle('alice-bob-charlie.bsky-app.social'); 59 expect(result, 'alice-bob-charlie.bsky-app.social'); 60 }); 61 62 test('should accept handle with multiple subdomains', () { 63 final result = authService 64 .validateAndNormalizeHandle('alice.subdomain.example.com'); 65 expect(result, 'alice.subdomain.example.com'); 66 }); 67 68 test('should accept handle with numbers', () { 69 final result = 70 authService.validateAndNormalizeHandle('user123.bsky.social'); 71 expect(result, 'user123.bsky.social'); 72 }); 73 74 test('should convert handle to lowercase', () { 75 final result = 76 authService.validateAndNormalizeHandle('Alice.Bsky.Social'); 77 expect(result, 'alice.bsky.social'); 78 }); 79 80 test('should extract and validate handle from Bluesky profile URL (HTTP)', () { 81 final result = authService.validateAndNormalizeHandle( 82 'http://bsky.app/profile/alice.bsky.social'); 83 expect(result, 'alice.bsky.social'); 84 }); 85 86 test('should extract and validate handle from Bluesky profile URL (HTTPS)', () { 87 final result = authService.validateAndNormalizeHandle( 88 'https://bsky.app/profile/alice.bsky.social'); 89 expect(result, 'alice.bsky.social'); 90 }); 91 92 test('should extract and validate handle from Bluesky profile URL with www', () { 93 final result = authService.validateAndNormalizeHandle( 94 'https://www.bsky.app/profile/alice.bsky.social'); 95 expect(result, 'alice.bsky.social'); 96 }); 97 98 test('should accept DID with plc method', () { 99 final result = 100 authService.validateAndNormalizeHandle('did:plc:abc123def456'); 101 expect(result, 'did:plc:abc123def456'); 102 }); 103 104 test('should accept DID with web method', () { 105 final result = 106 authService.validateAndNormalizeHandle('did:web:example.com'); 107 expect(result, 'did:web:example.com'); 108 }); 109 110 test('should accept DID with complex identifier', () { 111 final result = authService 112 .validateAndNormalizeHandle('did:plc:z72i7hdynmk6r22z27h6tvur'); 113 expect(result, 'did:plc:z72i7hdynmk6r22z27h6tvur'); 114 }); 115 116 test('should accept DID with periods and colons in identifier', () { 117 final result = authService 118 .validateAndNormalizeHandle('did:web:example.com:user:alice'); 119 expect(result, 'did:web:example.com:user:alice'); 120 }); 121 122 test('should accept short handle', () { 123 final result = authService.validateAndNormalizeHandle('a.b'); 124 expect(result, 'a.b'); 125 }); 126 127 test('should normalize handle with @ prefix and whitespace', () { 128 final result = 129 authService.validateAndNormalizeHandle(' @Alice.Bsky.Social '); 130 expect(result, 'alice.bsky.social'); 131 }); 132 133 test('should accept handle with numeric first segment', () { 134 final result = 135 authService.validateAndNormalizeHandle('123.bsky.social'); 136 expect(result, '123.bsky.social'); 137 }); 138 139 test('should accept handle with numeric middle segment', () { 140 final result = 141 authService.validateAndNormalizeHandle('alice.456.social'); 142 expect(result, 'alice.456.social'); 143 }); 144 145 test('should accept handle with multiple numeric segments', () { 146 final result = 147 authService.validateAndNormalizeHandle('42.example.com'); 148 expect(result, '42.example.com'); 149 }); 150 151 test('should accept handle similar to 4chan.org', () { 152 final result = authService.validateAndNormalizeHandle('4chan.org'); 153 expect(result, '4chan.org'); 154 }); 155 156 test('should accept handle with numeric and alpha mixed', () { 157 final result = 158 authService.validateAndNormalizeHandle('8.cn'); 159 expect(result, '8.cn'); 160 }); 161 162 test('should accept handle like IP but with valid TLD', () { 163 final result = 164 authService.validateAndNormalizeHandle('120.0.0.1.com'); 165 expect(result, '120.0.0.1.com'); 166 }); 167 }); 168 169 group('Invalid inputs', () { 170 test('should throw ArgumentError when handle is empty', () { 171 expect( 172 () => authService.validateAndNormalizeHandle(''), 173 throwsA(isA<ArgumentError>()), 174 ); 175 }); 176 177 test('should throw ArgumentError when handle is whitespace-only', () { 178 expect( 179 () => authService.validateAndNormalizeHandle(' '), 180 throwsA(isA<ArgumentError>()), 181 ); 182 }); 183 184 test('should throw ArgumentError for handle without period', () { 185 expect( 186 () => authService.validateAndNormalizeHandle('alice'), 187 throwsA( 188 predicate( 189 (e) => 190 e is ArgumentError && 191 e.message.toString().contains('domain format'), 192 ), 193 ), 194 ); 195 }); 196 197 test('should throw ArgumentError for handle starting with hyphen', () { 198 expect( 199 () => authService.validateAndNormalizeHandle('-alice.bsky.social'), 200 throwsA( 201 predicate( 202 (e) => 203 e is ArgumentError && 204 e.message.toString().contains('Invalid handle format'), 205 ), 206 ), 207 ); 208 }); 209 210 test('should throw ArgumentError for handle ending with hyphen', () { 211 expect( 212 () => authService.validateAndNormalizeHandle('alice-.bsky.social'), 213 throwsA( 214 predicate( 215 (e) => 216 e is ArgumentError && 217 e.message.toString().contains('Invalid handle format'), 218 ), 219 ), 220 ); 221 }); 222 223 test('should throw ArgumentError for segment with hyphen at end', () { 224 expect( 225 () => authService.validateAndNormalizeHandle('alice.bsky-.social'), 226 throwsA( 227 predicate( 228 (e) => 229 e is ArgumentError && 230 e.message.toString().contains('Invalid handle format'), 231 ), 232 ), 233 ); 234 }); 235 236 test('should throw ArgumentError for handle starting with period', () { 237 expect( 238 () => authService.validateAndNormalizeHandle('.alice.bsky.social'), 239 throwsA( 240 predicate( 241 (e) => 242 e is ArgumentError && 243 e.message.toString().contains('Invalid handle format'), 244 ), 245 ), 246 ); 247 }); 248 249 test('should throw ArgumentError for handle ending with period', () { 250 expect( 251 () => authService.validateAndNormalizeHandle('alice.bsky.social.'), 252 throwsA( 253 predicate( 254 (e) => 255 e is ArgumentError && 256 e.message.toString().contains('Invalid handle format'), 257 ), 258 ), 259 ); 260 }); 261 262 test('should throw ArgumentError for handle with consecutive periods', () { 263 expect( 264 () => authService.validateAndNormalizeHandle('alice..bsky.social'), 265 throwsA( 266 predicate( 267 (e) => 268 e is ArgumentError && 269 (e.message.toString().contains('empty segments') || 270 e.message.toString().contains('Invalid handle format')), 271 ), 272 ), 273 ); 274 }); 275 276 test('should throw ArgumentError for handle with spaces', () { 277 expect( 278 () => authService.validateAndNormalizeHandle('alice bsky.social'), 279 throwsA( 280 predicate( 281 (e) => 282 e is ArgumentError && 283 e.message.toString().contains('Invalid handle format'), 284 ), 285 ), 286 ); 287 }); 288 289 test('should throw ArgumentError for handle with @ in middle', () { 290 expect( 291 () => authService.validateAndNormalizeHandle('alice@bsky.social'), 292 throwsA( 293 predicate( 294 (e) => 295 e is ArgumentError && 296 e.message.toString().contains('Invalid handle format'), 297 ), 298 ), 299 ); 300 }); 301 302 test('should throw ArgumentError for handle with underscore', () { 303 expect( 304 () => authService.validateAndNormalizeHandle('alice_bob.bsky.social'), 305 throwsA( 306 predicate( 307 (e) => 308 e is ArgumentError && 309 e.message.toString().contains('Invalid handle format'), 310 ), 311 ), 312 ); 313 }); 314 315 test('should throw ArgumentError for handle with exclamation mark', () { 316 expect( 317 () => authService.validateAndNormalizeHandle('alice!.bsky.social'), 318 throwsA( 319 predicate( 320 (e) => 321 e is ArgumentError && 322 e.message.toString().contains('Invalid handle format'), 323 ), 324 ), 325 ); 326 }); 327 328 test('should throw ArgumentError for handle with slash', () { 329 expect( 330 () => authService.validateAndNormalizeHandle('alice/bob.bsky.social'), 331 throwsA( 332 predicate( 333 (e) => 334 e is ArgumentError && 335 e.message.toString().contains('Invalid handle format'), 336 ), 337 ), 338 ); 339 }); 340 341 test('should throw ArgumentError for handle exceeding 253 characters', () { 342 // Create a handle that's 254 characters long 343 final longHandle = '${'a' * 240}.bsky.social'; 344 expect( 345 () => authService.validateAndNormalizeHandle(longHandle), 346 throwsA( 347 predicate( 348 (e) => 349 e is ArgumentError && 350 e.message.toString().contains('too long'), 351 ), 352 ), 353 ); 354 }); 355 356 test('should throw ArgumentError for segment exceeding 63 characters', () { 357 // DNS label limit is 63 characters per segment 358 final longSegment = '${'a' * 64}.bsky.social'; 359 expect( 360 () => authService.validateAndNormalizeHandle(longSegment), 361 throwsA( 362 predicate( 363 (e) => 364 e is ArgumentError && 365 e.message.toString().contains('too long'), 366 ), 367 ), 368 ); 369 }); 370 371 test('should throw ArgumentError for TLD starting with digit', () { 372 expect( 373 () => authService.validateAndNormalizeHandle('alice.bsky.123'), 374 throwsA( 375 predicate( 376 (e) => 377 e is ArgumentError && 378 e.message.toString().contains('TLD') && 379 e.message.toString().contains('cannot start with a digit'), 380 ), 381 ), 382 ); 383 }); 384 385 test('should throw ArgumentError for all-numeric TLD', () { 386 expect( 387 () => authService.validateAndNormalizeHandle('123.456.789'), 388 throwsA( 389 predicate( 390 (e) => 391 e is ArgumentError && 392 e.message.toString().contains('TLD') && 393 e.message.toString().contains('cannot start with a digit'), 394 ), 395 ), 396 ); 397 }); 398 399 test('should throw ArgumentError for IPv4 address (TLD starts with digit)', () { 400 expect( 401 () => authService.validateAndNormalizeHandle('127.0.0.1'), 402 throwsA( 403 predicate( 404 (e) => 405 e is ArgumentError && 406 e.message.toString().contains('TLD') && 407 e.message.toString().contains('cannot start with a digit'), 408 ), 409 ), 410 ); 411 }); 412 413 test('should throw ArgumentError for IPv4 address variant', () { 414 expect( 415 () => authService.validateAndNormalizeHandle('192.168.0.142'), 416 throwsA( 417 predicate( 418 (e) => 419 e is ArgumentError && 420 e.message.toString().contains('TLD') && 421 e.message.toString().contains('cannot start with a digit'), 422 ), 423 ), 424 ); 425 }); 426 }); 427 428 group('DID Validation', () { 429 test('should accept valid plc DID', () { 430 final result = 431 authService.validateAndNormalizeHandle('did:plc:abc123'); 432 expect(result, 'did:plc:abc123'); 433 }); 434 435 test('should accept valid web DID', () { 436 final result = 437 authService.validateAndNormalizeHandle('did:web:example.com'); 438 expect(result, 'did:web:example.com'); 439 }); 440 441 test('should accept DID with underscores in identifier', () { 442 // Underscores are allowed in the DID pattern (part of [a-zA-Z0-9._:%-]+) 443 final result = 444 authService.validateAndNormalizeHandle('did:plc:abc_123'); 445 expect(result, 'did:plc:abc_123'); 446 }); 447 448 test('should throw ArgumentError for invalid DID with @ special chars', () { 449 expect( 450 () => authService.validateAndNormalizeHandle('did:plc:abc@123'), 451 throwsA( 452 predicate( 453 (e) => 454 e is ArgumentError && 455 e.message.toString().contains('Invalid DID format'), 456 ), 457 ), 458 ); 459 }); 460 461 test('should throw ArgumentError for DID with uppercase method', () { 462 expect( 463 () => authService.validateAndNormalizeHandle('did:PLC:abc123'), 464 throwsA( 465 predicate( 466 (e) => 467 e is ArgumentError && 468 e.message.toString().contains('Invalid DID format'), 469 ), 470 ), 471 ); 472 }); 473 474 test('should throw ArgumentError for DID with spaces', () { 475 expect( 476 () => authService.validateAndNormalizeHandle('did:plc:abc 123'), 477 throwsA( 478 predicate( 479 (e) => 480 e is ArgumentError && 481 e.message.toString().contains('Invalid DID format'), 482 ), 483 ), 484 ); 485 }); 486 487 test('should throw ArgumentError for malformed DID (missing identifier)', () { 488 expect( 489 () => authService.validateAndNormalizeHandle('did:plc'), 490 throwsA( 491 predicate( 492 (e) => 493 e is ArgumentError && 494 e.message.toString().contains('Invalid DID format'), 495 ), 496 ), 497 ); 498 }); 499 500 test('should throw ArgumentError for malformed DID (missing method)', () { 501 expect( 502 () => authService.validateAndNormalizeHandle('did::abc123'), 503 throwsA( 504 predicate( 505 (e) => 506 e is ArgumentError && 507 e.message.toString().contains('Invalid DID format'), 508 ), 509 ), 510 ); 511 }); 512 513 test('should throw ArgumentError for DID without prefix', () { 514 expect( 515 () => authService.validateAndNormalizeHandle('plc:abc123'), 516 throwsA( 517 predicate( 518 (e) => 519 e is ArgumentError && 520 e.message.toString().contains('domain format'), 521 ), 522 ), 523 ); 524 }); 525 526 test('should throw ArgumentError for DID with invalid method chars', () { 527 expect( 528 () => authService.validateAndNormalizeHandle('did:pl-c:abc123'), 529 throwsA( 530 predicate( 531 (e) => 532 e is ArgumentError && 533 e.message.toString().contains('Invalid DID format'), 534 ), 535 ), 536 ); 537 }); 538 }); 539 }); 540}