diff --git a/README.md b/README.md
index d268d4d8..3a2a8d88 100644
--- a/README.md
+++ b/README.md
@@ -125,9 +125,8 @@ can [become a sponsor on GitHub](https://github.com/sponsors/tsutsu3), or
Run these commands as needed whenever you update the images.
-1. Run ``flutter clean``
-2. Run ``dart run flutter_launcher_icons`
-3. Run ``dart run flutter_native_splash:create``
+1. Run ``dart run flutter_launcher_icons`
+2. Run ``dart run flutter_native_splash:create``
#### Android
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 83414124..d5f10f6e 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -44,7 +44,7 @@ analyzer:
errors:
invalid_annotation_target: ignore
exclude:
- - test/**
+ - test/**/*.mocks.dart
- lib/**/*.g.dart
- lib/screens/settings/about/about.dart
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 399f6981..320da1ba 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 89a4fb06..e9b4281b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,6 @@
+
+
-
-
diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
deleted file mode 100644
index 98aaa79b..00000000
Binary files a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
deleted file mode 100644
index f7b60d23..00000000
Binary files a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
deleted file mode 100644
index 02632235..00000000
Binary files a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
deleted file mode 100644
index e5adb552..00000000
Binary files a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and /dev/null differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
deleted file mode 100644
index 22193844..00000000
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png and /dev/null differ
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
index 399f6981..320da1ba 100644
--- a/android/app/src/profile/AndroidManifest.xml
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 5f2d171d..ffccf00d 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -47,5 +47,7 @@
UIStatusBarHidden
+ NSFaceIDUsageDescription
+ Why is my app authenticating using face id?
diff --git a/lib/main.dart b/lib/main.dart
index d33b47fe..56d52a4a 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -80,7 +80,9 @@ void main() async {
configProvider.setBiometricsSupport(canAuthenticateWithBiometrics);
if (canAuthenticateWithBiometrics &&
- availableBiometrics.contains(BiometricType.fingerprint) == false &&
+ !availableBiometrics.contains(BiometricType.fingerprint) &&
+ !availableBiometrics.contains(BiometricType.strong) &&
+ !availableBiometrics.contains(BiometricType.weak) &&
dbRepository.appConfig.useBiometricAuth == 1) {
await configProvider.setUseBiometrics(false);
}
diff --git a/lib/repository/database.dart b/lib/repository/database.dart
index 9debe850..0e488538 100644
--- a/lib/repository/database.dart
+++ b/lib/repository/database.dart
@@ -49,8 +49,8 @@ class DatabaseRepository {
///
/// Throws:
/// - [Exception] if the initialization fails or the database cannot be accessed.
- Future initialize() async {
- final piHoleClientData = await loadDb();
+ Future initialize({String? path}) async {
+ final piHoleClientData = await loadDb(path: path);
_servers = piHoleClientData.servers;
_appConfig = piHoleClientData.appConfig;
_dbInstance = piHoleClientData.dbInstance;
@@ -69,6 +69,10 @@ class DatabaseRepository {
/// This method is responsible for initializing or opening the database,
/// ensuring that necessary tables exist, and fetching the required data.
///
+ /// Parameters:
+ /// - [path]: The path to the database file. If not provided, the default
+ /// path 'pi_hole_client.db' will be used.
+ ///
/// Returns:
/// - A `Future` that resolves to a [PiHoleClientData] object containing:
/// - `servers`: A list of server configurations from the database.
@@ -77,12 +81,12 @@ class DatabaseRepository {
///
/// Throws:
/// - [Exception] if the database cannot be initialized or data cannot be retrieved.
- Future loadDb() async {
+ Future loadDb({String? path}) async {
List? servers;
AppDbData? appConfig;
Database db = await openDatabase(
- 'pi_hole_client.db',
+ path ?? 'pi_hole_client.db',
version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
@@ -140,7 +144,7 @@ class DatabaseRepository {
// Load sensitive data from secure storage
final passCode = await _secureStorage.getValue('passCode');
- AppDbData.withSecrets(appConfig!, passCode);
+ appConfig = AppDbData.withSecrets(appConfig!, passCode);
// _secureStorage.readAll()
logger.d((await _secureStorage.readAll()).toString());
@@ -175,6 +179,21 @@ class DatabaseRepository {
);
}
+ /// Closes the database connection.
+ ///
+ /// This method closes the database connection and releases any resources
+ ///
+ /// Returns:
+ /// - A `Future` that resolves to `true` if the operation is successful.
+ Future closeDb() async {
+ try {
+ await _dbInstance.close();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
/// Saves a new server entry into the database.
///
/// This method adds a new server record to the 'servers' table in the database.
@@ -486,6 +505,11 @@ class DatabaseRepository {
}) async {
try {
if (column == 'passCode') {
+ if (value == null) {
+ await _secureStorage.deleteValue('passCode');
+ return true;
+ }
+
await _secureStorage.saveValue('passCode', value.toString());
return true;
}
diff --git a/lib/repository/secure_storage.dart b/lib/repository/secure_storage.dart
index 351b4134..1a368093 100644
--- a/lib/repository/secure_storage.dart
+++ b/lib/repository/secure_storage.dart
@@ -1,7 +1,10 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageRepository {
- final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
+ final FlutterSecureStorage _secureStorage;
+
+ SecureStorageRepository({FlutterSecureStorage? secureStorage})
+ : _secureStorage = secureStorage ?? const FlutterSecureStorage();
// Save a value securely
Future saveValue(String key, String value) async {
diff --git a/test/gateways/v5/api_gateway_v5_test.dart b/test/gateways/v5/api_gateway_v5_test.dart
index c7470217..7864948c 100644
--- a/test/gateways/v5/api_gateway_v5_test.dart
+++ b/test/gateways/v5/api_gateway_v5_test.dart
@@ -18,11 +18,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
apiGateway = ApiGatewayV5(server);
});
@@ -48,60 +49,71 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success with valid auth token', () async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV5(server, client: mockClient);
- when(mockClient.get(Uri.parse(url), headers: {}))
- .thenAnswer((_) async => http.Response(
- jsonEncode({
- 'domains_being_blocked': 121,
- 'dns_queries_today': 12,
- 'ads_blocked_today': 1,
- 'ads_percentage_today': 8.333333,
- 'unique_domains': 11,
- 'queries_forwarded': 9,
- 'queries_cached': 2,
- 'clients_ever_seen': 2,
- 'unique_clients': 2,
- 'dns_queries_all_types': 12,
- 'reply_UNKNOWN': 0,
- 'reply_NODATA': 0,
- 'reply_NXDOMAIN': 1,
- 'reply_CNAME': 0,
- 'reply_IP': 10,
- 'reply_DOMAIN': 1,
- 'reply_RRNAME': 0,
- 'reply_SERVFAIL': 0,
- 'reply_REFUSED': 0,
- 'reply_NOTIMP': 0,
- 'reply_OTHER': 0,
- 'reply_DNSSEC': 0,
- 'reply_NONE': 0,
- 'reply_BLOB': 0,
- 'dns_queries_all_replies': 12,
- 'privacy_level': 0,
- 'status': 'enabled',
- 'gravity_last_updated': {
- 'file_exists': true,
- 'absolute': 17329,
- 'relative': {'days': 4, 'hours': 23, 'minutes': 41}
- }
- }),
- 200));
-
- when(mockClient.get(
+ when(mockClient.get(Uri.parse(url), headers: {})).thenAnswer(
+ (_) async => http.Response(
+ jsonEncode({
+ 'domains_being_blocked': 121,
+ 'dns_queries_today': 12,
+ 'ads_blocked_today': 1,
+ 'ads_percentage_today': 8.333333,
+ 'unique_domains': 11,
+ 'queries_forwarded': 9,
+ 'queries_cached': 2,
+ 'clients_ever_seen': 2,
+ 'unique_clients': 2,
+ 'dns_queries_all_types': 12,
+ 'reply_UNKNOWN': 0,
+ 'reply_NODATA': 0,
+ 'reply_NXDOMAIN': 1,
+ 'reply_CNAME': 0,
+ 'reply_IP': 10,
+ 'reply_DOMAIN': 1,
+ 'reply_RRNAME': 0,
+ 'reply_SERVFAIL': 0,
+ 'reply_REFUSED': 0,
+ 'reply_NOTIMP': 0,
+ 'reply_OTHER': 0,
+ 'reply_DNSSEC': 0,
+ 'reply_NONE': 0,
+ 'reply_BLOB': 0,
+ 'dns_queries_all_replies': 12,
+ 'privacy_level': 0,
+ 'status': 'enabled',
+ 'gravity_last_updated': {
+ 'file_exists': true,
+ 'absolute': 17329,
+ 'relative': {'days': 4, 'hours': 23, 'minutes': 41},
+ },
+ }),
+ 200,
+ ),
+ );
+
+ when(
+ mockClient.get(
Uri.parse('http://example.com/admin/api.php?auth=xxx123&enable=0'),
- headers: {})).thenAnswer((_) async => http.Response(
- jsonEncode({'status': 'enabled'}), 200, headers: {
- 'set-cookie': 'sid=$sessinId; path=/; HttpOnly; SameSite=Strict'
- }));
+ headers: {},
+ ),
+ ).thenAnswer(
+ (_) async => http.Response(
+ jsonEncode({'status': 'enabled'}),
+ 200,
+ headers: {
+ 'set-cookie': 'sid=$sessinId; path=/; HttpOnly; SameSite=Strict',
+ },
+ ),
+ );
final response = await apiGateway.loginQuery();
@@ -223,11 +235,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success', () async {
@@ -264,7 +277,7 @@ void main() {
'gravity_last_updated': {
'file_exists': true,
'absolute': 1732972589,
- 'relative': {'days': 5, 'hours': 18, 'minutes': 14}
+ 'relative': {'days': 5, 'hours': 18, 'minutes': 14},
},
'top_queries': {
'1.0.26.172.in-addr.arpa': 3,
@@ -276,7 +289,7 @@ void main() {
'google.com': 1,
'google.co.jp': 1,
'yahoo.co.jp': 1,
- 'fix.test.com': 1
+ 'fix.test.com': 1,
},
'top_ads': {'test.com': 1},
'top_sources': {'172.26.0.1': 10, 'localhost|127.0.0.1': 6},
@@ -285,7 +298,7 @@ void main() {
'blocked|blocked': 6.25,
'cached|cached': 37.5,
'other|other': 0,
- 'dns.google#53|8.8.8.8#53': 56.25
+ 'dns.google#53|8.8.8.8#53': 56.25,
},
'querytypes': {
'A (IPv4)': 62.5,
@@ -303,8 +316,8 @@ void main() {
'NS': 0,
'OTHER': 0,
'SVCB': 0,
- 'HTTPS': 0
- }
+ 'HTTPS': 0,
+ },
};
when(mockClient.get(Uri.parse(url), headers: {}))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
@@ -335,11 +348,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success', () async {
@@ -375,11 +389,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success', () async {
@@ -416,11 +431,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success', () async {
@@ -571,7 +587,7 @@ void main() {
'1733475300': 0,
'1733475900': 0,
'1733476500': 0,
- '1733477100': 0
+ '1733477100': 0,
},
'ads_over_time': {
'1733391300': 0,
@@ -717,11 +733,11 @@ void main() {
'1733475300': 0,
'1733475900': 0,
'1733476500': 0,
- '1733477100': 0
+ '1733477100': 0,
},
'clients': [
{'name': '', 'ip': '172.26.0.1'},
- {'name': 'localhost', 'ip': '127.0.0.1'}
+ {'name': 'localhost', 'ip': '127.0.0.1'},
],
'over_time': {
'1733391300': [0, 0],
@@ -867,8 +883,8 @@ void main() {
'1733475300': [0, 0],
'1733475900': [0, 0],
'1733476500': [0, 0],
- '1733477100': [0, 0]
- }
+ '1733477100': [0, 0],
+ },
};
when(mockClient.get(Uri.parse(url), headers: {}))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
@@ -900,11 +916,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success', () async {
@@ -924,7 +941,7 @@ void main() {
'N/A',
'-1',
'dns.google#53',
- ''
+ '',
],
[
'1733479462',
@@ -938,9 +955,9 @@ void main() {
'N/A',
'-1',
'dns.google#53',
- ''
- ]
- ]
+ '',
+ ],
+ ],
};
when(mockClient.get(Uri.parse(url), headers: {}))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
@@ -976,11 +993,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success when add new domain', () async {
@@ -1003,7 +1021,7 @@ void main() {
final apiGateway = ApiGatewayV5(server, client: mockClient);
final data = {
'success': true,
- 'message': 'Not adding google.com as it is already on the list'
+ 'message': 'Not adding google.com as it is already on the list',
};
when(mockClient.get(Uri.parse(url), headers: {}))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
@@ -1054,16 +1072,17 @@ void main() {
'http://example.com/admin/api.php?auth=xxx123&list=white',
'http://example.com/admin/api.php?auth=xxx123&list=regex_white',
'http://example.com/admin/api.php?auth=xxx123&list=black',
- 'http://example.com/admin/api.php?auth=xxx123&list=regex_black'
+ 'http://example.com/admin/api.php?auth=xxx123&list=regex_black',
];
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success', () async {
@@ -1080,9 +1099,9 @@ void main() {
'date_added': 1733559182,
'date_modified': 1733559182,
'comment': '',
- 'groups': [0]
- }
- ]
+ 'groups': [0],
+ },
+ ],
},
{'data': []},
{
@@ -1095,9 +1114,9 @@ void main() {
'date_added': 1733401118,
'date_modified': 1733496612,
'comment': '',
- 'groups': [0]
- }
- ]
+ 'groups': [0],
+ },
+ ],
},
{'data': []},
];
@@ -1137,11 +1156,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success', () async {
@@ -1151,7 +1171,8 @@ void main() {
when(mockClient.get(Uri.parse(url), headers: {}))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
- final response = await apiGateway.removeDomainFromList(Domain(
+ final response = await apiGateway.removeDomainFromList(
+ Domain(
id: 1,
domain: 'google.com',
type: 0,
@@ -1159,7 +1180,9 @@ void main() {
dateAdded: DateTime.now(),
dateModified: DateTime.now(),
comment: '',
- groups: []));
+ groups: [],
+ ),
+ );
expect(response.result, APiResponseType.success);
expect(response.message, isNull);
@@ -1172,7 +1195,8 @@ void main() {
when(mockClient.get(Uri.parse(url), headers: {}))
.thenThrow(Exception('Unexpected error test'));
- final response = await apiGateway.removeDomainFromList(Domain(
+ final response = await apiGateway.removeDomainFromList(
+ Domain(
id: 1,
domain: 'google.com',
type: 0,
@@ -1180,7 +1204,9 @@ void main() {
dateAdded: DateTime.now(),
dateModified: DateTime.now(),
comment: '',
- groups: []));
+ groups: [],
+ ),
+ );
expect(response.result, APiResponseType.error);
expect(response.message, isNull);
@@ -1194,11 +1220,12 @@ void main() {
setUp(() {
server = Server(
- address: 'http://example.com',
- alias: 'example',
- defaultServer: true,
- apiVersion: SupportedApiVersions.v5,
- token: 'xxx123');
+ address: 'http://example.com',
+ alias: 'example',
+ defaultServer: true,
+ apiVersion: SupportedApiVersions.v5,
+ token: 'xxx123',
+ );
});
test('Return success when add new domain', () async {
@@ -1219,7 +1246,7 @@ void main() {
final apiGateway = ApiGatewayV5(server, client: mockClient);
final data = {
'success': true,
- 'message': 'Not adding google.com as it is already on the list'
+ 'message': 'Not adding google.com as it is already on the list',
};
when(mockClient.get(Uri.parse(url), headers: {}))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
diff --git a/test/gateways/v6/api_gateway_v6_test.dart b/test/gateways/v6/api_gateway_v6_test.dart
index 0c301155..1b77e93b 100644
--- a/test/gateways/v6/api_gateway_v6_test.dart
+++ b/test/gateways/v6/api_gateway_v6_test.dart
@@ -16,13 +16,11 @@ class SessionManagerMock implements SessionManager {
String? _sid;
String? _password;
- SessionManagerMock(this._sid, this._password);
-
@override
- get sid => _sid;
+ String? get sid => _sid;
@override
- get password async {
+ Future? get password async {
try {
return _password;
} catch (e) {
@@ -30,6 +28,8 @@ class SessionManagerMock implements SessionManager {
}
}
+ SessionManagerMock(this._sid, this._password);
+
@override
Future save(String sid) async {
_sid = sid;
@@ -64,7 +64,7 @@ void main() async {
final sessinId = 'n9n9f6c3umrumfq2ese1lvu2pg';
final urls = [
'http://example.com/api/auth',
- 'http://example.com/api/dns/blocking'
+ 'http://example.com/api/dns/blocking',
];
setUp(() {
@@ -80,11 +80,14 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
- when(mockClient.post(
- Uri.parse(urls[0]),
- headers: anyNamed('headers'),
- body: anyNamed('body'),
- )).thenAnswer((_) async => http.Response(
+ when(
+ mockClient.post(
+ Uri.parse(urls[0]),
+ headers: anyNamed('headers'),
+ body: anyNamed('body'),
+ ),
+ ).thenAnswer(
+ (_) async => http.Response(
jsonEncode({
'session': {
'valid': true,
@@ -92,18 +95,22 @@ void main() async {
'sid': 'n9n9f6c3umrumfq2ese1lvu2pg',
'csrf': 'Ux87YTIiMOf/GKCefVIOMw=',
'validity': 300,
- 'message': 'correct password'
+ 'message': 'correct password',
},
- 'took': 0.039638996124267578
+ 'took': 0.039638996124267578,
}),
- 200));
+ 200,
+ ),
+ );
int callCount = 0;
- when(mockClient.get(
- Uri.parse(urls[1]),
- headers: anyNamed('headers'),
- )).thenAnswer((_) async {
+ when(
+ mockClient.get(
+ Uri.parse(urls[1]),
+ headers: anyNamed('headers'),
+ ),
+ ).thenAnswer((_) async {
callCount++;
if (callCount == 1) {
return http.Response(
@@ -111,9 +118,9 @@ void main() async {
'error': {
'key': 'unauthorized',
'message': 'Unauthorized',
- 'hint': null
+ 'hint': null,
},
- 'took': 4.1484832763671875e-05
+ 'took': 4.1484832763671875e-05,
}),
401,
);
@@ -138,10 +145,12 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
- when(mockClient.get(
- Uri.parse(urls[1]),
- headers: anyNamed('headers'),
- )).thenAnswer((_) async {
+ when(
+ mockClient.get(
+ Uri.parse(urls[1]),
+ headers: anyNamed('headers'),
+ ),
+ ).thenAnswer((_) async {
return http.Response(
jsonEncode({'blocking': 'enabled', 'timer': null, 'took': 0.003}),
200,
@@ -160,39 +169,46 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
- when(mockClient.get(
- Uri.parse(urls[1]),
- headers: anyNamed('headers'),
- )).thenAnswer((_) async {
+ when(
+ mockClient.get(
+ Uri.parse(urls[1]),
+ headers: anyNamed('headers'),
+ ),
+ ).thenAnswer((_) async {
return http.Response(
jsonEncode({
'error': {
'key': 'unauthorized',
'message': 'Unauthorized',
- 'hint': null
+ 'hint': null,
},
- 'took': 4.1484832763671875e-05
+ 'took': 4.1484832763671875e-05,
}),
401,
);
});
- when(mockClient.post(
- Uri.parse(urls[0]),
- headers: anyNamed('headers'),
- body: anyNamed('body'),
- )).thenAnswer((_) async => http.Response(
+ when(
+ mockClient.post(
+ Uri.parse(urls[0]),
+ headers: anyNamed('headers'),
+ body: anyNamed('body'),
+ ),
+ ).thenAnswer(
+ (_) async => http.Response(
jsonEncode({
'session': {
'valid': false,
'totp': false,
'sid': null,
'validity': -1,
- 'message': 'password incorrect'
+ 'message': 'password incorrect',
},
- 'took': 0.039638996124267578
+ 'took': 0.039638996124267578,
}),
- 401));
+ 401,
+ ),
+ );
final response = await apiGateway.loginQuery();
@@ -260,28 +276,32 @@ void main() async {
'''
.trimLeft();
- when(mockClient.get(
- Uri.parse(urls[1]),
- headers: anyNamed('headers'),
- )).thenAnswer((_) async {
+ when(
+ mockClient.get(
+ Uri.parse(urls[1]),
+ headers: anyNamed('headers'),
+ ),
+ ).thenAnswer((_) async {
return http.Response(
jsonEncode({
'error': {
'key': 'unauthorized',
'message': 'Unauthorized',
- 'hint': null
+ 'hint': null,
},
- 'took': 4.1484832763671875e-05
+ 'took': 4.1484832763671875e-05,
}),
401,
);
});
- when(mockClient.post(
- Uri.parse(urls[0]),
- headers: anyNamed('headers'),
- body: anyNamed('body'),
- )).thenAnswer((_) async => http.Response(htmlString, 404));
+ when(
+ mockClient.post(
+ Uri.parse(urls[0]),
+ headers: anyNamed('headers'),
+ body: anyNamed('body'),
+ ),
+ ).thenAnswer((_) async => http.Response(htmlString, 404));
final response = await apiGateway.loginQuery();
@@ -299,28 +319,32 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
- when(mockClient.get(
- Uri.parse(urls[1]),
- headers: anyNamed('headers'),
- )).thenAnswer((_) async {
+ when(
+ mockClient.get(
+ Uri.parse(urls[1]),
+ headers: anyNamed('headers'),
+ ),
+ ).thenAnswer((_) async {
return http.Response(
jsonEncode({
'error': {
'key': 'unauthorized',
'message': 'Unauthorized',
- 'hint': null
+ 'hint': null,
},
- 'took': 4.1484832763671875e-05
+ 'took': 4.1484832763671875e-05,
}),
401,
);
});
- when(mockClient.post(
- Uri.parse(urls[0]),
- headers: anyNamed('headers'),
- body: anyNamed('body'),
- )).thenThrow(Exception('Unexpected error test'));
+ when(
+ mockClient.post(
+ Uri.parse(urls[0]),
+ headers: anyNamed('headers'),
+ body: anyNamed('body'),
+ ),
+ ).thenThrow(Exception('Unexpected error test'));
final response = await apiGateway.loginQuery();
@@ -383,7 +407,7 @@ void main() async {
'NS': 868,
'SVCB': 645,
'HTTPS': 4,
- 'OTHER': 845
+ 'OTHER': 845,
},
'status': {
'UNKNOWN': 3,
@@ -403,7 +427,7 @@ void main() async {
'IN_PROGRESS': 0,
'DBBUSY': 0,
'SPECIAL_DOMAIN': 0,
- 'CACHE_STALE': 0
+ 'CACHE_STALE': 0,
},
'replies': {
'UNKNOWN': 3,
@@ -419,15 +443,15 @@ void main() async {
'OTHER': 0,
'DNSSEC': 31,
'NONE': 0,
- 'BLOB': 0
- }
+ 'BLOB': 0,
+ },
},
'clients': {'active': 10, 'total': 22},
'gravity': {
'domains_being_blocked': 104756,
- 'last_update': 1725194639
+ 'last_update': 1725194639,
},
- 'took': 0.003
+ 'took': 0.003,
},
{
'ftl': {
@@ -436,7 +460,7 @@ void main() async {
'groups': 6,
'lists': 1,
'clients': 5,
- 'domains': {'allowed': 10, 'denied': 3}
+ 'domains': {'allowed': 10, 'denied': 3},
},
'privacy_level': 0,
'clients': {'total': 10, 'active': 8},
@@ -471,43 +495,43 @@ void main() async {
'tcp_connections': 0,
'dnssec_max_crypto_use': 0,
'dnssec_max_sig_fail': 0,
- 'dnssec_max_work': 0
- }
+ 'dnssec_max_work': 0,
+ },
},
- 'took': 0.003
+ 'took': 0.003,
},
{'blocking': 'enabled', 'timer': 15, 'took': 0.003},
{
'domains': [
- {'domain': 'pi-hole.net', 'count': 8516}
+ {'domain': 'pi-hole.net', 'count': 8516},
],
'total_queries': 29160,
'blocked_queries': 6379,
- 'took': 0.003
+ 'took': 0.003,
},
{
'domains': [
- {'domain': 'pi-hole.net', 'count': 8516}
+ {'domain': 'pi-hole.net', 'count': 8516},
],
'total_queries': 29160,
'blocked_queries': 6379,
- 'took': 0.003
+ 'took': 0.003,
},
{
'clients': [
- {'ip': '192.168.0.44', 'name': 'raspberrypi.lan', 'count': 5896}
+ {'ip': '192.168.0.44', 'name': 'raspberrypi.lan', 'count': 5896},
],
'total_queries': 29160,
'blocked_queries': 6379,
- 'took': 0.003
+ 'took': 0.003,
},
{
'clients': [
- {'ip': '192.168.0.44', 'name': 'raspberrypi.lan', 'count': 5896}
+ {'ip': '192.168.0.44', 'name': 'raspberrypi.lan', 'count': 5896},
],
'total_queries': 29160,
'blocked_queries': 6379,
- 'took': 0.003
+ 'took': 0.003,
},
{
'upstreams': [
@@ -516,14 +540,14 @@ void main() async {
'name': 'blocklist',
'port': -1,
'count': 0,
- 'statistics': {'response': 0, 'variance': 0}
+ 'statistics': {'response': 0, 'variance': 0},
},
{
'ip': 'cache',
'name': 'cache',
'port': -1,
'count': 2,
- 'statistics': {'response': 0, 'variance': 0}
+ 'statistics': {'response': 0, 'variance': 0},
},
{
'ip': '8.8.8.8',
@@ -532,20 +556,22 @@ void main() async {
'count': 8,
'statistics': {
'response': 0.0516872935824924,
- 'variance': 0.0049697216173868828
- }
+ 'variance': 0.0049697216173868828,
+ },
},
],
'total_queries': 8,
'forwarded_queries': 6,
- 'took': 5.6982040405273438e-05
- }
+ 'took': 5.6982040405273438e-05,
+ },
];
for (int i = 0; i < urls.length; i++) {
- when(mockClient.get(
- Uri.parse(urls[i]),
- headers: anyNamed('headers'),
- )).thenAnswer((_) async => http.Response(jsonEncode(data[i]), 200));
+ when(
+ mockClient.get(
+ Uri.parse(urls[i]),
+ headers: anyNamed('headers'),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data[i]), 200));
}
final response = await apiGateway.realtimeStatus();
@@ -588,10 +614,13 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
final data = {'blocking': 'disabled', 'timer': 15, 'took': 0.003};
- when(mockClient.post(Uri.parse(url),
- headers: anyNamed('headers'),
- body: jsonEncode({'blocking': false, 'timer': 15})))
- .thenAnswer((_) async => http.Response(jsonEncode(data), 200));
+ when(
+ mockClient.post(
+ Uri.parse(url),
+ headers: anyNamed('headers'),
+ body: jsonEncode({'blocking': false, 'timer': 15}),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 200));
final response = await apiGateway.disableServerRequest(15);
@@ -603,10 +632,13 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
- when(mockClient.post(Uri.parse(url),
- headers: anyNamed('headers'),
- body: jsonEncode({'blocking': false, 'timer': 15})))
- .thenThrow(Exception('Unexpected error test'));
+ when(
+ mockClient.post(
+ Uri.parse(url),
+ headers: anyNamed('headers'),
+ body: jsonEncode({'blocking': false, 'timer': 15}),
+ ),
+ ).thenThrow(Exception('Unexpected error test'));
final response = await apiGateway.disableServerRequest(5);
@@ -632,10 +664,13 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
final data = {'blocking': 'enabled', 'timer': null, 'took': 0.03};
- when(mockClient.post(Uri.parse(url),
- headers: anyNamed('headers'),
- body: jsonEncode({'blocking': true, 'timer': null})))
- .thenAnswer((_) async => http.Response(jsonEncode(data), 200));
+ when(
+ mockClient.post(
+ Uri.parse(url),
+ headers: anyNamed('headers'),
+ body: jsonEncode({'blocking': true, 'timer': null}),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 200));
final response = await apiGateway.enableServerRequest();
@@ -647,10 +682,13 @@ void main() async {
final mockClient = MockClient();
final apiGateway = ApiGatewayV6(server, client: mockClient);
- when(mockClient.post(Uri.parse(url),
- headers: anyNamed('headers'),
- body: jsonEncode({'blocking': true, 'timer': null})))
- .thenThrow(Exception('Unexpected error test'));
+ when(
+ mockClient.post(
+ Uri.parse(url),
+ headers: anyNamed('headers'),
+ body: jsonEncode({'blocking': true, 'timer': null}),
+ ),
+ ).thenThrow(Exception('Unexpected error test'));
final response = await apiGateway.enableServerRequest();
@@ -663,7 +701,7 @@ void main() async {
late Server server;
final urls = [
'http://example.com/api/history',
- 'http://example.com/api/history/clients'
+ 'http://example.com/api/history/clients',
];
setUp(() {
server = Server(
@@ -685,17 +723,17 @@ void main() async {
'total': 2134,
'cached': 525,
'blocked': 413,
- 'forwarded': 1196
+ 'forwarded': 1196,
},
{
'timestamp': 1511820500.583821,
'total': 2014,
'cached': 52,
'blocked': 43,
- 'forwarded': 1910
- }
+ 'forwarded': 1910,
+ },
],
- 'took': 0.003
+ 'took': 0.003,
},
{
'clients': {
@@ -703,7 +741,7 @@ void main() async {
'::1': {'name': 'ip6-localnet', 'total': 2100},
'192.168.1.1': {'name': null, 'total': 254},
'::': {'name': 'pi.hole', 'total': 29},
- '0.0.0.0': {'name': 'other clients', 'total': 14}
+ '0.0.0.0': {'name': 'other clients', 'total': 14},
},
'history': [
{
@@ -713,23 +751,25 @@ void main() async {
'::1': 63,
'192.168.1.1': 20,
'::': 9,
- '0.0.0.0': 0
- }
+ '0.0.0.0': 0,
+ },
},
{
'timestamp': 1511820500.583821,
- 'data': {'127.0.0.1': 10, '::1': 44, '192.168.1.1': 56, '::': 52}
- }
+ 'data': {'127.0.0.1': 10, '::1': 44, '192.168.1.1': 56, '::': 52},
+ },
],
- 'took': 0.003
+ 'took': 0.003,
},
];
for (int i = 0; i < urls.length; i++) {
- when(mockClient.get(
- Uri.parse(urls[i]),
- headers: anyNamed('headers'),
- )).thenAnswer((_) async => http.Response(jsonEncode(data[i]), 200));
+ when(
+ mockClient.get(
+ Uri.parse(urls[i]),
+ headers: anyNamed('headers'),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data[i]), 200));
}
final response = await apiGateway.fetchOverTimeData();
@@ -785,7 +825,7 @@ void main() async {
'reply': {'type': 'IP', 'time': 19},
'list_id': null,
'upstream': 'localhost#5353',
- 'dbid': 112421354
+ 'dbid': 112421354,
},
{
'id': 2,
@@ -799,14 +839,14 @@ void main() async {
'reply': {'type': 'IP', 'time': 12.3},
'list_id': null,
'upstream': 'localhost#5353',
- 'dbid': 112421355
- }
+ 'dbid': 112421355,
+ },
],
'cursor': 175881,
'recordsTotal': 1234,
'recordsFiltered': 1234,
'draw': 1,
- 'took': 0.003
+ 'took': 0.003,
};
when(mockClient.get(Uri.parse(url), headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
@@ -863,32 +903,38 @@ void main() async {
'enabled': true,
'id': 1,
'date_added': 1734008144,
- 'date_modified': 1734008144
- }
+ 'date_modified': 1734008144,
+ },
],
'processed': {
'errors': [],
'success': [
- {'item': 'google.com'}
- ]
+ {'item': 'google.com'},
+ ],
},
- 'took': 0.0042212009429931641
+ 'took': 0.0042212009429931641,
};
- when(mockClient.post(Uri.parse(url),
+ when(
+ mockClient.post(
+ Uri.parse(url),
headers: anyNamed('headers'),
body: jsonEncode({
'domain': 'google.com',
'comment': null,
'groups': [0],
- 'enabled': true
- }))).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
+ 'enabled': true,
+ }),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
final response =
await apiGateway.setWhiteBlacklist('google.com', 'black');
expect(response.result, APiResponseType.success);
- expect(response.data!.toJson(),
- {'success': true, 'message': 'Added google.com'});
+ expect(
+ response.data!.toJson(),
+ {'success': true, 'message': 'Added google.com'},
+ );
expect(response.message, isNull);
});
@@ -907,29 +953,33 @@ void main() async {
'enabled': true,
'id': 8,
'date_added': 1734005851,
- 'date_modified': 1734005851
- }
+ 'date_modified': 1734005851,
+ },
],
'processed': {
'errors': [
{
'item': 'google.com',
'error':
- 'UNIQUE constraint failed: domainlist.domain, domainlist.type'
- }
+ 'UNIQUE constraint failed: domainlist.domain, domainlist.type',
+ },
],
- 'success': []
+ 'success': [],
},
- 'took': 0.000306844711303711
+ 'took': 0.000306844711303711,
};
- when(mockClient.post(Uri.parse(url),
+ when(
+ mockClient.post(
+ Uri.parse(url),
headers: anyNamed('headers'),
body: jsonEncode({
'domain': 'google.com',
'comment': null,
'groups': [0],
- 'enabled': true
- }))).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
+ 'enabled': true,
+ }),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
final response =
await apiGateway.setWhiteBlacklist('google.com', 'black');
@@ -938,7 +988,7 @@ void main() async {
expect(response.data!.toJson(), {
'success': false,
'message':
- 'UNIQUE constraint failed: domainlist.domain, domainlist.type'
+ 'UNIQUE constraint failed: domainlist.domain, domainlist.type',
});
expect(response.message, isNull);
});
@@ -950,16 +1000,22 @@ void main() async {
'error': {
'key': 'uri_error',
'message': 'Invalid request: Specify list to modify more precisely',
- 'hint': '/api/domains/xxxx/exact'
+ 'hint': '/api/domains/xxxx/exact',
},
- 'took': 0.00055241584777832031
+ 'took': 0.00055241584777832031,
};
- when(mockClient.post(Uri.parse(url), headers: anyNamed('headers'), body: {
- 'domain': 'google.com',
- 'comment': null,
- 'groups': [0],
- 'enabled': true
- })).thenAnswer((_) async => http.Response(jsonEncode(data), 200));
+ when(
+ mockClient.post(
+ Uri.parse(url),
+ headers: anyNamed('headers'),
+ body: {
+ 'domain': 'google.com',
+ 'comment': null,
+ 'groups': [0],
+ 'enabled': true,
+ },
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 200));
final response =
await apiGateway.setWhiteBlacklist('google.com', 'black');
@@ -1013,7 +1069,7 @@ void main() async {
'enabled': true,
'id': 299,
'date_added': 1611239095,
- 'date_modified': 1612163756
+ 'date_modified': 1612163756,
},
{
'domain': 'xn--4ca.com',
@@ -1025,11 +1081,11 @@ void main() async {
'enabled': true,
'id': 305,
'date_added': 1611240635,
- 'date_modified': 1611241276
- }
+ 'date_modified': 1611241276,
+ },
],
'took': 0.012,
- 'processed': null
+ 'processed': null,
};
when(mockClient.get(Uri.parse(url), headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response(jsonEncode(data), 200));
@@ -1073,7 +1129,8 @@ void main() async {
when(mockClient.delete(Uri.parse(url), headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response(jsonEncode(data), 204));
- final response = await apiGateway.removeDomainFromList(Domain(
+ final response = await apiGateway.removeDomainFromList(
+ Domain(
id: 1,
domain: 'google.com',
type: 0,
@@ -1081,7 +1138,9 @@ void main() async {
dateAdded: DateTime.now(),
dateModified: DateTime.now(),
comment: '',
- groups: []));
+ groups: [],
+ ),
+ );
expect(response.result, APiResponseType.success);
expect(response.message, isNull);
@@ -1094,7 +1153,8 @@ void main() async {
when(mockClient.delete(Uri.parse(url), headers: anyNamed('headers')))
.thenThrow(Exception('Unexpected error test'));
- final response = await apiGateway.removeDomainFromList(Domain(
+ final response = await apiGateway.removeDomainFromList(
+ Domain(
id: 1,
domain: 'google.com',
type: 0,
@@ -1102,7 +1162,9 @@ void main() async {
dateAdded: DateTime.now(),
dateModified: DateTime.now(),
comment: '',
- groups: []));
+ groups: [],
+ ),
+ );
expect(response.result, APiResponseType.error);
expect(response.message, isNull);
@@ -1137,25 +1199,29 @@ void main() async {
'enabled': true,
'id': 1,
'date_added': 1734008144,
- 'date_modified': 1734008144
- }
+ 'date_modified': 1734008144,
+ },
],
'processed': {
'errors': [],
'success': [
- {'item': 'google.com'}
- ]
+ {'item': 'google.com'},
+ ],
},
- 'took': 0.0042212009429931641
+ 'took': 0.0042212009429931641,
};
- when(mockClient.post(Uri.parse(url),
+ when(
+ mockClient.post(
+ Uri.parse(url),
headers: anyNamed('headers'),
body: jsonEncode({
'domain': 'google.com',
'comment': null,
'groups': [0],
- 'enabled': true
- }))).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
+ 'enabled': true,
+ }),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
final response = await apiGateway
.addDomainToList({'list': 'black', 'domain': 'google.com'});
@@ -1178,29 +1244,33 @@ void main() async {
'enabled': true,
'id': 8,
'date_added': 1734005851,
- 'date_modified': 1734005851
- }
+ 'date_modified': 1734005851,
+ },
],
'processed': {
'errors': [
{
'item': 'google.com',
'error':
- 'UNIQUE constraint failed: domainlist.domain, domainlist.type'
- }
+ 'UNIQUE constraint failed: domainlist.domain, domainlist.type',
+ },
],
- 'success': []
+ 'success': [],
},
- 'took': 0.000306844711303711
+ 'took': 0.000306844711303711,
};
- when(mockClient.post(Uri.parse(url),
+ when(
+ mockClient.post(
+ Uri.parse(url),
headers: anyNamed('headers'),
body: jsonEncode({
'domain': 'google.com',
'comment': null,
'groups': [0],
- 'enabled': true
- }))).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
+ 'enabled': true,
+ }),
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 201));
final response = await apiGateway
.addDomainToList({'list': 'black', 'domain': 'google.com'});
@@ -1215,16 +1285,22 @@ void main() async {
'error': {
'key': 'uri_error',
'message': 'Invalid request: Specify list to modify more precisely',
- 'hint': '/api/domains/xxxx/exact'
+ 'hint': '/api/domains/xxxx/exact',
},
- 'took': 0.00055241584777832031
+ 'took': 0.00055241584777832031,
};
- when(mockClient.post(Uri.parse(url), headers: anyNamed('headers'), body: {
- 'domain': 'google.com',
- 'comment': null,
- 'groups': [0],
- 'enabled': true
- })).thenAnswer((_) async => http.Response(jsonEncode(data), 200));
+ when(
+ mockClient.post(
+ Uri.parse(url),
+ headers: anyNamed('headers'),
+ body: {
+ 'domain': 'google.com',
+ 'comment': null,
+ 'groups': [0],
+ 'enabled': true,
+ },
+ ),
+ ).thenAnswer((_) async => http.Response(jsonEncode(data), 200));
final response = await apiGateway
.addDomainToList({'list': 'black', 'domain': 'google.com'});
diff --git a/test/repository/database_test.dart b/test/repository/database_test.dart
new file mode 100644
index 00000000..51de3cc0
--- /dev/null
+++ b/test/repository/database_test.dart
@@ -0,0 +1,307 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:mockito/mockito.dart';
+import 'package:mockito/annotations.dart';
+import 'package:pi_hole_client/services/session_manager.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
+import 'package:pi_hole_client/models/server.dart';
+import 'package:pi_hole_client/repository/secure_storage.dart';
+import 'package:pi_hole_client/repository/database.dart';
+import './database_test.mocks.dart';
+
+/// Initialize sqflite for test.
+void sqfliteTestInit() {
+ sqfliteFfiInit();
+ databaseFactory = databaseFactoryFfi;
+}
+
+@GenerateMocks([SecureStorageRepository, FlutterSecureStorage])
+void main() async {
+ // Initialize the sqflite for testing
+ sqfliteTestInit();
+
+ // For loading the .env file
+ TestWidgetsFlutterBinding.ensureInitialized();
+ await dotenv.load(fileName: '.env');
+
+ final testDb = 'test.db';
+
+ group('DatabaseRepository.loadDb', () {
+ late MockSecureStorageRepository mockSecureStorage;
+ late DatabaseRepository databaseRepository;
+
+ setUp(() async {
+ mockSecureStorage = MockSecureStorageRepository();
+ databaseRepository = DatabaseRepository(mockSecureStorage);
+
+ when(mockSecureStorage.getValue(any)).thenAnswer((_) async => null);
+ when(mockSecureStorage.readAll()).thenAnswer(
+ (_) async => {},
+ );
+
+ // Use test sqflite database. Before each test, initialize the database
+ await databaseRepository.initialize(path: testDb);
+ await databaseRepository.deleteServersDataQuery();
+ await databaseRepository.restoreAppConfigQuery();
+ await databaseRepository.closeDb();
+ });
+
+ tearDown(() async {
+ await databaseRepository.closeDb();
+ });
+
+ test('should initialize database with default values', () async {
+ databaseRepository = DatabaseRepository(mockSecureStorage);
+ await databaseRepository.initialize(path: testDb);
+
+ final servers = databaseRepository.servers;
+ final appConfig = databaseRepository.appConfig;
+ expect(servers, []);
+ expect(appConfig, isNotNull);
+ expect(appConfig.autoRefreshTime, 5);
+ expect(appConfig.theme, 0);
+ expect(appConfig.language, 'en');
+ expect(appConfig.overrideSslCheck, 0);
+ expect(appConfig.oneColumnLegend, 0);
+ expect(appConfig.reducedDataCharts, 0);
+ expect(appConfig.logsPerQuery, 2);
+ expect(appConfig.passCode, isNull);
+ expect(appConfig.useBiometricAuth, 0);
+ expect(appConfig.sendCrashReports, 0);
+ });
+
+ test('should return one registered server without passcode', () async {
+ //Mock the secure storage calls
+ when(mockSecureStorage.getValue('http://localhost:8080_token'))
+ .thenAnswer((_) async => '');
+ when(mockSecureStorage.getValue('http://localhost:8080_basicAuthUser'))
+ .thenAnswer((_) async => '');
+ when(
+ mockSecureStorage.getValue('http://localhost:8080_basicAuthPassword'),
+ ).thenAnswer((_) async => '');
+ when(mockSecureStorage.getValue('http://localhost:8080_sid')).thenAnswer(
+ (_) async => '',
+ );
+ when(mockSecureStorage.readAll()).thenAnswer(
+ (_) async => {
+ 'http://localhost:8080_token': '',
+ 'http://localhost:8080_basicAuthUser': '',
+ 'http://localhost:8080_basicAuthPassword': '',
+ 'http://localhost:8080_sid': '',
+ },
+ );
+
+ // Add test server
+ final server = Server(
+ address: 'http://localhost:8080',
+ alias: 'test v6',
+ defaultServer: false,
+ apiVersion: 'v6',
+ sm: SessionManager(
+ MockSecureStorageRepository(),
+ 'http://localhost:8080',
+ ),
+ );
+ final DatabaseRepository databaseRepositoryTmp =
+ DatabaseRepository(mockSecureStorage);
+ await databaseRepositoryTmp.initialize(path: testDb);
+ await databaseRepositoryTmp.saveServerQuery(server);
+ await databaseRepositoryTmp.closeDb();
+
+ databaseRepository = DatabaseRepository(mockSecureStorage);
+ await databaseRepository.initialize(path: testDb);
+
+ // Assert the returned data
+ final servers = databaseRepository.servers;
+ final appConfig = databaseRepository.appConfig;
+ expect(servers, isNotNull);
+ expect(servers.length, 1);
+ expect(servers[0].address, 'http://localhost:8080');
+ expect(servers[0].alias, 'test v6');
+ expect(servers[0].isDefaultServer, 0);
+ expect(servers[0].apiVersion, 'v6');
+
+ expect(appConfig, isNotNull);
+ expect(appConfig.autoRefreshTime, 5);
+ expect(appConfig.theme, 0);
+ expect(appConfig.language, 'en');
+ expect(appConfig.overrideSslCheck, 0);
+ expect(appConfig.oneColumnLegend, 0);
+ expect(appConfig.reducedDataCharts, 0);
+ expect(appConfig.logsPerQuery, 2);
+ expect(appConfig.passCode, isNull);
+ expect(appConfig.useBiometricAuth, 0);
+ expect(appConfig.sendCrashReports, 0);
+ });
+
+ test('should return one registered server with passcode', () async {
+ //Mock the secure storage calls
+ when(mockSecureStorage.getValue('passCode'))
+ .thenAnswer((_) async => '9999');
+ when(mockSecureStorage.getValue('http://localhost:8080_token'))
+ .thenAnswer((_) async => '');
+ when(mockSecureStorage.getValue('http://localhost:8080_basicAuthUser'))
+ .thenAnswer((_) async => '');
+ when(
+ mockSecureStorage.getValue('http://localhost:8080_basicAuthPassword'),
+ ).thenAnswer((_) async => '');
+ when(mockSecureStorage.getValue('http://localhost:8080_sid')).thenAnswer(
+ (_) async => '',
+ );
+ when(mockSecureStorage.readAll()).thenAnswer(
+ (_) async => {
+ 'passCode': '9999',
+ 'http://localhost:8080_token': '',
+ 'http://localhost:8080_basicAuthUser': '',
+ 'http://localhost:8080_basicAuthPassword': '',
+ 'http://localhost:8080_sid': '',
+ },
+ );
+
+ // Add test server
+ final server = Server(
+ address: 'http://localhost:8080',
+ alias: 'test v6',
+ defaultServer: false,
+ apiVersion: 'v6',
+ sm: SessionManager(
+ MockSecureStorageRepository(),
+ 'http://localhost:8080',
+ ),
+ );
+ final DatabaseRepository databaseRepositoryTmp =
+ DatabaseRepository(mockSecureStorage);
+ await databaseRepositoryTmp.initialize(path: testDb);
+ await databaseRepositoryTmp.saveServerQuery(server);
+ await databaseRepositoryTmp.closeDb();
+
+ databaseRepository = DatabaseRepository(mockSecureStorage);
+ await databaseRepository.initialize(path: testDb);
+
+ // Assert the returned data
+ final servers = databaseRepository.servers;
+ final appConfig = databaseRepository.appConfig;
+ expect(servers, isNotNull);
+ expect(servers.length, 1);
+ expect(servers[0].address, 'http://localhost:8080');
+ expect(servers[0].alias, 'test v6');
+ expect(servers[0].isDefaultServer, 0);
+ expect(servers[0].apiVersion, 'v6');
+
+ expect(appConfig, isNotNull);
+ expect(appConfig.autoRefreshTime, 5);
+ expect(appConfig.theme, 0);
+ expect(appConfig.language, 'en');
+ expect(appConfig.overrideSslCheck, 0);
+ expect(appConfig.oneColumnLegend, 0);
+ expect(appConfig.reducedDataCharts, 0);
+ expect(appConfig.logsPerQuery, 2);
+ expect(appConfig.passCode, '9999');
+ expect(appConfig.useBiometricAuth, 0);
+ expect(appConfig.sendCrashReports, 0);
+ });
+ });
+
+ group('DatabaseRepository.updateConfigQuery', () {
+ late MockFlutterSecureStorage mockFlutterSecureStorage;
+ late SecureStorageRepository secureStorage;
+ late DatabaseRepository databaseRepository;
+
+ setUp(() async {
+ mockFlutterSecureStorage = MockFlutterSecureStorage();
+ secureStorage =
+ SecureStorageRepository(secureStorage: mockFlutterSecureStorage);
+ databaseRepository = DatabaseRepository(secureStorage);
+
+ when(mockFlutterSecureStorage.read(key: anyNamed('key')))
+ .thenAnswer((_) async => null);
+ when(mockFlutterSecureStorage.readAll()).thenAnswer(
+ (_) async => {},
+ );
+
+ // Use test sqflite database. Before each test, initialize the database
+ await databaseRepository.initialize(path: testDb);
+ });
+
+ tearDown(() async {
+ await databaseRepository.closeDb();
+ });
+
+ test(
+ 'should delete key from secure storage when null is passed (Clear)',
+ () async {
+ const testColumn = 'passCode';
+
+ when(mockFlutterSecureStorage.delete(key: testColumn))
+ .thenAnswer((_) async {});
+
+ final result = await databaseRepository.updateConfigQuery(
+ column: testColumn,
+ value: null,
+ );
+
+ // Check call to delete
+ // Check not call to write
+ expect(result, true);
+ verify(mockFlutterSecureStorage.delete(key: testColumn)).called(1);
+ verifyNever(
+ mockFlutterSecureStorage.write(
+ key: testColumn,
+ value: anyNamed('value'),
+ ),
+ );
+ },
+ );
+
+ test(
+ 'should save value to secure storage when non-null value is passed (Update)',
+ () async {
+ const testColumn = 'passCode';
+ const testValue = '1234';
+
+ when(mockFlutterSecureStorage.write(key: testColumn, value: testValue))
+ .thenAnswer(
+ (_) async {},
+ );
+
+ final result = await databaseRepository.updateConfigQuery(
+ column: testColumn,
+ value: testValue,
+ );
+
+ // Check not call to delete
+ // Check call to write
+ expect(result, true);
+ verifyNever(mockFlutterSecureStorage.delete(key: testColumn));
+ verify(
+ mockFlutterSecureStorage.write(
+ key: testColumn,
+ value: testValue,
+ ),
+ ).called(1);
+ },
+ );
+
+ test('should return false if secure storage delete fails', () async {
+ const testColumn = 'passCode';
+
+ when(mockFlutterSecureStorage.delete(key: testColumn))
+ .thenThrow(Exception('Failed to delete'));
+
+ final result = await databaseRepository.updateConfigQuery(
+ column: testColumn,
+ value: null,
+ );
+
+ expect(result, false);
+ verify(mockFlutterSecureStorage.delete(key: testColumn)).called(1);
+ verifyNever(
+ mockFlutterSecureStorage.write(
+ key: testColumn,
+ value: anyNamed('value'),
+ ),
+ );
+ });
+ });
+}
diff --git a/test/repository/database_test.mocks.dart b/test/repository/database_test.mocks.dart
new file mode 100644
index 00000000..802248eb
--- /dev/null
+++ b/test/repository/database_test.mocks.dart
@@ -0,0 +1,441 @@
+// Mocks generated by Mockito 5.4.4 from annotations
+// in pi_hole_client/test/repository/database_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i4;
+
+import 'package:flutter/foundation.dart' as _i5;
+import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:pi_hole_client/repository/secure_storage.dart' as _i3;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeIOSOptions_0 extends _i1.SmartFake implements _i2.IOSOptions {
+ _FakeIOSOptions_0(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
+class _FakeAndroidOptions_1 extends _i1.SmartFake
+ implements _i2.AndroidOptions {
+ _FakeAndroidOptions_1(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
+class _FakeLinuxOptions_2 extends _i1.SmartFake implements _i2.LinuxOptions {
+ _FakeLinuxOptions_2(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
+class _FakeWindowsOptions_3 extends _i1.SmartFake
+ implements _i2.WindowsOptions {
+ _FakeWindowsOptions_3(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
+class _FakeWebOptions_4 extends _i1.SmartFake implements _i2.WebOptions {
+ _FakeWebOptions_4(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
+class _FakeMacOsOptions_5 extends _i1.SmartFake implements _i2.MacOsOptions {
+ _FakeMacOsOptions_5(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
+/// A class which mocks [SecureStorageRepository].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockSecureStorageRepository extends _i1.Mock
+ implements _i3.SecureStorageRepository {
+ MockSecureStorageRepository() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i4.Future saveValue(
+ String? key,
+ String? value,
+ ) =>
+ (super.noSuchMethod(
+ Invocation.method(
+ #saveValue,
+ [
+ key,
+ value,
+ ],
+ ),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ ) as _i4.Future);
+
+ @override
+ _i4.Future getValue(String? key) => (super.noSuchMethod(
+ Invocation.method(
+ #getValue,
+ [key],
+ ),
+ returnValue: _i4.Future.value(),
+ ) as _i4.Future);
+
+ @override
+ _i4.Future deleteValue(String? key) => (super.noSuchMethod(
+ Invocation.method(
+ #deleteValue,
+ [key],
+ ),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ ) as _i4.Future);
+
+ @override
+ _i4.Future clearAll() => (super.noSuchMethod(
+ Invocation.method(
+ #clearAll,
+ [],
+ ),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ ) as _i4.Future);
+
+ @override
+ _i4.Future