Add unit tests for AppConfig and AuthController, and include mount checks in AuthController.
This commit is contained in:
parent
73597b9bca
commit
bbd7a371dd
|
|
@ -21,11 +21,13 @@ class AuthController extends Notifier<AuthState> {
|
||||||
final config = ref.read(appConfigProvider);
|
final config = ref.read(appConfigProvider);
|
||||||
final configError = config.validationError;
|
final configError = config.validationError;
|
||||||
if (configError != null) {
|
if (configError != null) {
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = state.copyWith(isLoading: false, errorMessage: configError);
|
state = state.copyWith(isLoading: false, errorMessage: configError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (config.useLocalAuth) {
|
if (config.useLocalAuth) {
|
||||||
final localSession = await _localDataSource.readLocalSession();
|
final localSession = await _localDataSource.readLocalSession();
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = AuthState(
|
state = AuthState(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
token: localSession != null ? const AuthToken(accessToken: 'local') : null,
|
token: localSession != null ? const AuthToken(accessToken: 'local') : null,
|
||||||
|
|
@ -33,6 +35,7 @@ class AuthController extends Notifier<AuthState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final token = await _repository.getSavedToken();
|
final token = await _repository.getSavedToken();
|
||||||
|
if (!ref.mounted) return;
|
||||||
state = AuthState(isLoading: false, token: token);
|
state = AuthState(isLoading: false, token: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
96
front001/mosenioring/test/core/config/app_config_test.dart
Normal file
96
front001/mosenioring/test/core/config/app_config_test.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mosenioring/src/core/config/app_config.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AppConfig', () {
|
||||||
|
test('validationError returns null when API_BASE_URL and useLocalAuth are set', () {
|
||||||
|
const config = AppConfig(
|
||||||
|
apiBaseUrl: 'http://localhost:8080',
|
||||||
|
useLocalAuth: true,
|
||||||
|
localTenantId: '123',
|
||||||
|
localRoles: 'USER',
|
||||||
|
keycloakIssuer: '',
|
||||||
|
keycloakClientId: '',
|
||||||
|
keycloakRedirectUrl: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.validationError, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validationError returns error when API_BASE_URL is missing', () {
|
||||||
|
const config = AppConfig(
|
||||||
|
apiBaseUrl: ' ',
|
||||||
|
useLocalAuth: true,
|
||||||
|
localTenantId: '123',
|
||||||
|
localRoles: 'USER',
|
||||||
|
keycloakIssuer: '',
|
||||||
|
keycloakClientId: '',
|
||||||
|
keycloakRedirectUrl: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.validationError, contains('Missing API_BASE_URL'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validationError returns error when Keycloak config is missing and not using local auth', () {
|
||||||
|
// Assuming we are on a platform that supports AppAuth in tests (usually not, so it might hit the platform check first)
|
||||||
|
// Actually, in terminal tests, defaultTargetPlatform might be android or ios depending on environment, but often it's not.
|
||||||
|
// Let's test the logic regardless of platform by checking the specific requirements.
|
||||||
|
|
||||||
|
const config = AppConfig(
|
||||||
|
apiBaseUrl: 'http://localhost:8080',
|
||||||
|
useLocalAuth: false,
|
||||||
|
localTenantId: '123',
|
||||||
|
localRoles: 'USER',
|
||||||
|
keycloakIssuer: '',
|
||||||
|
keycloakClientId: '',
|
||||||
|
keycloakRedirectUrl: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
final error = config.validationError;
|
||||||
|
expect(error, isNotNull);
|
||||||
|
// It will either fail on platform support or on missing issuer.
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keycloakDiscoveryUrl is correctly formed', () {
|
||||||
|
const config = AppConfig(
|
||||||
|
apiBaseUrl: 'http://localhost:8080',
|
||||||
|
useLocalAuth: false,
|
||||||
|
localTenantId: '123',
|
||||||
|
localRoles: 'USER',
|
||||||
|
keycloakIssuer: 'https://auth.example.com/realms/myrealm',
|
||||||
|
keycloakClientId: 'client',
|
||||||
|
keycloakRedirectUrl: 'app://callback',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.keycloakDiscoveryUrl, 'https://auth.example.com/realms/myrealm/.well-known/openid-configuration');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allowInsecureConnections is true for http issuer', () {
|
||||||
|
const config = AppConfig(
|
||||||
|
apiBaseUrl: 'http://localhost:8080',
|
||||||
|
useLocalAuth: false,
|
||||||
|
localTenantId: '123',
|
||||||
|
localRoles: 'USER',
|
||||||
|
keycloakIssuer: 'http://localhost:8081',
|
||||||
|
keycloakClientId: 'client',
|
||||||
|
keycloakRedirectUrl: 'app://callback',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.allowInsecureConnections, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allowInsecureConnections is false for https issuer', () {
|
||||||
|
const config = AppConfig(
|
||||||
|
apiBaseUrl: 'http://localhost:8080',
|
||||||
|
useLocalAuth: false,
|
||||||
|
localTenantId: '123',
|
||||||
|
localRoles: 'USER',
|
||||||
|
keycloakIssuer: 'https://auth.example.com',
|
||||||
|
keycloakClientId: 'client',
|
||||||
|
keycloakRedirectUrl: 'app://callback',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.allowInsecureConnections, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mosenioring/src/di/providers.dart';
|
||||||
|
import 'package:mosenioring/src/features/auth/domain/auth_repository.dart';
|
||||||
|
import 'package:mosenioring/src/features/auth/domain/models/auth_token.dart';
|
||||||
|
import 'package:mosenioring/src/features/auth/data/auth_local_data_source.dart';
|
||||||
|
import 'package:mosenioring/src/core/config/app_config.dart';
|
||||||
|
|
||||||
|
class MockAuthRepository implements AuthRepository {
|
||||||
|
AuthToken? token;
|
||||||
|
Object? loginError;
|
||||||
|
bool logoutCalled = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AuthToken?> getSavedToken() async => token;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AuthToken> login({required String email, required String password}) async {
|
||||||
|
if (loginError != null) throw loginError!;
|
||||||
|
return const AuthToken(accessToken: 'new_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> logout() async {
|
||||||
|
logoutCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> persistToken(AuthToken token) async {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockAuthLocalDataSource implements AuthLocalDataSource {
|
||||||
|
LocalAuthSession? session;
|
||||||
|
AuthToken? token;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clear() async {
|
||||||
|
session = null;
|
||||||
|
token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LocalAuthSession?> readLocalSession() async => session;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AuthToken?> readToken() async => token;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveLocalSession({required String email, required String roles, required String tenantId}) async {
|
||||||
|
session = LocalAuthSession(email: email, roles: roles, tenantId: tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveToken(AuthToken token) async {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AuthController', () {
|
||||||
|
late MockAuthRepository mockRepository;
|
||||||
|
late AppConfig testConfig;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockRepository = MockAuthRepository();
|
||||||
|
testConfig = const AppConfig(
|
||||||
|
apiBaseUrl: 'https://api.example.com',
|
||||||
|
useLocalAuth: false,
|
||||||
|
localTenantId: '123',
|
||||||
|
localRoles: 'USER',
|
||||||
|
keycloakIssuer: 'https://keycloak.com',
|
||||||
|
keycloakClientId: 'client',
|
||||||
|
keycloakRedirectUrl: 'app://callback',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ProviderContainer createContainer({AppConfig? config}) {
|
||||||
|
final container = ProviderContainer(
|
||||||
|
overrides: [
|
||||||
|
authRepositoryProvider.overrideWithValue(mockRepository),
|
||||||
|
appConfigProvider.overrideWithValue(config ?? testConfig),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('initial state is loading then unauthenticated when no token', () async {
|
||||||
|
final container = createContainer();
|
||||||
|
|
||||||
|
// Wait for bootstrap microtask
|
||||||
|
await Future.microtask(() {});
|
||||||
|
|
||||||
|
final state = container.read(authControllerProvider);
|
||||||
|
expect(state.isLoading, isFalse);
|
||||||
|
expect(state.isAuthenticated, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bootstrap loads saved token', () async {
|
||||||
|
mockRepository.token = const AuthToken(accessToken: 'saved_token');
|
||||||
|
final container = createContainer();
|
||||||
|
|
||||||
|
// Keep checking until state is updated or timeout
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
await Future.delayed(Duration.zero);
|
||||||
|
if (container.read(authControllerProvider).isAuthenticated) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final state = container.read(authControllerProvider);
|
||||||
|
expect(state.isAuthenticated, isTrue);
|
||||||
|
expect(state.token?.accessToken, 'saved_token');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login success updates state', () async {
|
||||||
|
final container = createContainer();
|
||||||
|
await Future.microtask(() {});
|
||||||
|
|
||||||
|
await container.read(authControllerProvider.notifier).login(
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password',
|
||||||
|
);
|
||||||
|
|
||||||
|
final state = container.read(authControllerProvider);
|
||||||
|
expect(state.isAuthenticated, isTrue);
|
||||||
|
expect(state.token?.accessToken, 'new_token');
|
||||||
|
expect(state.errorMessage, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login failure updates error message', () async {
|
||||||
|
final container = createContainer();
|
||||||
|
await Future.microtask(() {});
|
||||||
|
mockRepository.loginError = Exception('Invalid credentials');
|
||||||
|
|
||||||
|
await container.read(authControllerProvider.notifier).login(
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password',
|
||||||
|
);
|
||||||
|
|
||||||
|
final state = container.read(authControllerProvider);
|
||||||
|
expect(state.isAuthenticated, isFalse);
|
||||||
|
expect(state.errorMessage, 'Invalid credentials');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logout clears state', () async {
|
||||||
|
mockRepository.token = const AuthToken(accessToken: 'saved_token');
|
||||||
|
final container = createContainer();
|
||||||
|
await Future.microtask(() {});
|
||||||
|
|
||||||
|
await container.read(authControllerProvider.notifier).logout();
|
||||||
|
|
||||||
|
final state = container.read(authControllerProvider);
|
||||||
|
expect(state.isAuthenticated, isFalse);
|
||||||
|
expect(mockRepository.logoutCalled, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue