import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:sxchnge_kyc/sxchnge_kyc.dart'; String response({String status = 'pending'}) => jsonEncode({ 'status': 'success', 'data': { 'status': status, 'submissionId': 'submission', 'profile': { 'fullName': 'Legal Name', 'dateOfBirth': '1990-04-23', 'residenceCountry': 'PL', }, }, }); void main() { test('an approved review with an unfinished upgrade stays pending and cannot resubmit', () async { final payload = jsonDecode(response()) as Map; (payload['data'] as Map)['upgradePending'] = true; final service = BackendIdentityService( baseUri: Uri.parse('https://api.example.test'), accessToken: () => 'token', client: MockClient((_) async => http.Response(jsonEncode(payload), 200)), ); addTearDown(service.close); final result = await service.fetch(); expect(result.status, IdentityStatus.pending); expect(result.upgradePending, true); expect(result.canSubmit, false); }); test( 'uploads profile plus both files, credentials scoped to current session', () async { final service = BackendIdentityService( baseUri: Uri.parse('https://api.example.test/api/v1'), accessToken: () => 'session-token', client: MockClient((request) async { expect(request.url.path, '/api/v1/accounts/identity'); expect(request.headers['Authorization'], 'Bearer session-token'); expect(request.followRedirects, false); expect( request.headers['content-type'], contains('multipart/form-data'), ); expect(request.body, contains('name="document"')); expect(request.body, contains('name="selfie"')); expect(request.body, contains('"fullName":"Legal Name"')); expect(request.body, contains('stable-key')); expect(request.body, isNot(contains('customer_ref'))); return http.Response(response(), 200); }), ); addTearDown(service.close); final result = await service.submit( profile: const IdentityProfile( fullName: 'Legal Name', dateOfBirth: '1990-04-23', residenceCountry: 'PL', ), passport: Photo(Uint8List.fromList([1])), selfie: Photo(Uint8List.fromList([2])), idempotencyKey: 'stable-key', ); expect(result.status, IdentityStatus.pending); expect(result.simulated, false); }, ); test('session switch rejects a late response', () async { var token = 'first'; final pending = Completer(); final service = BackendIdentityService( baseUri: Uri.parse('https://api.example.test'), accessToken: () => token, client: MockClient((_) => pending.future), ); addTearDown(service.close); final result = service.fetch(); token = 'second'; pending.complete(http.Response(response(), 200)); await expectLater( result, throwsA( isA().having( (e) => e.code, 'code', KycError.sessionExpired, ), ), ); }); test( 'production limits preserve exact decimals and never use demo rows', () async { final service = BackendIdentityService( baseUri: Uri.parse('https://api.example.test'), accessToken: () => 'token', client: MockClient((request) async { expect(request.url.path, '/accounts/limits'); return http.Response( jsonEncode({ 'status': 'success', 'data': { 'tier': 'verified', 'profileRef': 'verified-policy', 'entries': [ { 'operation': 'Transfer', 'period': 'day', 'currency': 'BTC', 'cap': '0.12345678', 'remaining': '0.10000001', 'used': '0.02', 'reserved': '0.00345677', }, ], }, }), 200, ); }), ); addTearDown(service.close); final result = await BackendLimitsService(service).fetch(); expect(result.rows, isEmpty); expect(result.simulated, false); expect(result.entries.single.cap, '0.12345678'); expect(result.tier, AccountTier.verified); }, ); test( 'unavailable, conflict and expired sessions retain typed recovery', () async { for (final pair in [ (503, KycError.unavailable), (409, KycError.conflict), (401, KycError.sessionExpired), ]) { final service = BackendIdentityService( baseUri: Uri.parse('https://api.example.test'), accessToken: () => 'token', client: MockClient( (_) async => http.Response('private upstream details', pair.$1), ), ); addTearDown(service.close); await expectLater( service.fetch(), throwsA(isA().having((e) => e.code, 'code', pair.$2)), ); } }, ); }