test: expand client-side coverage and fix lifecycle issues
This commit is contained in:
409
test/test_support.dart
Normal file
409
test/test_support.dart
Normal file
@@ -0,0 +1,409 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ignis_app/providers/providers.dart';
|
||||
import 'package:ignis_app/services/api_client.dart';
|
||||
import 'package:ignis_app/services/credentials_storage.dart';
|
||||
import 'package:ignis_app/services/settings_service.dart';
|
||||
|
||||
class InMemoryCredentialsStorage implements CredentialsStorage {
|
||||
final Map<String, String> _apiKeys = {};
|
||||
|
||||
@override
|
||||
Future<String?> getApiKey(String homeId) async => _apiKeys[homeId];
|
||||
|
||||
@override
|
||||
Future<void> setApiKey(String homeId, String apiKey) async {
|
||||
_apiKeys[homeId] = apiKey;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteApiKey(String homeId) async {
|
||||
_apiKeys.remove(homeId);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeIgnisApi extends IgnisApi {
|
||||
Object? groupsData;
|
||||
Object? groupStatusData;
|
||||
Object? devicesData;
|
||||
Object? scenesData;
|
||||
Object? tasksData;
|
||||
Object? statsData;
|
||||
Object? eventLogData;
|
||||
Object? apiKeysData;
|
||||
Object? authData;
|
||||
|
||||
Object? devicesError;
|
||||
Object? scenesError;
|
||||
Object? tasksError;
|
||||
Object? statsError;
|
||||
Object? eventLogError;
|
||||
Object? apiKeysError;
|
||||
Object? authError;
|
||||
Object? groupsError;
|
||||
Object? groupStatusError;
|
||||
Object? controlGroupError;
|
||||
Object? cancelTaskError;
|
||||
Object? revokeApiKeyError;
|
||||
Object? activateApiKeyError;
|
||||
Object? createApiKeyError;
|
||||
Object? createGroupError;
|
||||
Object? deleteGroupError;
|
||||
Object? scheduleOnceError;
|
||||
Object? scheduleCronError;
|
||||
Object? rescanNetworkError;
|
||||
|
||||
String? controlledGroupId;
|
||||
Map<String, dynamic>? controlGroupParams;
|
||||
int? requestedDays;
|
||||
int? requestedLimit;
|
||||
String? cancelledJobId;
|
||||
String? revokedApiKey;
|
||||
String? activatedApiKey;
|
||||
String? createdApiKeyName;
|
||||
bool? createdApiKeyIsAdmin;
|
||||
String? createdGroupId;
|
||||
String? createdGroupName;
|
||||
List<String>? createdGroupMacs;
|
||||
String? deletedGroupId;
|
||||
Map<String, dynamic>? scheduledOnceParams;
|
||||
Map<String, dynamic>? scheduledCronParams;
|
||||
int rescanCalls = 0;
|
||||
|
||||
FakeIgnisApi({
|
||||
this.groupsData,
|
||||
this.groupStatusData,
|
||||
this.devicesData,
|
||||
this.scenesData,
|
||||
this.tasksData,
|
||||
this.statsData,
|
||||
this.eventLogData,
|
||||
this.apiKeysData,
|
||||
this.authData,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Response> getAuthMe() async {
|
||||
final error = authError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/auth/me'),
|
||||
data: authData ?? <String, dynamic>{'is_admin': false},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> validateCredentials(String baseUrl, String apiKey) async {
|
||||
final error = authError;
|
||||
if (error != null) throw error;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getDevices() async {
|
||||
final error = devicesError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/devices'),
|
||||
data: devicesData ?? <String, dynamic>{'devices': <Object>[]},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getScenes() async {
|
||||
final error = scenesError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/devices/scenes'),
|
||||
data: scenesData ?? <Object>[],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getGroups() async {
|
||||
final error = groupsError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/devices/groups'),
|
||||
data: groupsData ?? <Object>[],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getGroupStatus(String id) async {
|
||||
final error = groupStatusError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/control/group/$id/status'),
|
||||
data:
|
||||
groupStatusData ??
|
||||
{
|
||||
'results': [
|
||||
{
|
||||
'status': {'state': false, 'dimming': 100, 'temp': 4000},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> controlGroup(String id, Map<String, dynamic> params) async {
|
||||
controlledGroupId = id;
|
||||
controlGroupParams = params;
|
||||
final error = controlGroupError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/control/group/$id'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getTasks() async {
|
||||
final error = tasksError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/schedules/tasks'),
|
||||
data: tasksData ?? <String, dynamic>{'tasks': <Object>[]},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> cancelTask(String jobId) async {
|
||||
cancelledJobId = jobId;
|
||||
final error = cancelTaskError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/schedules/$jobId'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> scheduleOnce(Map<String, dynamic> params) async {
|
||||
scheduledOnceParams = Map<String, dynamic>.from(params);
|
||||
final error = scheduleOnceError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/schedules/once'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> scheduleCron(Map<String, dynamic> params) async {
|
||||
scheduledCronParams = Map<String, dynamic>.from(params);
|
||||
final error = scheduleCronError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/schedules/cron'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getStatsSummary({int days = 7}) async {
|
||||
requestedDays = days;
|
||||
final error = statsError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/stats/summary'),
|
||||
data: statsData,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getStatsLog({int limit = 100}) async {
|
||||
requestedLimit = limit;
|
||||
final error = eventLogError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/stats/log'),
|
||||
data: eventLogData,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getApiKeys() async {
|
||||
final error = apiKeysError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/api-keys'),
|
||||
data: apiKeysData ?? <String, dynamic>{'keys': <Object>[]},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> createApiKey(String name, {bool isAdmin = false}) async {
|
||||
createdApiKeyName = name;
|
||||
createdApiKeyIsAdmin = isAdmin;
|
||||
final error = createApiKeyError;
|
||||
if (error != null) throw error;
|
||||
|
||||
final newKey = '${name}_token';
|
||||
apiKeysData = {
|
||||
'keys': [
|
||||
...(apiKeysData is Map
|
||||
? List<Map<String, dynamic>>.from(
|
||||
(apiKeysData as Map)['keys'] as List? ?? const [],
|
||||
)
|
||||
: const <Map<String, dynamic>>[]),
|
||||
{'name': name, 'key': newKey, 'is_admin': isAdmin, 'is_active': true},
|
||||
],
|
||||
};
|
||||
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/api-keys'),
|
||||
data: <String, dynamic>{'key': newKey},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> revokeApiKey(String key) async {
|
||||
revokedApiKey = key;
|
||||
final error = revokeApiKeyError;
|
||||
if (error != null) throw error;
|
||||
apiKeysData = _mapApiKeys(
|
||||
apiKeysData,
|
||||
(item) => item['key'] == key ? {...item, 'is_active': false} : item,
|
||||
);
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/api-keys/revoke'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> activateApiKey(String key) async {
|
||||
activatedApiKey = key;
|
||||
final error = activateApiKeyError;
|
||||
if (error != null) throw error;
|
||||
apiKeysData = _mapApiKeys(
|
||||
apiKeysData,
|
||||
(item) => item['key'] == key ? {...item, 'is_active': true} : item,
|
||||
);
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/api-keys/activate'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> createGroup(
|
||||
String id,
|
||||
String name,
|
||||
List<String> macs,
|
||||
) async {
|
||||
createdGroupId = id;
|
||||
createdGroupName = name;
|
||||
createdGroupMacs = List<String>.from(macs);
|
||||
final error = createGroupError;
|
||||
if (error != null) throw error;
|
||||
groupsData = _addGroup(groupsData, id, name, macs);
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/devices/groups'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> deleteGroup(String groupId) async {
|
||||
deletedGroupId = groupId;
|
||||
final error = deleteGroupError;
|
||||
if (error != null) throw error;
|
||||
groupsData = _removeGroup(groupsData, groupId);
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/devices/groups/$groupId'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> rescanNetwork() async {
|
||||
rescanCalls += 1;
|
||||
final error = rescanNetworkError;
|
||||
if (error != null) throw error;
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: '/devices/rescan'),
|
||||
data: <String, dynamic>{'ok': true},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProviderContainer createTestContainer(
|
||||
FakeIgnisApi api, {
|
||||
SettingsService? settingsService,
|
||||
}) {
|
||||
final overrides = [apiProvider.overrideWithValue(api)];
|
||||
if (settingsService != null) {
|
||||
overrides.add(settingsServiceProvider.overrideWithValue(settingsService));
|
||||
}
|
||||
|
||||
final container = ProviderContainer(overrides: overrides);
|
||||
addTearDown(container.dispose);
|
||||
return container;
|
||||
}
|
||||
|
||||
Future<ProviderContainer> pumpTestApp(
|
||||
WidgetTester tester, {
|
||||
required Widget child,
|
||||
FakeIgnisApi? api,
|
||||
SettingsService? settingsService,
|
||||
}) async {
|
||||
final container = createTestContainer(
|
||||
api ?? FakeIgnisApi(),
|
||||
settingsService: settingsService,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: MaterialApp(home: child),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
return container;
|
||||
}
|
||||
|
||||
Object _mapApiKeys(
|
||||
Object? source,
|
||||
Map<String, dynamic> Function(Map<String, dynamic>) transform,
|
||||
) {
|
||||
final current = source is Map
|
||||
? List<Map<String, dynamic>>.from(source['keys'] as List? ?? const [])
|
||||
: <Map<String, dynamic>>[];
|
||||
return {
|
||||
'keys': current
|
||||
.map((item) => transform(Map<String, dynamic>.from(item)))
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
Object _addGroup(Object? source, String id, String name, List<String> macs) {
|
||||
if (source is Map) {
|
||||
final next = Map<String, dynamic>.from(source);
|
||||
next[id] = {'name': name, 'macs': macs};
|
||||
return next;
|
||||
}
|
||||
|
||||
final list = source is List ? List<Object>.from(source) : <Object>[];
|
||||
list.add({'id': id, 'name': name, 'macs': macs});
|
||||
return list;
|
||||
}
|
||||
|
||||
Object _removeGroup(Object? source, String id) {
|
||||
if (source is Map) {
|
||||
final next = Map<String, dynamic>.from(source);
|
||||
next.remove(id);
|
||||
return next;
|
||||
}
|
||||
|
||||
if (source is List) {
|
||||
return List<Object>.from(source)
|
||||
..removeWhere((item) => item is Map && item['id']?.toString() == id);
|
||||
}
|
||||
|
||||
return <Object>[];
|
||||
}
|
||||
Reference in New Issue
Block a user