import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:sxchnge_auth/sxchnge_auth.dart'; void main() { http.Response response(Map data, [int status = 202]) => http.Response(jsonEncode({'data': data}), status); test( 'signup distinguishes support recovery and keeps the retry key', () async { final keys = []; var failed = true; final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient((request) async { keys.add(jsonDecode(request.body)['idempotencyKey'] as String); return failed ? response({'error': 'signup_attention_required'}, 503) : response({'userId': 'user'}); }), ); addTearDown(service.close); const input = SignupRequest( name: 'Ada', email: 'ada@example.com', password: 'password preserved', locale: 'en', ); await expectLater( service.signup(input), throwsA( isA().having( (error) => error.code, 'code', AuthError.signupNeedsAssistance, ), ), ); failed = false; expect((await service.signup(input)).isSimulated, isFalse); expect(keys[0], keys[1]); }, ); test('signup receipt avoids duplicate delivery and subsequent resend reaches API', () async { final paths = []; final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient((request) async { paths.add(request.url.path); if (request.url.path.endsWith('/signup')) { expect(jsonDecode(request.body)['password'], ' password preserved '); return response({'userId': 'user'}); } return response({'accepted': true}); }), ); addTearDown(service.close); final result = await service.signup( const SignupRequest( name: 'User', email: 'USER@example.com', password: ' password preserved ', locale: 'en', ), ); expect(result.isSimulated, isFalse); await service.send(email: 'user@example.com', locale: 'en'); expect(paths, ['/api/v1/accounts/signup']); await service.send(email: 'user@example.com', locale: 'en'); expect(paths.last, '/api/v1/accounts/verification/send'); }); test('login confirms backend session only after factor and consumes local grant once', () async { final now = DateTime.now(); var verified = false; final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient((request) async { final body = jsonDecode(request.body); if (request.url.path.endsWith('/login')) { expect(body['login'], { 'login': 'user@example.com', 'password': ' password ', }); expect(body['clientId'], 'web'); return response({ 'userId': 'user', 'pendingToken': {'token': 'signed-pending'}, 'challengeId': 'server-challenge', 'channel': 'email', 'target': 'u***@example.com', 'expiresAt': now.add(const Duration(minutes: 10)).toIso8601String(), 'resendAt': now.add(const Duration(minutes: 1)).toIso8601String(), }); } expect(request.url.path, '/api/v1/accounts/login/verify'); expect(body['pendingToken'], 'signed-pending'); expect(body['challengeId'], 'server-challenge'); if (body['code'] != '123456') return response({'error': 'invalid'}, 400); verified = true; return response({ 'account': {'id': 'user'}, 'accessToken': {'token': 'access'}, 'refreshToken': {'token': 'refresh'}, }, 200); }), ); addTearDown(service.close); final result = await service.login( const LoginRequest( email: 'user@example.com', password: ' password ', locale: 'en', ), ); expect(result.challenge, isNotNull); expect(service.session, isNull); final challenge = result.challenge!; expect(challenge.id, isNot('signed-pending')); await expectLater( service.verify(challenge.id, '000000'), throwsA(isA()), ); expect(verified, isFalse); expect(service.session, isNull); final grant = await service.verify(challenge.id, '123456'); final accepted = await service.completeLogin(challenge, grant); expect(accepted.userId, 'user'); expect(accepted.isSimulated, isFalse); expect(service.session!.accessToken, 'access'); await expectLater( service.completeLogin(challenge, grant), throwsA(isA()), ); await expectLater( service.authorize(FactorPurpose.payout, 'payment', grant.token), throwsA(isA()), ); }); test( 'failed delivery preserves resend idempotency and never claims success', () async { final keys = []; final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient((request) async { keys.add(jsonDecode(request.body)['idempotencyKey'] as String); return response({'error': 'unavailable'}, 503); }), ); addTearDown(service.close); for (var i = 0; i < 2; i++) { await expectLater( service.send(email: 'user@example.com', locale: 'en'), throwsA(isA()), ); } expect(keys[0], keys[1]); }, ); test('expired activation maps to recovery state', () async { final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient((_) async => response({'error': 'expired'}, 400)), ); addTearDown(service.close); await expectLater( service.confirm(token: 'expired'), throwsA( isA().having( (e) => e.code, 'code', VerificationError.expired, ), ), ); }); test( 'verification recovery preserves assistance instead of requesting retry', () async { final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient( (_) async => response({'error': 'signup_attention_required'}, 503), ), ); addTearDown(service.close); final provider = VerificationProvider(service); addTearDown(provider.dispose); await provider.send(email: 'ada@example.com', locale: 'en'); expect(provider.state.status, VerificationStatus.error); expect(provider.state.error, VerificationError.needsAssistance); await expectLater( service.confirm(token: 'old-token'), throwsA( isA().having( (e) => e.code, 'code', VerificationError.needsAssistance, ), ), ); }, ); test( 'resend preserves server challenge and retries the same delivery key', () async { final now = DateTime.now(); final keys = []; Map challenge() => { 'userId': 'user', 'pendingToken': {'token': 'pending'}, 'challengeId': 'server-challenge', 'channel': 'email', 'target': 's***@example.com', 'expiresAt': now.add(const Duration(minutes: 10)).toIso8601String(), 'resendAt': now.add(const Duration(minutes: 1)).toIso8601String(), }; final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient((request) async { if (request.url.path.endsWith('/login')) return response(challenge()); final body = jsonDecode(request.body); expect(body['challengeId'], 'server-challenge'); expect(body['pendingToken'], 'pending'); keys.add(body['idempotencyKey'] as String); if (keys.length == 1) return response({'error': 'unavailable'}, 503); return response(challenge()); }), ); addTearDown(service.close); final result = await service.login( const LoginRequest( email: 'user@example.com', password: 'password', locale: 'en', ), ); final id = result.challenge!.id; await expectLater(service.resend(id), throwsA(isA())); final resent = await service.resend(id); expect(resent.id, id); expect(resent.target, 's***@example.com'); expect(keys[0], keys[1]); expect(service.session, isNull); }, ); for (final channel in ['sms', '']) { test( 'unsupported or missing channel $channel cannot create a login challenge', () async { final service = BackendAuthServices( baseUri: Uri.parse('https://api.example/api/v1'), clientId: 'web', client: MockClient( (_) async => response({'challengeId': 'server', 'channel': channel}), ), ); addTearDown(service.close); await expectLater( service.login( const LoginRequest( email: 'user@example.com', password: 'password', locale: 'en', ), ), throwsA(isA()), ); expect(service.session, isNull); }, ); } }