Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions test/packages/service/transaction_history_service_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:mocktail/mocktail.dart';
import 'package:realunit_wallet/packages/config/api_config.dart';
import 'package:realunit_wallet/packages/config/network_mode.dart';
import 'package:realunit_wallet/packages/repository/cache_repository.dart';
import 'package:realunit_wallet/packages/repository/transaction_repository.dart';
import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/service/session_cache.dart';
import 'package:realunit_wallet/packages/service/transaction_history_service.dart';

class _MockAppStore extends Mock implements AppStore {}

class _MockCacheRepository extends Mock implements CacheRepository {}

class _MockTransactionRepository extends Mock implements TransactionRepository {}

const _wallet = '0x000000000000000000000000000000000000beef';
const _other = '0x0000000000000000000000000000000000001234';

Map<String, dynamic> _txJson({
int id = 1,
String state = 'Processing',
String? source = _wallet,
String? target,
}) =>
{
'id': id,
'type': 'Buy',
'state': state,
'rate': 1.0,
'inputAmount': 100.0,
'inputAsset': 'CHF',
'inputTxId': null,
'outputAmount': 1.0,
'outputAsset': 'REALU',
'outputTxId': null,
'sourceAccount': source,
'targetAccount': target,
};

void main() {
late _MockAppStore appStore;
late SessionCache sessionCache;
late _MockTransactionRepository txRepo;

setUp(() {
appStore = _MockAppStore();
sessionCache = SessionCache(_MockCacheRepository());
txRepo = _MockTransactionRepository();
when(() => appStore.sessionCache).thenReturn(sessionCache);
when(() => appStore.apiConfig)
.thenReturn(const ApiConfig(networkMode: NetworkMode.mainnet));
when(() => appStore.primaryAddress).thenReturn(_wallet);
});

TransactionHistoryService build(http.Client client) {
when(() => appStore.httpClient).thenReturn(client);
return TransactionHistoryService(appStore, txRepo);
}

group('$TransactionHistoryService', () {
group('fetchPendingTransactions', () {
test('returns [] without hitting the network when no auth token is set', () async {
var called = false;
final client = MockClient((_) async {
called = true;
return http.Response('[]', 200);
});

final list = await build(client).fetchPendingTransactions();

expect(list, isEmpty);
expect(called, isFalse);
});

test('GETs /v1/transaction/detail with the Bearer JWT', () async {
sessionCache.setAuthToken('jwt-1');
String? auth;
String? path;
final client = MockClient((request) async {
auth = request.headers['Authorization'];
path = request.url.path;
return http.Response(jsonEncode([_txJson()]), 200);
});

await build(client).fetchPendingTransactions();

expect(auth, 'Bearer jwt-1');
expect(path, '/v1/transaction/detail');
});

test('returns [] on non-200 (does not throw)', () async {
sessionCache.setAuthToken('jwt-1');
final client = MockClient((_) async => http.Response('boom', 500));

final list = await build(client).fetchPendingTransactions();

expect(list, isEmpty);
});

test('filters out completed transactions (isPending=false)', () async {
sessionCache.setAuthToken('jwt-1');
final client = MockClient((_) async => http.Response(
jsonEncode([
_txJson(id: 1, state: 'Processing'),
_txJson(id: 2, state: 'Completed'),
_txJson(id: 3, state: 'Failed'),
_txJson(id: 4, state: 'Returned'),
_txJson(id: 5, state: 'CheckPending'),
]),
200,
));

final list = await build(client).fetchPendingTransactions();

expect(list.map((t) => t.id), [1, 5]);
});

test('filters out transactions that do not belong to the current wallet', () async {
sessionCache.setAuthToken('jwt-1');
final client = MockClient((_) async => http.Response(
jsonEncode([
_txJson(id: 1, source: _wallet),
_txJson(id: 2, source: _other, target: _other),
_txJson(id: 3, source: null, target: _wallet),
]),
200,
));

final list = await build(client).fetchPendingTransactions();

// 1 (source = wallet) and 3 (target = wallet) belong, 2 doesn't.
expect(list.map((t) => t.id), [1, 3]);
});

test('wallet match is case-insensitive', () async {
sessionCache.setAuthToken('jwt-1');
when(() => appStore.primaryAddress).thenReturn(_wallet.toUpperCase());
final client = MockClient((_) async => http.Response(
jsonEncode([_txJson(id: 1, source: _wallet)]),
200,
));

final list = await build(client).fetchPendingTransactions();

expect(list, hasLength(1));
});
});
});
}
222 changes: 222 additions & 0 deletions test/screens/settings_user_data/settings_user_data_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_country_service.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/country/country.dart';
import 'package:realunit_wallet/packages/service/dfx/models/kyc/dto/kyc_level_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/models/kyc/dto/kyc_step_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/models/kyc/kyc_level.dart';
import 'package:realunit_wallet/packages/service/dfx/models/registration/kyc/kyc_personal_data.dart';
import 'package:realunit_wallet/packages/service/dfx/models/user/dto/real_unit_user_data_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/models/user/dto/user_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/models/wallet/real_unit_wallet_status_dto.dart';
import 'package:realunit_wallet/packages/service/dfx/real_unit_wallet_service.dart';
import 'package:realunit_wallet/screens/settings_user_data/cubit/settings_user_data_cubit.dart';

class _MockWalletService extends Mock implements RealUnitWalletService {}

class _MockCountryService extends Mock implements DfxCountryService {}

class _MockKycService extends Mock implements DfxKycService {}

const _ch = Country(id: 41, symbol: 'CH', name: 'Switzerland');
const _de = Country(id: 49, symbol: 'DE', name: 'Germany');

const _address = KycAddress(
street: 'Teststrasse',
houseNumber: '1',
zip: '8000',
city: 'Zurich',
country: 41,
);

const _kycData = KycPersonalData(
accountType: KycAccountType.personal,
firstName: 'Test',
lastName: 'User',
phone: '+41790000000',
address: _address,
);

RealUnitUserDataDto _userData({
String nationality = 'CH',
String addressCountry = 'CH',
}) =>
RealUnitUserDataDto(
email: 'a@b.com',
name: 'Test User',
type: 'HUMAN',
phoneNumber: '+41790000000',
birthday: '1990-01-15',
nationality: nationality,
addressStreet: 'Teststrasse 1',
addressPostalCode: '8000',
addressCity: 'Zurich',
addressCountry: addressCountry,
swissTaxResidence: true,
lang: 'de',
kycData: _kycData,
);

KycStepDto _step(KycStepName name, KycStepStatus status, {int seq = 0}) =>
KycStepDto(
name: name,
status: status,
sequenceNumber: seq,
isCurrent: false,
);

void main() {
late _MockWalletService walletService;
late _MockCountryService countryService;
late _MockKycService kycService;

setUp(() {
walletService = _MockWalletService();
countryService = _MockCountryService();
kycService = _MockKycService();
});

SettingsUserDataCubit build() => SettingsUserDataCubit(
walletService: walletService,
countryService: countryService,
kycService: kycService,
);

// Cubit fires getUserData() in its constructor; we assert the final
// state via stream.firstWhere rather than the full sequence.
group('$SettingsUserDataCubit', () {
test('full Success when userData is present and no change steps are pending', () async {
when(() => walletService.getWalletStatus()).thenAnswer(
(_) async => RealUnitWalletStatusDto(
isRegistered: true,
realUnitUserDataDto: _userData(addressCountry: 'DE'),
),
);
when(() => kycService.getKycStatus()).thenAnswer(
(_) async => const KycLevelDto(kycLevel: KycLevel.level20, kycSteps: []),
);
when(() => countryService.getCountryBySymbol('CH')).thenAnswer((_) async => _ch);
when(() => countryService.getCountryBySymbol('DE')).thenAnswer((_) async => _de);

final cubit = build();
await cubit.stream.firstWhere((s) => s is SettingsUserDataSuccess);

final success = cubit.state as SettingsUserDataSuccess;
expect(success.userData, isNotNull);
expect(success.userData!.email, 'a@b.com');
expect(success.userData!.nationality, _ch);
expect(success.userData!.addressCountry, _de);
expect(success.pendingSteps, isEmpty);
});

test('Success surfaces pending change steps that are inReview', () async {
when(() => walletService.getWalletStatus()).thenAnswer(
(_) async => RealUnitWalletStatusDto(
isRegistered: true,
realUnitUserDataDto: _userData(),
),
);
when(() => kycService.getKycStatus()).thenAnswer(
(_) async => KycLevelDto(
kycLevel: KycLevel.level20,
kycSteps: [
_step(KycStepName.nameChange, KycStepStatus.inReview),
_step(KycStepName.addressChange, KycStepStatus.notStarted),
_step(KycStepName.phoneChange, KycStepStatus.inReview, seq: 1),
// Non-change-step in review is ignored.
_step(KycStepName.contactData, KycStepStatus.inReview, seq: 2),
],
),
);
when(() => countryService.getCountryBySymbol(any())).thenAnswer((_) async => _ch);

final cubit = build();
await cubit.stream.firstWhere((s) => s is SettingsUserDataSuccess);

final success = cubit.state as SettingsUserDataSuccess;
expect(success.pendingSteps, {KycStepName.nameChange, KycStepName.phoneChange});
});

test('userData null + getUser returns mail → Success(email)', () async {
when(() => walletService.getWalletStatus()).thenAnswer(
(_) async => RealUnitWalletStatusDto(isRegistered: false),
);
when(() => kycService.getKycStatus()).thenAnswer(
(_) async => const KycLevelDto(kycLevel: KycLevel.level0, kycSteps: []),
);
when(() => kycService.getUser()).thenAnswer(
(_) async => const UserDto(
mail: 'fallback@b.com',
kyc: UserKycDto(
hash: 'h',
level: KycLevel.level0,
dataComplete: false,
),
),
);

final cubit = build();
await cubit.stream.firstWhere((s) => s is SettingsUserDataSuccess);

final success = cubit.state as SettingsUserDataSuccess;
expect(success.userData, isNull);
expect(success.email, 'fallback@b.com');
// No country lookups when userData is missing.
verifyNever(() => countryService.getCountryBySymbol(any()));
});

test('userData null + getUser throws → Success() with no email', () async {
when(() => walletService.getWalletStatus()).thenAnswer(
(_) async => RealUnitWalletStatusDto(isRegistered: false),
);
when(() => kycService.getKycStatus()).thenAnswer(
(_) async => const KycLevelDto(kycLevel: KycLevel.level0, kycSteps: []),
);
when(() => kycService.getUser())
.thenAnswer((_) async => throw Exception('no user'));

final cubit = build();
await cubit.stream.firstWhere((s) => s is SettingsUserDataSuccess);

final success = cubit.state as SettingsUserDataSuccess;
expect(success.userData, isNull);
expect(success.email, isNull);
});

test('Failure when walletService.getWalletStatus throws', () async {
when(() => walletService.getWalletStatus())
.thenAnswer((_) async => throw Exception('network'));
when(() => kycService.getKycStatus()).thenAnswer(
(_) async => const KycLevelDto(kycLevel: KycLevel.level0, kycSteps: []),
);

final cubit = build();
await cubit.stream.firstWhere((s) => s is SettingsUserDataFailure);

expect((cubit.state as SettingsUserDataFailure).message, contains('network'));
});

test('Failure when countryService.getCountryBySymbol throws', () async {
when(() => walletService.getWalletStatus()).thenAnswer(
(_) async => RealUnitWalletStatusDto(
isRegistered: true,
realUnitUserDataDto: _userData(),
),
);
when(() => kycService.getKycStatus()).thenAnswer(
(_) async => const KycLevelDto(kycLevel: KycLevel.level20, kycSteps: []),
);
when(() => countryService.getCountryBySymbol(any()))
.thenAnswer((_) async => throw Exception('unknown country'));

final cubit = build();
await cubit.stream.firstWhere((s) => s is SettingsUserDataFailure);

expect(
(cubit.state as SettingsUserDataFailure).message,
contains('unknown country'),
);
});
});
}
Loading