Added test for login page

This commit is contained in:
Anton Stubenbord
2022-12-05 19:15:00 +01:00
parent 0a63259693
commit d79682a011
20 changed files with 444 additions and 50 deletions

View File

@@ -7,28 +7,30 @@ import 'package:injectable/injectable.dart';
@singleton
class ConnectivityCubit extends Cubit<ConnectivityState> {
final ConnectivityStatusService connectivityStatusService;
late final StreamSubscription<bool> _sub;
StreamSubscription<bool>? _sub;
ConnectivityCubit(this.connectivityStatusService)
: super(ConnectivityState.undefined);
Future<void> initialize() async {
final bool isConnected =
await connectivityStatusService.isConnectedToInternet();
emit(isConnected
? ConnectivityState.connected
: ConnectivityState.notConnected);
_sub =
connectivityStatusService.connectivityChanges().listen((isConnected) {
if (_sub == null) {
final bool isConnected =
await connectivityStatusService.isConnectedToInternet();
emit(isConnected
? ConnectivityState.connected
: ConnectivityState.notConnected);
});
_sub =
connectivityStatusService.connectivityChanges().listen((isConnected) {
emit(isConnected
? ConnectivityState.connected
: ConnectivityState.notConnected);
});
}
}
@override
Future<void> close() {
_sub.cancel();
_sub?.cancel();
return super.close();
}
}

View File

@@ -7,6 +7,8 @@ import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
@injectable
@dev
@prod
class AuthenticationInterceptor implements InterceptorContract {
final LocalVault _localVault;
AuthenticationInterceptor(this._localVault);

View File

@@ -5,6 +5,8 @@ import 'package:injectable/injectable.dart';
const interceptedRoutes = ['thumb/'];
@injectable
@dev
@prod
class ResponseConversionInterceptor implements InterceptorContract {
@override
Future<BaseRequest> interceptRequest({required BaseRequest request}) async =>

View File

@@ -13,6 +13,8 @@ import 'package:injectable/injectable.dart';
/// Convenience class which handles timeout errors.
///
@Injectable(as: BaseClient)
@dev
@prod
@Named("timeoutClient")
class TimeoutClient implements BaseClient {
final ConnectivityStatusService connectivityStatusService;

View File

@@ -9,7 +9,7 @@ abstract class ConnectivityStatusService {
Stream<bool> connectivityChanges();
}
@Injectable(as: ConnectivityStatusService)
@Injectable(as: ConnectivityStatusService, env: ['prod', 'dev'])
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
final Connectivity connectivity;

View File

@@ -8,15 +8,27 @@ import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:injectable/injectable.dart';
@singleton
class LocalVault {
abstract class LocalVault {
Future<void> storeAuthenticationInformation(AuthenticationInformation auth);
Future<AuthenticationInformation?> loadAuthenticationInformation();
Future<ClientCertificate?> loadCertificate();
Future<bool> storeApplicationSettings(ApplicationSettingsState settings);
Future<ApplicationSettingsState?> loadApplicationSettings();
Future<void> clear();
}
@Injectable(as: LocalVault)
@prod
@dev
class LocalVaultImpl implements LocalVault {
static const applicationSettingsKey = "applicationSettings";
static const authenticationKey = "authentication";
final EncryptedSharedPreferences sharedPreferences;
LocalVault(this.sharedPreferences);
LocalVaultImpl(this.sharedPreferences);
@override
Future<void> storeAuthenticationInformation(
AuthenticationInformation auth,
) async {
@@ -26,6 +38,7 @@ class LocalVault {
);
}
@override
Future<AuthenticationInformation?> loadAuthenticationInformation() async {
if ((await sharedPreferences.getString(authenticationKey)).isEmpty) {
return null;
@@ -35,11 +48,13 @@ class LocalVault {
);
}
@override
Future<ClientCertificate?> loadCertificate() async {
return loadAuthenticationInformation()
.then((value) => value?.clientCertificate);
}
@override
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
return sharedPreferences.setString(
applicationSettingsKey,
@@ -47,6 +62,7 @@ class LocalVault {
);
}
@override
Future<ApplicationSettingsState?> loadApplicationSettings() async {
final settings = await sharedPreferences.getString(applicationSettingsKey);
if (settings.isEmpty) {
@@ -58,6 +74,7 @@ class LocalVault {
);
}
@override
Future<void> clear() {
return sharedPreferences.clear();
}

View File

@@ -7,13 +7,13 @@ import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final getIt = GetIt.instance..allowReassignment;
@InjectableInit(
initializerName: r'$initGetIt', // default
preferRelativeImports: true, // default
asExtension: false, // default
)
void configureDependencies() => $initGetIt(getIt);
void configureDependencies(String environment) =>
$initGetIt(getIt, environment: environment);
///
/// Registers new security context, which will be used by the HttpClient, see [RegisterModule].

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart';
@@ -16,18 +17,33 @@ import 'package:local_auth/local_auth.dart';
@module
abstract class RegisterModule {
@singleton
@dev
@prod
LocalAuthentication get localAuthentication => LocalAuthentication();
@singleton
@dev
@prod
EncryptedSharedPreferences get encryptedSharedPreferences =>
EncryptedSharedPreferences();
@singleton
@dev
@prod
@test
SecurityContext get securityContext => SecurityContext();
@singleton
@dev
@prod
Connectivity get connectivity => Connectivity();
///
/// Factory method creating an [HttpClient] with the currently registered [SecurityContext].
///
@injectable
@dev
@prod
HttpClient getHttpClient(SecurityContext securityContext) =>
HttpClient(context: securityContext)
..connectionTimeout = const Duration(seconds: 10);
@@ -35,6 +51,9 @@ abstract class RegisterModule {
///
/// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient].
///
@injectable
@dev
@prod
BaseClient getBaseClient(
AuthenticationInterceptor authInterceptor,
ResponseConversionInterceptor responseConversionInterceptor,
@@ -50,28 +69,46 @@ abstract class RegisterModule {
client: IOClient(client),
);
@injectable
@dev
@prod
CacheManager getCacheManager(BaseClient client) => CacheManager(
Config('cacheKey', fileService: HttpFileService(httpClient: client)));
@injectable
@dev
@prod
PaperlessAuthenticationApi authenticationModule(BaseClient client) =>
PaperlessAuthenticationApiImpl(client);
@injectable
@dev
@prod
PaperlessLabelsApi labelsModule(
@Named('timeoutClient') BaseClient timeoutClient,
) =>
PaperlessLabelApiImpl(timeoutClient);
@injectable
@dev
@prod
PaperlessDocumentsApi documentsModule(
@Named('timeoutClient') BaseClient timeoutClient,
HttpClient httpClient,
) =>
PaperlessDocumentsApiImpl(timeoutClient, httpClient);
@injectable
@dev
@prod
PaperlessSavedViewsApi savedViewsModule(
@Named('timeoutClient') BaseClient timeoutClient,
) =>
PaperlessSavedViewsApiImpl(timeoutClient);
@injectable
@dev
@prod
PaperlessServerStatsApi serverStatsModule(
@Named('timeoutClient') BaseClient timeoutClient,
) =>

69
lib/di_test_mocks.dart Normal file
View File

@@ -0,0 +1,69 @@
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:injectable/injectable.dart';
import 'package:local_auth/local_auth.dart';
import 'package:mockito/annotations.dart';
@GenerateNiceMocks([
MockSpec<PaperlessDocumentsApi>(),
MockSpec<PaperlessLabelsApi>(),
MockSpec<PaperlessSavedViewsApi>(),
MockSpec<PaperlessAuthenticationApi>(),
MockSpec<PaperlessServerStatsApi>(),
MockSpec<LocalVault>(),
MockSpec<EncryptedSharedPreferences>(),
MockSpec<ConnectivityStatusService>(),
MockSpec<LocalAuthentication>(),
])
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'di_test_mocks.mocks.dart';
@module
abstract class DiMocksModule {
// All fields must be singleton in order to verify behavior in tests.
@singleton
@test
CacheManager get testCacheManager => CacheManager(Config('testKey'));
@singleton
@test
PaperlessDocumentsApi get mockDocumentsApi => MockPaperlessDocumentsApi();
@singleton
@test
PaperlessLabelsApi get mockLabelsApi => MockPaperlessLabelsApi();
@singleton
@test
PaperlessSavedViewsApi get mockSavedViewsApi => MockPaperlessSavedViewsApi();
@singleton
@test
PaperlessAuthenticationApi get mockAuthenticationApi =>
MockPaperlessAuthenticationApi();
@singleton
@test
PaperlessServerStatsApi get mockServerStatsApi =>
MockPaperlessServerStatsApi();
@singleton
@test
LocalVault get mockLocalVault => MockLocalVault();
@singleton
@test
EncryptedSharedPreferences get mockSharedPreferences =>
MockEncryptedSharedPreferences();
@singleton
@test
ConnectivityStatusService get mockConnectivityStatusService =>
MockConnectivityStatusService();
@singleton
@test
LocalAuthentication get localAuthentication => MockLocalAuthentication();
}

View File

@@ -72,6 +72,7 @@ class _LoginPageState extends State<LoginPage> {
Widget _buildLoginButton() {
return ElevatedButton(
key: const ValueKey('login-login-button'),
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.primaryContainer,

View File

@@ -23,6 +23,7 @@ class _ClientCertificateFormFieldState
@override
Widget build(BuildContext context) {
return FormBuilderField<ClientCertificate?>(
key: const ValueKey('login-client-cert'),
initialValue: null,
validator: (value) {
if (value == null) {
@@ -70,6 +71,7 @@ class _ClientCertificateFormFieldState
),
if (_selectedFile != null) ...[
ObscuredInputTextFormField(
key: const ValueKey('login-client-cert-passphrase'),
initialValue: field.value?.passphrase,
onChanged: (value) => field.didChange(
field.value?.copyWith(passphrase: value),

View File

@@ -21,6 +21,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
@override
Widget build(BuildContext context) {
return FormBuilderTextField(
key: const ValueKey('login-server-address'),
name: ServerAddressFormField.fkServerAddress,
validator: FormBuilderValidators.required(
errorText: S.of(context).loginPageServerUrlValidatorMessageText,

View File

@@ -24,6 +24,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
child: Column(
children: [
TextFormField(
key: const ValueKey('login-username'),
textCapitalization: TextCapitalization.words,
autovalidateMode: AutovalidateMode.onUserInteraction,
// USERNAME
@@ -41,6 +42,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
),
),
ObscuredInputTextFormField(
key: const ValueKey('login-password'),
label: S.of(context).loginPagePasswordFieldLabel,
onChanged: (password) => field.didChange(
field.value?.copyWith(password: password) ??

View File

@@ -33,7 +33,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
void main() async {
Future<void> startAppProd() async {
Bloc.observer = BlocChangesObserver();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@@ -42,25 +42,22 @@ void main() async {
// Required for self signed client certificates
HttpOverrides.global = X509HttpOverrides();
configureDependencies();
configureDependencies('prod');
// Remove temporarily downloaded files.
(await FileService.temporaryDirectory).deleteSync(recursive: true);
kPackageInfo = await PackageInfo.fromPlatform();
// Load application settings and stored authentication data
getIt<ConnectivityCubit>().initialize();
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize();
// Preload asset images
// WARNING: This seems to bloat up the app up to almost 200mb!
// await Future.forEach<AssetImage>(
// AssetImages.values.map((e) => e.image),
// (img) => loadImage(img),
// );
runApp(const PaperlessMobileEntrypoint());
}
void main() async {
await startAppProd();
}
class PaperlessMobileEntrypoint extends StatefulWidget {
const PaperlessMobileEntrypoint({Key? key}) : super(key: key);
@@ -114,11 +111,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
),
),
themeMode: settings.preferredThemeMode,
supportedLocales: const [
Locale('en'), // Default if system locale is not available
Locale('de'),
Locale('cs'),
],
supportedLocales: S.delegate.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag),
localizationsDelegates: const [