import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; import '../repository.mocks.dart'; import '../service.mocks.dart'; import '../test_utils.dart'; void main() { late AuthService sut; late MockAuthApiRepository authApiRepository; late MockAuthRepository authRepository; late MockApiService apiService; late MockNetworkService networkService; setUp(() async { authApiRepository = MockAuthApiRepository(); authRepository = MockAuthRepository(); apiService = MockApiService(); networkService = MockNetworkService(); sut = AuthService( authApiRepository, authRepository, apiService, networkService, ); registerFallbackValue(Uri()); }); group('validateServerUrl', () { setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); final db = await TestUtils.initIsar(); db.writeTxnSync(() => db.clearSync()); Store.init(db); }); test('Should resolve HTTP endpoint', () async { const testUrl = 'http://ip:2283'; const resolvedUrl = 'http://ip:2283/api'; when(() => apiService.resolveAndSetEndpoint(testUrl)) .thenAnswer((_) async => resolvedUrl); when(() => apiService.setDeviceInfoHeader()).thenAnswer((_) async => {}); final result = await sut.validateServerUrl(testUrl); expect(result, resolvedUrl); verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1); verify(() => apiService.setDeviceInfoHeader()).called(1); }); test('Should resolve HTTPS endpoint', () async { const testUrl = 'https://immich.domain.com'; const resolvedUrl = 'https://immich.domain.com/api'; when(() => apiService.resolveAndSetEndpoint(testUrl)) .thenAnswer((_) async => resolvedUrl); when(() => apiService.setDeviceInfoHeader()).thenAnswer((_) async => {}); final result = await sut.validateServerUrl(testUrl); expect(result, resolvedUrl); verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1); verify(() => apiService.setDeviceInfoHeader()).called(1); }); test('Should throw error on invalid URL', () async { const testUrl = 'invalid-url'; when(() => apiService.resolveAndSetEndpoint(testUrl)) .thenThrow(Exception('Invalid URL')); expect( () async => await sut.validateServerUrl(testUrl), throwsA(isA()), ); verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1); verifyNever(() => apiService.setDeviceInfoHeader()); }); test('Should throw error on unreachable server', () async { const testUrl = 'https://unreachable.server'; when(() => apiService.resolveAndSetEndpoint(testUrl)) .thenThrow(Exception('Server is not reachable')); expect( () async => await sut.validateServerUrl(testUrl), throwsA(isA()), ); verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1); verifyNever(() => apiService.setDeviceInfoHeader()); }); }); group('logout', () { test('Should logout user', () async { when(() => authApiRepository.logout()).thenAnswer((_) async => {}); when(() => authRepository.clearLocalData()) .thenAnswer((_) => Future.value(null)); await sut.logout(); verify(() => authApiRepository.logout()).called(1); verify(() => authRepository.clearLocalData()).called(1); }); test('Should clear local data even on server error', () async { when(() => authApiRepository.logout()) .thenThrow(Exception('Server error')); when(() => authRepository.clearLocalData()) .thenAnswer((_) => Future.value(null)); await sut.logout(); verify(() => authApiRepository.logout()).called(1); verify(() => authRepository.clearLocalData()).called(1); }); }); group('setOpenApiServiceEndpoint', () { setUp(() { when(() => networkService.getWifiName()) .thenAnswer((_) async => 'TestWifi'); }); test('Should return null if auto endpoint switching is disabled', () async { when(() => authRepository.getEndpointSwitchingFeature()) .thenReturn((false)); final result = await sut.setOpenApiServiceEndpoint(); expect(result, isNull); verify(() => authRepository.getEndpointSwitchingFeature()).called(1); verifyNever(() => networkService.getWifiName()); }); test('Should set local connection if wifi name matches', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi'); when(() => authRepository.getLocalEndpoint()) .thenReturn('http://local.endpoint'); when(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) .thenAnswer((_) async => 'http://local.endpoint'); final result = await sut.setOpenApiServiceEndpoint(); expect(result, 'http://local.endpoint'); verify(() => authRepository.getEndpointSwitchingFeature()).called(1); verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getLocalEndpoint()).called(1); verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) .called(1); }); test('Should set external endpoint if wifi name not matching', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()) .thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), ]); when( () => apiService.resolveAndSetEndpoint('https://external.endpoint'), ).thenAnswer((_) async => 'https://external.endpoint/api'); final result = await sut.setOpenApiServiceEndpoint(); expect(result, 'https://external.endpoint/api'); verify(() => authRepository.getEndpointSwitchingFeature()).called(1); verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getExternalEndpointList()).called(1); verify( () => apiService.resolveAndSetEndpoint('https://external.endpoint'), ).called(1); }); test('Should set second external endpoint if the first throw any error', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()) .thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), AuxilaryEndpoint( url: 'https://external.endpoint2', status: AuxCheckStatus.valid, ), ]); when( () => apiService.resolveAndSetEndpoint('https://external.endpoint'), ).thenThrow(Exception('Invalid endpoint')); when( () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), ).thenAnswer((_) async => 'https://external.endpoint2/api'); final result = await sut.setOpenApiServiceEndpoint(); expect(result, 'https://external.endpoint2/api'); verify(() => authRepository.getEndpointSwitchingFeature()).called(1); verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getExternalEndpointList()).called(1); verify( () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), ).called(1); }); test('Should set second external endpoint if the first throw ApiException', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()) .thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), AuxilaryEndpoint( url: 'https://external.endpoint2', status: AuxCheckStatus.valid, ), ]); when( () => apiService.resolveAndSetEndpoint('https://external.endpoint'), ).thenThrow(ApiException(503, 'Invalid endpoint')); when( () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), ).thenAnswer((_) async => 'https://external.endpoint2/api'); final result = await sut.setOpenApiServiceEndpoint(); expect(result, 'https://external.endpoint2/api'); verify(() => authRepository.getEndpointSwitchingFeature()).called(1); verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getExternalEndpointList()).called(1); verify( () => apiService.resolveAndSetEndpoint('https://external.endpoint2'), ).called(1); }); test('Should handle error when setting local connection', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi'); when(() => authRepository.getLocalEndpoint()) .thenReturn('http://local.endpoint'); when(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) .thenThrow(Exception('Local endpoint error')); final result = await sut.setOpenApiServiceEndpoint(); expect(result, isNull); verify(() => authRepository.getEndpointSwitchingFeature()).called(1); verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getLocalEndpoint()).called(1); verify(() => apiService.resolveAndSetEndpoint('http://local.endpoint')) .called(1); }); test('Should handle error when setting external connection', () async { when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true); when(() => authRepository.getPreferredWifiName()) .thenReturn('DifferentWifi'); when(() => authRepository.getExternalEndpointList()).thenReturn([ AuxilaryEndpoint( url: 'https://external.endpoint', status: AuxCheckStatus.valid, ), ]); when( () => apiService.resolveAndSetEndpoint('https://external.endpoint'), ).thenThrow(Exception('External endpoint error')); final result = await sut.setOpenApiServiceEndpoint(); expect(result, isNull); verify(() => authRepository.getEndpointSwitchingFeature()).called(1); verify(() => networkService.getWifiName()).called(1); verify(() => authRepository.getPreferredWifiName()).called(1); verify(() => authRepository.getExternalEndpointList()).called(1); verify( () => apiService.resolveAndSetEndpoint('https://external.endpoint'), ).called(1); }); }); }