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
95 changes: 95 additions & 0 deletions test/screens/legal/cubit/legal_disclaimer_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:realunit_wallet/screens/legal/cubit/legal_disclaimer_cubit.dart';

void main() {
group('$LegalDisclaimerCubit', () {
test('initial state is step 0', () {
final cubit = LegalDisclaimerCubit();

expect(cubit.state.currentStep, 0);
expect(cubit.state.canGoBack, isFalse);
expect(cubit.state.isLastStep, isFalse);
// 5 steps total → step 0 == 1/5 = 0.2.
expect(cubit.state.progress, closeTo(0.2, 1e-9));
});

blocTest<LegalDisclaimerCubit, LegalDisclaimerState>(
'nextStep advances by one when not on the last step',
build: LegalDisclaimerCubit.new,
act: (cubit) => cubit.nextStep(),
expect: () => [
const LegalDisclaimerState(currentStep: 1),
],
);

blocTest<LegalDisclaimerCubit, LegalDisclaimerState>(
'nextStep walks from 0 all the way to the last step',
build: LegalDisclaimerCubit.new,
act: (cubit) {
for (var i = 0; i < LegalDisclaimerState.totalSteps - 1; i++) {
cubit.nextStep();
}
},
expect: () => [
const LegalDisclaimerState(currentStep: 1),
const LegalDisclaimerState(currentStep: 2),
const LegalDisclaimerState(currentStep: 3),
const LegalDisclaimerState(currentStep: 4),
],
verify: (cubit) {
expect(cubit.state.isLastStep, isTrue);
expect(cubit.state.progress, 1.0);
},
);

test('nextStep on the last step invokes onComplete and does not emit', () async {
final cubit = LegalDisclaimerCubit();
for (var i = 0; i < LegalDisclaimerState.totalSteps - 1; i++) {
cubit.nextStep();
}
final emitted = <LegalDisclaimerState>[];
final sub = cubit.stream.listen(emitted.add);

var completed = false;
cubit.nextStep(onComplete: () => completed = true);
await Future<void>.delayed(Duration.zero);
await sub.cancel();

expect(completed, isTrue);
expect(emitted, isEmpty);
expect(cubit.state.currentStep, LegalDisclaimerState.totalSteps - 1);
});

test('nextStep on the last step without onComplete is a no-op', () async {
final cubit = LegalDisclaimerCubit();
for (var i = 0; i < LegalDisclaimerState.totalSteps - 1; i++) {
cubit.nextStep();
}
final emitted = <LegalDisclaimerState>[];
final sub = cubit.stream.listen(emitted.add);

cubit.nextStep();
await Future<void>.delayed(Duration.zero);
await sub.cancel();

expect(emitted, isEmpty);
expect(cubit.state.currentStep, LegalDisclaimerState.totalSteps - 1);
});

blocTest<LegalDisclaimerCubit, LegalDisclaimerState>(
'previousStep moves back when canGoBack is true',
build: LegalDisclaimerCubit.new,
seed: () => const LegalDisclaimerState(currentStep: 2),
act: (cubit) => cubit.previousStep(),
expect: () => [const LegalDisclaimerState(currentStep: 1)],
);

blocTest<LegalDisclaimerCubit, LegalDisclaimerState>(
'previousStep at step 0 is a no-op (cannot go below zero)',
build: LegalDisclaimerCubit.new,
act: (cubit) => cubit.previousStep(),
expect: () => [],
);
});
}
162 changes: 162 additions & 0 deletions test/screens/pin/setup_pin_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import 'dart:typed_data';

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:realunit_wallet/packages/service/biometric_service.dart';
import 'package:realunit_wallet/packages/storage/secure_storage.dart';
import 'package:realunit_wallet/screens/pin/bloc/setup_pin/setup_pin_cubit.dart';

class _MockSecureStorage extends Mock implements SecureStorage {}

class _MockBiometricService extends Mock implements BiometricService {}

void main() {
late _MockSecureStorage secureStorage;
late _MockBiometricService biometricService;

setUpAll(() {
// Uint8List is a restricted type; a Fake subclass is illegal, but mocktail
// accepts a concrete instance as the registered fallback.
registerFallbackValue(Uint8List(0));
});

setUp(() {
secureStorage = _MockSecureStorage();
biometricService = _MockBiometricService();
when(() => secureStorage.setPinSalt(any())).thenAnswer((_) async {});
when(() => secureStorage.setPinHash(any())).thenAnswer((_) async {});
});

SetupPinCubit build() => SetupPinCubit(secureStorage, biometricService);

group('$SetupPinCubit', () {
test('initial state is create mode, empty pin, no mismatch, not complete', () {
final cubit = build();

expect(cubit.state.mode, SetupPinMode.create);
expect(cubit.state.currentPin, '');
expect(cubit.state.mismatch, isFalse);
expect(cubit.state.isComplete, isFalse);
});

blocTest<SetupPinCubit, SetupPinState>(
'addDigit appends a digit to the current pin',
build: build,
act: (cubit) {
cubit.addDigit(1);
cubit.addDigit(2);
},
expect: () => [
const SetupPinState(currentPin: '1'),
const SetupPinState(currentPin: '12'),
],
);

blocTest<SetupPinCubit, SetupPinState>(
'addDigit ignored when the pin is already 6 digits long',
build: build,
seed: () => const SetupPinState(currentPin: '123456'),
act: (cubit) => cubit.addDigit(7),
expect: () => [],
);

blocTest<SetupPinCubit, SetupPinState>(
'deleteDigit drops the last character and clears mismatch',
build: build,
seed: () => const SetupPinState(currentPin: '12', mismatch: true),
act: (cubit) => cubit.deleteDigit(),
expect: () => [const SetupPinState(currentPin: '1')],
);

blocTest<SetupPinCubit, SetupPinState>(
'deleteDigit on an empty pin is a no-op',
build: build,
act: (cubit) => cubit.deleteDigit(),
expect: () => [],
);

test('completing 6 digits in create mode switches to confirm with an empty pin', () async {
final cubit = build();

for (final d in [1, 2, 3, 4, 5, 6]) {
cubit.addDigit(d);
}

expect(cubit.state.mode, SetupPinMode.confirm);
expect(cubit.state.currentPin, '');
expect(cubit.state.isComplete, isFalse);
});

test('matching confirm-pin persists salt + hash and emits isComplete=true', () async {
final cubit = build();
// The cubit's stream is broadcast and does not replay past events —
// subscribe BEFORE driving the digits so we don't race the emit.
final completed = cubit.stream.firstWhere((s) => s.isComplete);

for (final d in [1, 2, 3, 4, 5, 6]) {
cubit.addDigit(d);
}
for (final d in [1, 2, 3, 4, 5, 6]) {
cubit.addDigit(d);
}
// _onPinComplete fires _confirmPin without awaiting; PBKDF2 with
// 600k iterations runs via `compute()`. On a Flutter-test isolate
// shim this can take several seconds — generous timeout.
await completed.timeout(const Duration(seconds: 30));

expect(cubit.state.isComplete, isTrue);
verify(() => secureStorage.setPinSalt(any())).called(1);
verify(() => secureStorage.setPinHash(any())).called(1);
});

test('mismatching confirm-pin resets currentPin and sets mismatch=true', () async {
final cubit = build();
final mismatched = cubit.stream.firstWhere((s) => s.mismatch);

for (final d in [1, 2, 3, 4, 5, 6]) {
cubit.addDigit(d);
}
for (final d in [9, 9, 9, 9, 9, 9]) {
cubit.addDigit(d);
}
await mismatched.timeout(const Duration(seconds: 1));

expect(cubit.state.mismatch, isTrue);
expect(cubit.state.currentPin, '');
expect(cubit.state.isComplete, isFalse);
verifyNever(() => secureStorage.setPinSalt(any()));
verifyNever(() => secureStorage.setPinHash(any()));
});

blocTest<SetupPinCubit, SetupPinState>(
'reset returns to the initial state',
build: build,
seed: () => const SetupPinState(
mode: SetupPinMode.confirm,
currentPin: '123',
mismatch: true,
),
act: (cubit) => cubit.reset(),
expect: () => [const SetupPinState()],
);

test('isBiometricAvailable delegates to BiometricService.isAvailable', () async {
when(() => biometricService.isAvailable()).thenAnswer((_) async => true);

final result = await build().isBiometricAvailable();

expect(result, isTrue);
verify(() => biometricService.isAvailable()).called(1);
});

test('enableBiometrics delegates to BiometricService.enable', () async {
when(() => biometricService.enable()).thenAnswer((_) async => true);

final result = await build().enableBiometrics();

expect(result, isTrue);
verify(() => biometricService.enable()).called(1);
});
});
}
84 changes: 84 additions & 0 deletions test/screens/restore_wallet/cubit/validate_seed_cubit_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:realunit_wallet/packages/service/wallet_service.dart';
import 'package:realunit_wallet/screens/restore_wallet/cubit/validate_seed/validate_seed_cubit.dart';

class _MockWalletService extends Mock implements WalletService {}

const _validMnemonic =
'test test test test test test test test test test test junk';

void main() {
late _MockWalletService service;

setUp(() {
service = _MockWalletService();
});

group('$ValidateSeedCubit', () {
test('initial state is uncomplete', () {
expect(ValidateSeedCubit(service).state, ValidateSeedState.uncomplete);
});

group('checkSeedLength', () {
blocTest<ValidateSeedCubit, ValidateSeedState>(
'emits complete when 12 words are all in the bip39 english wordlist',
build: () => ValidateSeedCubit(service),
act: (cubit) => cubit.checkSeedLength(_validMnemonic),
expect: () => [ValidateSeedState.complete],
);

blocTest<ValidateSeedCubit, ValidateSeedState>(
'emits uncomplete when fewer than 12 words',
build: () => ValidateSeedCubit(service),
seed: () => ValidateSeedState.complete,
act: (cubit) => cubit.checkSeedLength('test test test'),
expect: () => [ValidateSeedState.uncomplete],
);

blocTest<ValidateSeedCubit, ValidateSeedState>(
'emits uncomplete when 12 words but at least one is not in the wordlist',
build: () => ValidateSeedCubit(service),
act: (cubit) => cubit.checkSeedLength(
'test test test test test test test test test test test notaword',
),
expect: () => [ValidateSeedState.uncomplete],
);

blocTest<ValidateSeedCubit, ValidateSeedState>(
'tolerates extra whitespace between words',
build: () => ValidateSeedCubit(service),
act: (cubit) =>
// Extra inner whitespace is filtered out by the where-isNotEmpty.
cubit.checkSeedLength('test test test test test test test test test test test junk'),
expect: () => [ValidateSeedState.complete],
);
});

group('validateSeed', () {
blocTest<ValidateSeedCubit, ValidateSeedState>(
'emits valid when the underlying wallet service accepts the seed',
build: () {
when(() => service.validateSeed(any())).thenReturn(true);
return ValidateSeedCubit(service);
},
act: (cubit) => cubit.validateSeed(_validMnemonic),
expect: () => [ValidateSeedState.valid],
verify: (_) {
verify(() => service.validateSeed(_validMnemonic)).called(1);
},
);

blocTest<ValidateSeedCubit, ValidateSeedState>(
'emits invalid when the underlying wallet service rejects the seed',
build: () {
when(() => service.validateSeed(any())).thenReturn(false);
return ValidateSeedCubit(service);
},
act: (cubit) => cubit.validateSeed('garbage'),
expect: () => [ValidateSeedState.invalid],
);
});
});
}
Loading
Loading