mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 09:15:48 -06:00
Added test for login page
This commit is contained in:
@@ -53,6 +53,7 @@ android {
|
|||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -77,4 +78,8 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:integration_test/integration_test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
testWidgets('screenshot', (WidgetTester tester) async {
|
|
||||||
// Build the app.
|
|
||||||
|
|
||||||
// This is required prior to taking the screenshot (Android only).
|
|
||||||
await binding.convertFlutterSurfaceToImage();
|
|
||||||
|
|
||||||
// Trigger a frame.
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await binding.takeScreenshot('screenshot-1');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
235
integration_test/login_integration_test.dart
Normal file
235
integration_test/login_integration_test.dart
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
|
||||||
|
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||||
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
|
import 'package:paperless_mobile/di_test_mocks.mocks.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
|
import 'src/framework.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
final t = await initializeTestingFramework(languageCode: 'de');
|
||||||
|
|
||||||
|
const testServerUrl = 'https://example.com';
|
||||||
|
const testUsername = 'user';
|
||||||
|
const testPassword = 'pass';
|
||||||
|
|
||||||
|
final serverAddressField = find.byKey(const ValueKey('login-server-address'));
|
||||||
|
final usernameField = find.byKey(const ValueKey('login-username'));
|
||||||
|
final passwordField = find.byKey(const ValueKey('login-password'));
|
||||||
|
final loginBtn = find.byKey(const ValueKey('login-login-button'));
|
||||||
|
|
||||||
|
testWidgets('Test successful login flow', (WidgetTester tester) async {
|
||||||
|
await initAndLaunchTestApp(tester, () async {
|
||||||
|
// Initialize dat for mocked classes
|
||||||
|
when((getIt<ConnectivityStatusService>()).connectivityChanges())
|
||||||
|
.thenAnswer((i) => Stream.value(true));
|
||||||
|
when((getIt<LocalVault>() as MockLocalVault)
|
||||||
|
.loadAuthenticationInformation())
|
||||||
|
.thenAnswer((realInvocation) async => null);
|
||||||
|
when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
|
||||||
|
.thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||||
|
preferredLocaleSubtag: 'en',
|
||||||
|
preferredThemeMode: ThemeMode.light,
|
||||||
|
isLocalAuthenticationEnabled: false,
|
||||||
|
preferredViewType: ViewType.list,
|
||||||
|
showInboxOnStartup: false,
|
||||||
|
));
|
||||||
|
when(getIt<PaperlessAuthenticationApi>().login(
|
||||||
|
username: testUsername,
|
||||||
|
password: testPassword,
|
||||||
|
serverUrl: testServerUrl,
|
||||||
|
)).thenAnswer((i) => Future.value("eyTestToken"));
|
||||||
|
|
||||||
|
await getIt<ConnectivityCubit>().initialize();
|
||||||
|
await getIt<ApplicationSettingsCubit>().initialize();
|
||||||
|
await getIt<AuthenticationCubit>().initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mocked classes
|
||||||
|
|
||||||
|
await t.binding.waitUntilFirstFrameRasterized;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(serverAddressField, testServerUrl);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(usernameField, testUsername);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(passwordField, testPassword);
|
||||||
|
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(loginBtn);
|
||||||
|
|
||||||
|
verify(getIt<PaperlessAuthenticationApi>().login(
|
||||||
|
username: testUsername,
|
||||||
|
password: testPassword,
|
||||||
|
serverUrl: testServerUrl,
|
||||||
|
)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Test login validation missing password',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await initAndLaunchTestApp(tester, () async {
|
||||||
|
when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
|
||||||
|
.connectivityChanges())
|
||||||
|
.thenAnswer((i) => Stream.value(true));
|
||||||
|
when((getIt<LocalVault>() as MockLocalVault)
|
||||||
|
.loadAuthenticationInformation())
|
||||||
|
.thenAnswer((realInvocation) async => null);
|
||||||
|
|
||||||
|
when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
|
||||||
|
.thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||||
|
preferredLocaleSubtag: 'en',
|
||||||
|
preferredThemeMode: ThemeMode.light,
|
||||||
|
isLocalAuthenticationEnabled: false,
|
||||||
|
preferredViewType: ViewType.list,
|
||||||
|
showInboxOnStartup: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
await getIt<ConnectivityCubit>().initialize();
|
||||||
|
await getIt<ApplicationSettingsCubit>().initialize();
|
||||||
|
await getIt<AuthenticationCubit>().initialize();
|
||||||
|
});
|
||||||
|
// Mocked classes
|
||||||
|
|
||||||
|
// Initialize dat for mocked classes
|
||||||
|
|
||||||
|
await t.binding.waitUntilFirstFrameRasterized;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(serverAddressField, testServerUrl);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(usernameField, testUsername);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(loginBtn);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verifyNever(
|
||||||
|
(getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
|
||||||
|
.login(
|
||||||
|
username: testUsername,
|
||||||
|
password: testPassword,
|
||||||
|
serverUrl: testServerUrl,
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
find.textContaining(t.translations.loginPagePasswordValidatorMessageText),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Test login validation missing username',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await initAndLaunchTestApp(tester, () async {
|
||||||
|
when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
|
||||||
|
.connectivityChanges())
|
||||||
|
.thenAnswer((i) => Stream.value(true));
|
||||||
|
when((getIt<LocalVault>() as MockLocalVault)
|
||||||
|
.loadAuthenticationInformation())
|
||||||
|
.thenAnswer((realInvocation) async => null);
|
||||||
|
when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
|
||||||
|
.thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||||
|
preferredLocaleSubtag: 'en',
|
||||||
|
preferredThemeMode: ThemeMode.light,
|
||||||
|
isLocalAuthenticationEnabled: false,
|
||||||
|
preferredViewType: ViewType.list,
|
||||||
|
showInboxOnStartup: false,
|
||||||
|
));
|
||||||
|
await getIt<ConnectivityCubit>().initialize();
|
||||||
|
await getIt<ApplicationSettingsCubit>().initialize();
|
||||||
|
await getIt<AuthenticationCubit>().initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.binding.waitUntilFirstFrameRasterized;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(serverAddressField, testServerUrl);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(passwordField, testPassword);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(loginBtn);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verifyNever(
|
||||||
|
(getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
|
||||||
|
.login(
|
||||||
|
username: testUsername,
|
||||||
|
password: testPassword,
|
||||||
|
serverUrl: testServerUrl,
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
find.textContaining(t.translations.loginPageUsernameValidatorMessageText),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Test login validation missing server address',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
initAndLaunchTestApp(tester, () async {
|
||||||
|
when((getIt<ConnectivityStatusService>()).connectivityChanges())
|
||||||
|
.thenAnswer((i) => Stream.value(true));
|
||||||
|
|
||||||
|
when((getIt<LocalVault>()).loadAuthenticationInformation())
|
||||||
|
.thenAnswer((realInvocation) async => null);
|
||||||
|
|
||||||
|
when((getIt<LocalVault>()).loadApplicationSettings())
|
||||||
|
.thenAnswer((realInvocation) async => ApplicationSettingsState(
|
||||||
|
preferredLocaleSubtag: 'en',
|
||||||
|
preferredThemeMode: ThemeMode.light,
|
||||||
|
isLocalAuthenticationEnabled: false,
|
||||||
|
preferredViewType: ViewType.list,
|
||||||
|
showInboxOnStartup: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
await getIt<ConnectivityCubit>().initialize();
|
||||||
|
await getIt<ApplicationSettingsCubit>().initialize();
|
||||||
|
await getIt<AuthenticationCubit>().initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.binding.waitUntilFirstFrameRasterized;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(usernameField, testUsername);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.enterText(passwordField, testPassword);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(loginBtn);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
verifyNever(getIt<PaperlessAuthenticationApi>().login(
|
||||||
|
username: testUsername,
|
||||||
|
password: testPassword,
|
||||||
|
serverUrl: testServerUrl,
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
find.textContaining(
|
||||||
|
t.translations.loginPageServerUrlValidatorMessageText),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
41
integration_test/src/framework.dart
Normal file
41
integration_test/src/framework.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:paperless_mobile/di_initializer.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
import 'package:paperless_mobile/main.dart';
|
||||||
|
|
||||||
|
Future<TestingFrameworkVariables> initializeTestingFramework(
|
||||||
|
{String languageCode = 'en'}) async {
|
||||||
|
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
configureDependencies('test');
|
||||||
|
final translations = await S.load(
|
||||||
|
Locale.fromSubtags(
|
||||||
|
languageCode: languageCode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return TestingFrameworkVariables(
|
||||||
|
binding: binding,
|
||||||
|
translations: translations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestingFrameworkVariables {
|
||||||
|
final IntegrationTestWidgetsFlutterBinding binding;
|
||||||
|
final S translations;
|
||||||
|
|
||||||
|
TestingFrameworkVariables({
|
||||||
|
required this.binding,
|
||||||
|
required this.translations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initAndLaunchTestApp(
|
||||||
|
WidgetTester tester,
|
||||||
|
Future<void> Function() initializationCallback,
|
||||||
|
) async {
|
||||||
|
await initializationCallback();
|
||||||
|
runApp(const PaperlessMobileEntrypoint());
|
||||||
|
}
|
||||||
@@ -7,28 +7,30 @@ import 'package:injectable/injectable.dart';
|
|||||||
@singleton
|
@singleton
|
||||||
class ConnectivityCubit extends Cubit<ConnectivityState> {
|
class ConnectivityCubit extends Cubit<ConnectivityState> {
|
||||||
final ConnectivityStatusService connectivityStatusService;
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
late final StreamSubscription<bool> _sub;
|
StreamSubscription<bool>? _sub;
|
||||||
|
|
||||||
ConnectivityCubit(this.connectivityStatusService)
|
ConnectivityCubit(this.connectivityStatusService)
|
||||||
: super(ConnectivityState.undefined);
|
: super(ConnectivityState.undefined);
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
final bool isConnected =
|
if (_sub == null) {
|
||||||
await connectivityStatusService.isConnectedToInternet();
|
final bool isConnected =
|
||||||
emit(isConnected
|
await connectivityStatusService.isConnectedToInternet();
|
||||||
? ConnectivityState.connected
|
|
||||||
: ConnectivityState.notConnected);
|
|
||||||
_sub =
|
|
||||||
connectivityStatusService.connectivityChanges().listen((isConnected) {
|
|
||||||
emit(isConnected
|
emit(isConnected
|
||||||
? ConnectivityState.connected
|
? ConnectivityState.connected
|
||||||
: ConnectivityState.notConnected);
|
: ConnectivityState.notConnected);
|
||||||
});
|
_sub =
|
||||||
|
connectivityStatusService.connectivityChanges().listen((isConnected) {
|
||||||
|
emit(isConnected
|
||||||
|
? ConnectivityState.connected
|
||||||
|
: ConnectivityState.notConnected);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_sub.cancel();
|
_sub?.cancel();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:http_interceptor/http_interceptor.dart';
|
|||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
@injectable
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
class AuthenticationInterceptor implements InterceptorContract {
|
class AuthenticationInterceptor implements InterceptorContract {
|
||||||
final LocalVault _localVault;
|
final LocalVault _localVault;
|
||||||
AuthenticationInterceptor(this._localVault);
|
AuthenticationInterceptor(this._localVault);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:injectable/injectable.dart';
|
|||||||
const interceptedRoutes = ['thumb/'];
|
const interceptedRoutes = ['thumb/'];
|
||||||
|
|
||||||
@injectable
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
class ResponseConversionInterceptor implements InterceptorContract {
|
class ResponseConversionInterceptor implements InterceptorContract {
|
||||||
@override
|
@override
|
||||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async =>
|
Future<BaseRequest> interceptRequest({required BaseRequest request}) async =>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import 'package:injectable/injectable.dart';
|
|||||||
/// Convenience class which handles timeout errors.
|
/// Convenience class which handles timeout errors.
|
||||||
///
|
///
|
||||||
@Injectable(as: BaseClient)
|
@Injectable(as: BaseClient)
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
@Named("timeoutClient")
|
@Named("timeoutClient")
|
||||||
class TimeoutClient implements BaseClient {
|
class TimeoutClient implements BaseClient {
|
||||||
final ConnectivityStatusService connectivityStatusService;
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ abstract class ConnectivityStatusService {
|
|||||||
Stream<bool> connectivityChanges();
|
Stream<bool> connectivityChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable(as: ConnectivityStatusService)
|
@Injectable(as: ConnectivityStatusService, env: ['prod', 'dev'])
|
||||||
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||||
final Connectivity connectivity;
|
final Connectivity connectivity;
|
||||||
|
|
||||||
|
|||||||
@@ -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:paperless_mobile/features/settings/model/application_settings_state.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
@singleton
|
abstract class LocalVault {
|
||||||
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 applicationSettingsKey = "applicationSettings";
|
||||||
static const authenticationKey = "authentication";
|
static const authenticationKey = "authentication";
|
||||||
|
|
||||||
final EncryptedSharedPreferences sharedPreferences;
|
final EncryptedSharedPreferences sharedPreferences;
|
||||||
|
|
||||||
LocalVault(this.sharedPreferences);
|
LocalVaultImpl(this.sharedPreferences);
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> storeAuthenticationInformation(
|
Future<void> storeAuthenticationInformation(
|
||||||
AuthenticationInformation auth,
|
AuthenticationInformation auth,
|
||||||
) async {
|
) async {
|
||||||
@@ -26,6 +38,7 @@ class LocalVault {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<AuthenticationInformation?> loadAuthenticationInformation() async {
|
Future<AuthenticationInformation?> loadAuthenticationInformation() async {
|
||||||
if ((await sharedPreferences.getString(authenticationKey)).isEmpty) {
|
if ((await sharedPreferences.getString(authenticationKey)).isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
@@ -35,11 +48,13 @@ class LocalVault {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<ClientCertificate?> loadCertificate() async {
|
Future<ClientCertificate?> loadCertificate() async {
|
||||||
return loadAuthenticationInformation()
|
return loadAuthenticationInformation()
|
||||||
.then((value) => value?.clientCertificate);
|
.then((value) => value?.clientCertificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
|
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
|
||||||
return sharedPreferences.setString(
|
return sharedPreferences.setString(
|
||||||
applicationSettingsKey,
|
applicationSettingsKey,
|
||||||
@@ -47,6 +62,7 @@ class LocalVault {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<ApplicationSettingsState?> loadApplicationSettings() async {
|
Future<ApplicationSettingsState?> loadApplicationSettings() async {
|
||||||
final settings = await sharedPreferences.getString(applicationSettingsKey);
|
final settings = await sharedPreferences.getString(applicationSettingsKey);
|
||||||
if (settings.isEmpty) {
|
if (settings.isEmpty) {
|
||||||
@@ -58,6 +74,7 @@ class LocalVault {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> clear() {
|
Future<void> clear() {
|
||||||
return sharedPreferences.clear();
|
return sharedPreferences.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import 'package:get_it/get_it.dart';
|
|||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
final getIt = GetIt.instance..allowReassignment;
|
final getIt = GetIt.instance..allowReassignment;
|
||||||
|
|
||||||
@InjectableInit(
|
@InjectableInit(
|
||||||
initializerName: r'$initGetIt', // default
|
initializerName: r'$initGetIt', // default
|
||||||
preferRelativeImports: true, // default
|
preferRelativeImports: true, // default
|
||||||
asExtension: false, // 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].
|
/// Registers new security context, which will be used by the HttpClient, see [RegisterModule].
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.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:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart';
|
||||||
@@ -16,18 +17,33 @@ import 'package:local_auth/local_auth.dart';
|
|||||||
@module
|
@module
|
||||||
abstract class RegisterModule {
|
abstract class RegisterModule {
|
||||||
@singleton
|
@singleton
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
LocalAuthentication get localAuthentication => LocalAuthentication();
|
LocalAuthentication get localAuthentication => LocalAuthentication();
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
EncryptedSharedPreferences get encryptedSharedPreferences =>
|
EncryptedSharedPreferences get encryptedSharedPreferences =>
|
||||||
EncryptedSharedPreferences();
|
EncryptedSharedPreferences();
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
|
@test
|
||||||
SecurityContext get securityContext => SecurityContext();
|
SecurityContext get securityContext => SecurityContext();
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
Connectivity get connectivity => Connectivity();
|
Connectivity get connectivity => Connectivity();
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Factory method creating an [HttpClient] with the currently registered [SecurityContext].
|
/// Factory method creating an [HttpClient] with the currently registered [SecurityContext].
|
||||||
///
|
///
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
HttpClient getHttpClient(SecurityContext securityContext) =>
|
HttpClient getHttpClient(SecurityContext securityContext) =>
|
||||||
HttpClient(context: securityContext)
|
HttpClient(context: securityContext)
|
||||||
..connectionTimeout = const Duration(seconds: 10);
|
..connectionTimeout = const Duration(seconds: 10);
|
||||||
@@ -35,6 +51,9 @@ abstract class RegisterModule {
|
|||||||
///
|
///
|
||||||
/// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient].
|
/// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient].
|
||||||
///
|
///
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
BaseClient getBaseClient(
|
BaseClient getBaseClient(
|
||||||
AuthenticationInterceptor authInterceptor,
|
AuthenticationInterceptor authInterceptor,
|
||||||
ResponseConversionInterceptor responseConversionInterceptor,
|
ResponseConversionInterceptor responseConversionInterceptor,
|
||||||
@@ -50,28 +69,46 @@ abstract class RegisterModule {
|
|||||||
client: IOClient(client),
|
client: IOClient(client),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
CacheManager getCacheManager(BaseClient client) => CacheManager(
|
CacheManager getCacheManager(BaseClient client) => CacheManager(
|
||||||
Config('cacheKey', fileService: HttpFileService(httpClient: client)));
|
Config('cacheKey', fileService: HttpFileService(httpClient: client)));
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
PaperlessAuthenticationApi authenticationModule(BaseClient client) =>
|
PaperlessAuthenticationApi authenticationModule(BaseClient client) =>
|
||||||
PaperlessAuthenticationApiImpl(client);
|
PaperlessAuthenticationApiImpl(client);
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
PaperlessLabelsApi labelsModule(
|
PaperlessLabelsApi labelsModule(
|
||||||
@Named('timeoutClient') BaseClient timeoutClient,
|
@Named('timeoutClient') BaseClient timeoutClient,
|
||||||
) =>
|
) =>
|
||||||
PaperlessLabelApiImpl(timeoutClient);
|
PaperlessLabelApiImpl(timeoutClient);
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
PaperlessDocumentsApi documentsModule(
|
PaperlessDocumentsApi documentsModule(
|
||||||
@Named('timeoutClient') BaseClient timeoutClient,
|
@Named('timeoutClient') BaseClient timeoutClient,
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
) =>
|
) =>
|
||||||
PaperlessDocumentsApiImpl(timeoutClient, httpClient);
|
PaperlessDocumentsApiImpl(timeoutClient, httpClient);
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
PaperlessSavedViewsApi savedViewsModule(
|
PaperlessSavedViewsApi savedViewsModule(
|
||||||
@Named('timeoutClient') BaseClient timeoutClient,
|
@Named('timeoutClient') BaseClient timeoutClient,
|
||||||
) =>
|
) =>
|
||||||
PaperlessSavedViewsApiImpl(timeoutClient);
|
PaperlessSavedViewsApiImpl(timeoutClient);
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
@dev
|
||||||
|
@prod
|
||||||
PaperlessServerStatsApi serverStatsModule(
|
PaperlessServerStatsApi serverStatsModule(
|
||||||
@Named('timeoutClient') BaseClient timeoutClient,
|
@Named('timeoutClient') BaseClient timeoutClient,
|
||||||
) =>
|
) =>
|
||||||
|
|||||||
69
lib/di_test_mocks.dart
Normal file
69
lib/di_test_mocks.dart
Normal 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();
|
||||||
|
}
|
||||||
@@ -72,6 +72,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
Widget _buildLoginButton() {
|
Widget _buildLoginButton() {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
|
key: const ValueKey('login-login-button'),
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: MaterialStatePropertyAll(
|
backgroundColor: MaterialStatePropertyAll(
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class _ClientCertificateFormFieldState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormBuilderField<ClientCertificate?>(
|
return FormBuilderField<ClientCertificate?>(
|
||||||
|
key: const ValueKey('login-client-cert'),
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
@@ -70,6 +71,7 @@ class _ClientCertificateFormFieldState
|
|||||||
),
|
),
|
||||||
if (_selectedFile != null) ...[
|
if (_selectedFile != null) ...[
|
||||||
ObscuredInputTextFormField(
|
ObscuredInputTextFormField(
|
||||||
|
key: const ValueKey('login-client-cert-passphrase'),
|
||||||
initialValue: field.value?.passphrase,
|
initialValue: field.value?.passphrase,
|
||||||
onChanged: (value) => field.didChange(
|
onChanged: (value) => field.didChange(
|
||||||
field.value?.copyWith(passphrase: value),
|
field.value?.copyWith(passphrase: value),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormBuilderTextField(
|
return FormBuilderTextField(
|
||||||
|
key: const ValueKey('login-server-address'),
|
||||||
name: ServerAddressFormField.fkServerAddress,
|
name: ServerAddressFormField.fkServerAddress,
|
||||||
validator: FormBuilderValidators.required(
|
validator: FormBuilderValidators.required(
|
||||||
errorText: S.of(context).loginPageServerUrlValidatorMessageText,
|
errorText: S.of(context).loginPageServerUrlValidatorMessageText,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
key: const ValueKey('login-username'),
|
||||||
textCapitalization: TextCapitalization.words,
|
textCapitalization: TextCapitalization.words,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
// USERNAME
|
// USERNAME
|
||||||
@@ -41,6 +42,7 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
ObscuredInputTextFormField(
|
ObscuredInputTextFormField(
|
||||||
|
key: const ValueKey('login-password'),
|
||||||
label: S.of(context).loginPagePasswordFieldLabel,
|
label: S.of(context).loginPagePasswordFieldLabel,
|
||||||
onChanged: (password) => field.didChange(
|
onChanged: (password) => field.didChange(
|
||||||
field.value?.copyWith(password: password) ??
|
field.value?.copyWith(password: password) ??
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
|||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
|
||||||
void main() async {
|
Future<void> startAppProd() async {
|
||||||
Bloc.observer = BlocChangesObserver();
|
Bloc.observer = BlocChangesObserver();
|
||||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||||
@@ -42,25 +42,22 @@ void main() async {
|
|||||||
// Required for self signed client certificates
|
// Required for self signed client certificates
|
||||||
HttpOverrides.global = X509HttpOverrides();
|
HttpOverrides.global = X509HttpOverrides();
|
||||||
|
|
||||||
configureDependencies();
|
configureDependencies('prod');
|
||||||
// Remove temporarily downloaded files.
|
// Remove temporarily downloaded files.
|
||||||
(await FileService.temporaryDirectory).deleteSync(recursive: true);
|
(await FileService.temporaryDirectory).deleteSync(recursive: true);
|
||||||
kPackageInfo = await PackageInfo.fromPlatform();
|
kPackageInfo = await PackageInfo.fromPlatform();
|
||||||
// Load application settings and stored authentication data
|
// Load application settings and stored authentication data
|
||||||
getIt<ConnectivityCubit>().initialize();
|
await getIt<ConnectivityCubit>().initialize();
|
||||||
await getIt<ApplicationSettingsCubit>().initialize();
|
await getIt<ApplicationSettingsCubit>().initialize();
|
||||||
await getIt<AuthenticationCubit>().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());
|
runApp(const PaperlessMobileEntrypoint());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
await startAppProd();
|
||||||
|
}
|
||||||
|
|
||||||
class PaperlessMobileEntrypoint extends StatefulWidget {
|
class PaperlessMobileEntrypoint extends StatefulWidget {
|
||||||
const PaperlessMobileEntrypoint({Key? key}) : super(key: key);
|
const PaperlessMobileEntrypoint({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -114,11 +111,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
themeMode: settings.preferredThemeMode,
|
themeMode: settings.preferredThemeMode,
|
||||||
supportedLocales: const [
|
supportedLocales: S.delegate.supportedLocales,
|
||||||
Locale('en'), // Default if system locale is not available
|
|
||||||
Locale('de'),
|
|
||||||
Locale('cs'),
|
|
||||||
],
|
|
||||||
locale: Locale.fromSubtags(
|
locale: Locale.fromSubtags(
|
||||||
languageCode: settings.preferredLocaleSubtag),
|
languageCode: settings.preferredLocaleSubtag),
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
|
|||||||
@@ -610,7 +610,7 @@ packages:
|
|||||||
name: form_builder_validators
|
name: form_builder_validators
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.3.0"
|
version: "8.4.0"
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git
|
url: https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git
|
||||||
ref: main
|
ref: main
|
||||||
form_builder_validators: ^8.3.0
|
form_builder_validators: ^8.4.0
|
||||||
infinite_scroll_pagination: ^3.2.0
|
infinite_scroll_pagination: ^3.2.0
|
||||||
sliding_up_panel: ^2.0.0+1
|
sliding_up_panel: ^2.0.0+1
|
||||||
package_info_plus: ^1.4.3+1
|
package_info_plus: ^1.4.3+1
|
||||||
|
|||||||
Reference in New Issue
Block a user