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 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'}, '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'); 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, ), ), ); }); }