diff --git a/.gitignore b/.gitignore
index a45e2a8..1f54de6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,6 @@
backend/**/target
finlog_app/**/.idea
-finlog_app/**/.dart_tool
\ No newline at end of file
+finlog_app/**/.dart_tool
+
+finlog_app/**/pubspec.lock
\ No newline at end of file
diff --git a/backend/.gitlab-ci.yml b/backend/.gitlab-ci.yml
index a126f41..c88a6c4 100644
--- a/backend/.gitlab-ci.yml
+++ b/backend/.gitlab-ci.yml
@@ -7,5 +7,7 @@ build_backend:
artifacts:
paths:
- backend/common/target/
- - backend/server/target
+ - backend/discovery/target/
+ - backend/gateway/target/
+ - backend/server/target/
expire_in: 1 hour
diff --git a/backend/discovery/pom.xml b/backend/discovery/pom.xml
new file mode 100644
index 0000000..541418c
--- /dev/null
+++ b/backend/discovery/pom.xml
@@ -0,0 +1,13 @@
+
+
+ 4.0.0
+
+ dev.rheinsw.finlog.backend
+ backend
+ ${revision}
+
+
+ discovery
+
\ No newline at end of file
diff --git a/backend/gateway/pom.xml b/backend/gateway/pom.xml
new file mode 100644
index 0000000..db4c83a
--- /dev/null
+++ b/backend/gateway/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+ dev.rheinsw.finlog.backend
+ backend
+ ${revision}
+
+
+ gateway
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+ dev.rheinsw.finlog.backend
+ common
+ ${revision}
+ compile
+
+
+
+
\ No newline at end of file
diff --git a/backend/pom.xml b/backend/pom.xml
index d0f671d..056d12a 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -12,6 +12,8 @@
common
server
+ gateway
+ discovery
diff --git a/finlog_app/app/lib/main.dart b/finlog_app/app/lib/main.dart
index 7b7f5b6..8f08deb 100644
--- a/finlog_app/app/lib/main.dart
+++ b/finlog_app/app/lib/main.dart
@@ -1,6 +1,19 @@
import 'package:flutter/material.dart';
+import 'package:fluttery/fluttery.dart';
+import 'package:fluttery/logger.dart';
+import 'package:fluttery/worker.dart';
+
+Future main() async {
+ // Ensures that the Flutter engine and widget binding
+ // are initialized before using async services or plugins
+ WidgetsFlutterBinding.ensureInitialized();
+
+ // any services
+ App.registerDefaultServices();
+
+ final logger = App.service();
+ logger.debug("[MAIN] Registered all default services");
-void main() {
runApp(const MyApp());
}
@@ -10,24 +23,11 @@ class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
+ App.service().info("test");
+
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
- // This is the theme of your application.
- //
- // TRY THIS: Try running your application with "flutter run". You'll see
- // the application has a purple toolbar. Then, without quitting the app,
- // try changing the seedColor in the colorScheme below to Colors.green
- // and then invoke "hot reload" (save your changes or press the "hot
- // reload" button in a Flutter-supported IDE, or press "r" if you used
- // the command line to start the app).
- //
- // Notice that the counter didn't reset back to zero; the application
- // state is not lost during the reload. To reset the state, use hot
- // restart instead.
- //
- // This works for code too, not just values: Most code changes can be
- // tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
@@ -38,15 +38,6 @@ class MyApp extends StatelessWidget {
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
- // This widget is the home page of your application. It is stateful, meaning
- // that it has a State object (defined below) that contains fields that affect
- // how it looks.
-
- // This class is the configuration for the state. It holds the values (in this
- // case the title) provided by the parent (in this case the App widget) and
- // used by the build method of the State. Fields in a Widget subclass are
- // always marked "final".
-
final String title;
@override
@@ -57,51 +48,27 @@ class _MyHomePageState extends State {
int _counter = 0;
void _incrementCounter() {
+ App.service().spawn("worker-$_counter", () async {
+ App.service().info("test");
+
+ await Future.delayed(const Duration(seconds: 10));
+
+ App.service().info("end worker");
+ });
setState(() {
- // This call to setState tells the Flutter framework that something has
- // changed in this State, which causes it to rerun the build method below
- // so that the display can reflect the updated values. If we changed
- // _counter without calling setState(), then the build method would not be
- // called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
- // This method is rerun every time setState is called, for instance as done
- // by the _incrementCounter method above.
- //
- // The Flutter framework has been optimized to make rerunning build methods
- // fast, so that you can just rebuild anything that needs updating rather
- // than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
- // TRY THIS: Try changing the color here to a specific color (to
- // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
- // change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
- // Here we take the value from the MyHomePage object that was created by
- // the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
- // Center is a layout widget. It takes a single child and positions it
- // in the middle of the parent.
child: Column(
- // Column is also a layout widget. It takes a list of children and
- // arranges them vertically. By default, it sizes itself to fit its
- // children horizontally, and tries to be as tall as its parent.
- //
- // Column has various properties to control how it sizes itself and
- // how it positions its children. Here we use mainAxisAlignment to
- // center the children vertically; the main axis here is the vertical
- // axis because Columns are vertical (the cross axis would be
- // horizontal).
- //
- // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
- // action in the IDE, or press "p" in the console), to see the
- // wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
@@ -109,6 +76,11 @@ class _MyHomePageState extends State {
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
+ TextButton(
+ onPressed: () {
+ },
+ child: Text("Print workers"),
+ ),
],
),
),
diff --git a/finlog_app/app/pubspec.yaml b/finlog_app/app/pubspec.yaml
index 09d2538..4a153ec 100644
--- a/finlog_app/app/pubspec.yaml
+++ b/finlog_app/app/pubspec.yaml
@@ -31,6 +31,8 @@ environment:
dependencies:
flutter:
sdk: flutter
+ fluttery:
+ path: ../fluttery
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
diff --git a/finlog_app/app/test/widget_test.dart b/finlog_app/app/test/widget_test.dart
index 4e2a713..6a6887d 100644
--- a/finlog_app/app/test/widget_test.dart
+++ b/finlog_app/app/test/widget_test.dart
@@ -1,30 +1,7 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
-import 'package:app/main.dart';
-
void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(const MyApp());
-
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
-
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add));
- await tester.pump();
-
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
+ test('description', () {
+ expect(1, 1);
});
}
diff --git a/finlog_app/fluttery/lib/environment.dart b/finlog_app/fluttery/lib/environment.dart
new file mode 100644
index 0000000..1e9e91b
--- /dev/null
+++ b/finlog_app/fluttery/lib/environment.dart
@@ -0,0 +1,27 @@
+import 'package:fluttery/fluttery.dart';
+
+/// Abstract Environment contract
+abstract class Environment extends Service {
+ /// Platform checks
+ bool get isAndroid;
+
+ bool get isIOS;
+
+ /// Build mode
+ bool get isDebug;
+
+ bool get isRelease;
+
+ bool get isProfile;
+
+ /// App info
+ Future loadPackageInfo();
+
+ String get appName;
+
+ String get packageName;
+
+ String get version;
+
+ String get buildNumber;
+}
diff --git a/finlog_app/fluttery/lib/fluttery.dart b/finlog_app/fluttery/lib/fluttery.dart
index 298576d..02e1dce 100644
--- a/finlog_app/fluttery/lib/fluttery.dart
+++ b/finlog_app/fluttery/lib/fluttery.dart
@@ -1,5 +1,73 @@
-/// A Calculator.
-class Calculator {
- /// Returns [value] plus 1.
- int addOne(int value) => value + 1;
+library;
+
+import 'package:fluttery/environment.dart';
+import 'package:fluttery/logger.dart';
+import 'package:fluttery/preferences.dart';
+import 'package:fluttery/secure_storage.dart';
+import 'package:fluttery/src/logger/logger_impl.dart';
+import 'package:fluttery/src/preferences/preferences_impl.dart';
+import 'package:fluttery/src/storage/secure/secure_storage_impl.dart';
+import 'package:fluttery/src/system/environment/environment_impl.dart';
+import 'package:fluttery/src/system/worker/worker_impl.dart';
+import 'package:fluttery/worker.dart';
+import 'package:kiwi/kiwi.dart';
+
+/// A class to manage services.
+class App {
+ static final _AppService _appService = _AppService();
+
+ /// Registers a service with a factory function to instantiate the implementation.
+ ///
+ /// This ensures that the implementation is created when the service is requested.
+ ///
+ /// `implFactory` - A factory method to create the service implementation.
+ static void registerService(T Function() implFactory) {
+ _appService.registerSingleton(implFactory);
+ }
+
+ /// Retrieves the registered service.
+ ///
+ /// Returns an instance of the registered service.
+ static T service() {
+ return _appService.resolve();
+ }
+
+ /// Registers the default services required by the application.
+ static void registerDefaultServices() {
+ registerService(() => LoggerImpl());
+ registerService(() => PreferencesImpl());
+ registerService(() => EnvironmentImpl());
+ registerService(() => SecureStorageImpl());
+ registerService(() => WorkerImpl());
+ }
+}
+
+/// Abstract class to represent a service.
+abstract class Service {}
+
+/// Internal class to manage the registration and resolution of services.
+class _AppService {
+ static _AppService? _singleton;
+
+ static final KiwiContainer _kiwi = KiwiContainer();
+
+ /// Factory constructor to ensure singleton instance of _AppService.
+ factory _AppService() => _singleton ??= _AppService._();
+
+ /// Private constructor.
+ _AppService._();
+
+ /// Registers a singleton service with a factory function to create the instance.
+ ///
+ /// `serviceFactory` - A factory method to create the service implementation.
+ void registerSingleton(T Function() serviceFactory) {
+ _kiwi.registerSingleton((c) => serviceFactory());
+ }
+
+ /// Resolves and retrieves the registered service.
+ ///
+ /// Returns an instance of the registered service.
+ T resolve() {
+ return _kiwi.resolve();
+ }
}
diff --git a/finlog_app/fluttery/lib/logger.dart b/finlog_app/fluttery/lib/logger.dart
new file mode 100644
index 0000000..ee1f438
--- /dev/null
+++ b/finlog_app/fluttery/lib/logger.dart
@@ -0,0 +1,33 @@
+import 'package:fluttery/fluttery.dart';
+import 'package:logging/logging.dart' as lib;
+
+/// Abstract class for logging service.
+/// Provides methods for different log levels and configuration.
+abstract class Logger extends Service {
+ /// Logs an informational message.
+ ///
+ /// [message] is the information to log.
+ void info(String message);
+
+ /// Logs a warning message.
+ ///
+ /// [message] is the warning to log.
+ void warning(String message);
+
+ /// Logs an error message with optional error and stack trace.
+ ///
+ /// [message] is the error message to log.
+ /// [error] is the optional error object associated with this log entry.
+ /// [stackTrace] is the optional stack trace associated with this log entry.
+ void error(String message, [Object? error, StackTrace? stackTrace]);
+
+ /// Logs a debug message.
+ ///
+ /// [message] is the debug message to log.
+ void debug(String message);
+
+ /// Sets the log level for the logger.
+ ///
+ /// [level] is the new log level to set.
+ void setLogLevel(lib.Level level);
+}
diff --git a/finlog_app/fluttery/lib/preferences.dart b/finlog_app/fluttery/lib/preferences.dart
new file mode 100644
index 0000000..f0b7585
--- /dev/null
+++ b/finlog_app/fluttery/lib/preferences.dart
@@ -0,0 +1,40 @@
+import 'package:fluttery/fluttery.dart';
+
+/// providing methods for managing persistent storage of key-value pairs.
+abstract class Preferences implements Service {
+ /// Stores a string value with the given [key].
+ Future setString(String key, String value);
+
+ /// Retrieves the string value associated with the given [key].
+ Future getString(String key);
+
+ /// Stores an integer value with the given [key].
+ Future setInt(String key, int value);
+
+ /// Retrieves the integer value associated with the given [key].
+ Future getInt(String key);
+
+ /// Stores a boolean value with the given [key].
+ Future setBool(String key, bool value);
+
+ /// Retrieves the boolean value associated with the given [key].
+ Future getBool(String key);
+
+ /// Stores a double value with the given [key].
+ Future setDouble(String key, double value);
+
+ /// Retrieves the double value associated with the given [key].
+ Future getDouble(String key);
+
+ /// Stores a list of strings with the given [key].
+ Future setStringList(String key, List value);
+
+ /// Retrieves the list of strings associated with the given [key].
+ Future?> getStringList(String key);
+
+ /// Removes the key-value pair associated with the given [key].
+ Future remove(String key);
+
+ /// Clears all key-value pairs in the preferences.
+ Future clear();
+}
diff --git a/finlog_app/fluttery/lib/secure_storage.dart b/finlog_app/fluttery/lib/secure_storage.dart
new file mode 100644
index 0000000..af89b8c
--- /dev/null
+++ b/finlog_app/fluttery/lib/secure_storage.dart
@@ -0,0 +1,75 @@
+import 'package:fluttery/fluttery.dart';
+
+/// Interface for secure storage operations.
+///
+/// Provides methods for securely storing and retrieving sensitive data
+/// like passwords, tokens, API keys, etc. Data stored through this interface
+/// is encrypted and stored in the device's secure storage (Keychain on iOS,
+/// Keystore on Android).
+abstract class SecureStorage implements Service {
+ /// Stores a string value securely with the given [key].
+ ///
+ /// Returns a Future that completes when the value is successfully stored.
+ /// Throws an exception if the storage operation fails.
+ Future write(String key, String value);
+
+ /// Retrieves the securely stored string value for the given [key].
+ ///
+ /// Returns the stored value if found, null otherwise.
+ /// Throws an exception if the retrieval operation fails.
+ Future read(String key);
+
+ /// Stores an integer value securely with the given [key].
+ ///
+ /// The integer is converted to a string for storage.
+ Future writeInt(String key, int value);
+
+ /// Retrieves the securely stored integer value for the given [key].
+ ///
+ /// Returns the stored integer if found and valid, null otherwise.
+ Future readInt(String key);
+
+ /// Stores a boolean value securely with the given [key].
+ ///
+ /// The boolean is converted to a string for storage.
+ Future writeBool(String key, bool value);
+
+ /// Retrieves the securely stored boolean value for the given [key].
+ ///
+ /// Returns the stored boolean if found and valid, null otherwise.
+ Future readBool(String key);
+
+ /// Stores a double value securely with the given [key].
+ ///
+ /// The double is converted to a string for storage.
+ Future writeDouble(String key, double value);
+
+ /// Retrieves the securely stored double value for the given [key].
+ ///
+ /// Returns the stored double if found and valid, null otherwise.
+ Future readDouble(String key);
+
+ /// Removes the securely stored value for the given [key].
+ ///
+ /// Returns a Future that completes when the value is successfully removed.
+ Future delete(String key);
+
+ /// Removes all securely stored values.
+ ///
+ /// Returns a Future that completes when all values are successfully removed.
+ /// Use with caution as this operation cannot be undone.
+ Future deleteAll();
+
+ /// Returns all keys incl. values in secure storage
+ Future> readAll();
+
+ /// Returns all keys currently stored in secure storage.
+ ///
+ /// Useful for debugging or migration purposes.
+ Future> readAllKeys();
+
+ /// Checks if a value exists for the given [key].
+ ///
+ /// Returns true if a value exists, false otherwise.
+ Future containsKey(String key);
+}
diff --git a/finlog_app/fluttery/lib/src/logger/logger_impl.dart b/finlog_app/fluttery/lib/src/logger/logger_impl.dart
new file mode 100644
index 0000000..a9429b4
--- /dev/null
+++ b/finlog_app/fluttery/lib/src/logger/logger_impl.dart
@@ -0,0 +1,57 @@
+import 'package:flutter/material.dart';
+import 'package:fluttery/logger.dart';
+import 'package:logging/logging.dart' as lib;
+
+// ignore_for_file: avoid_print
+class LoggerImpl implements Logger {
+ final lib.Logger _logger;
+
+ // coverage:ignore-start
+ /// Constructor
+ LoggerImpl() : _logger = lib.Logger("Logger") {
+ _logger.onRecord.listen((lib.LogRecord record) {
+ print('${record.level.name}: ${record.time}: ${record.message}');
+ if (record.error != null) {
+ print('Error: ${record.error}');
+ }
+ if (record.stackTrace != null) {
+ print('Stack Trace: ${record.stackTrace}');
+ }
+ });
+ }
+
+ // coverage:ignore-end
+ @visibleForTesting
+ factory LoggerImpl.forTest(lib.Logger logger) {
+ final instance = LoggerImpl._internal(logger);
+ return instance;
+ }
+
+ // Private internal constructor
+ LoggerImpl._internal(this._logger);
+
+ @override
+ void info(String message) {
+ _logger.info(message);
+ }
+
+ @override
+ void warning(String message) {
+ _logger.warning(message);
+ }
+
+ @override
+ void error(String message, [Object? error, StackTrace? stackTrace]) {
+ _logger.severe(message, error, stackTrace);
+ }
+
+ @override
+ void debug(String message) {
+ _logger.fine(message);
+ }
+
+ @override
+ void setLogLevel(lib.Level level) {
+ _logger.level = level;
+ }
+}
diff --git a/finlog_app/fluttery/lib/src/preferences/preferences_impl.dart b/finlog_app/fluttery/lib/src/preferences/preferences_impl.dart
new file mode 100644
index 0000000..5b8d7b1
--- /dev/null
+++ b/finlog_app/fluttery/lib/src/preferences/preferences_impl.dart
@@ -0,0 +1,86 @@
+import 'package:fluttery/preferences.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class PreferencesImpl implements Preferences {
+ late final SharedPreferences _prefs;
+ bool _initialized = false;
+
+ Future _ensureInitialized() async {
+ if (!_initialized) {
+ _prefs = await SharedPreferences.getInstance();
+ _initialized = true;
+ }
+ }
+
+ @override
+ Future setString(String key, String value) async {
+ await _ensureInitialized();
+ await _prefs.setString(key, value);
+ }
+
+ @override
+ Future getString(String key) async {
+ await _ensureInitialized();
+ return _prefs.getString(key);
+ }
+
+ @override
+ Future setInt(String key, int value) async {
+ await _ensureInitialized();
+ await _prefs.setInt(key, value);
+ }
+
+ @override
+ Future getInt(String key) async {
+ await _ensureInitialized();
+ return _prefs.getInt(key);
+ }
+
+ @override
+ Future setBool(String key, bool value) async {
+ await _ensureInitialized();
+ await _prefs.setBool(key, value);
+ }
+
+ @override
+ Future getBool(String key) async {
+ await _ensureInitialized();
+ return _prefs.getBool(key);
+ }
+
+ @override
+ Future setDouble(String key, double value) async {
+ await _ensureInitialized();
+ await _prefs.setDouble(key, value);
+ }
+
+ @override
+ Future getDouble(String key) async {
+ await _ensureInitialized();
+ return _prefs.getDouble(key);
+ }
+
+ @override
+ Future setStringList(String key, List value) async {
+ await _ensureInitialized();
+ await _prefs.setStringList(key, value);
+ }
+
+ @override
+ Future?> getStringList(String key) async {
+ await _ensureInitialized();
+ return _prefs.getStringList(key);
+ }
+
+ @override
+ Future remove(String key) async {
+ await _ensureInitialized();
+ await _prefs.remove(key);
+ }
+
+ @override
+ Future clear() async {
+ await _ensureInitialized();
+ await _prefs.clear();
+ }
+}
\ No newline at end of file
diff --git a/finlog_app/fluttery/lib/src/storage/secure/secure_storage_impl.dart b/finlog_app/fluttery/lib/src/storage/secure/secure_storage_impl.dart
new file mode 100644
index 0000000..df5b7f2
--- /dev/null
+++ b/finlog_app/fluttery/lib/src/storage/secure/secure_storage_impl.dart
@@ -0,0 +1,90 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:fluttery/secure_storage.dart';
+
+class SecureStorageImpl implements SecureStorage {
+ final FlutterSecureStorage _secureStorage;
+
+ /// Constructor - creates a single instance with default FlutterSecureStorage
+ SecureStorageImpl() : _secureStorage = const FlutterSecureStorage();
+
+ /// Testing constructor
+ @visibleForTesting
+ SecureStorageImpl.forTesting({required FlutterSecureStorage instance})
+ : _secureStorage = instance;
+
+ @override
+ Future write(String key, String value) async {
+ await _secureStorage.write(key: key, value: value);
+ }
+
+ @override
+ Future read(String key) async {
+ return await _secureStorage.read(key: key);
+ }
+
+ @override
+ Future containsKey(String key) async {
+ return await _secureStorage.containsKey(key: key);
+ }
+
+ @override
+ Future delete(String key) async {
+ await _secureStorage.delete(key: key);
+ }
+
+ @override
+ Future deleteAll() async {
+ await _secureStorage.deleteAll();
+ }
+
+ @override
+ Future> readAll() async {
+ final allData = await _secureStorage.readAll();
+ return allData.keys.toSet();
+ }
+
+ @override
+ Future> readAllKeys() async {
+ final allData = await _secureStorage.readAll();
+ return allData.keys.toSet();
+ }
+
+ @override
+ Future writeInt(String key, int value) async {
+ await _secureStorage.write(key: key, value: value.toString());
+ }
+
+ @override
+ Future readInt(String key) async {
+ final value = await _secureStorage.read(key: key);
+ if (value == null) return null;
+ return int.tryParse(value);
+ }
+
+ @override
+ Future writeBool(String key, bool value) async {
+ await _secureStorage.write(key: key, value: value.toString());
+ }
+
+ @override
+ Future readBool(String key) async {
+ final value = await _secureStorage.read(key: key);
+ if (value == null) return null;
+ if (value.toLowerCase() == 'true') return true;
+ if (value.toLowerCase() == 'false') return false;
+ return null; // Invalid boolean value
+ }
+
+ @override
+ Future writeDouble(String key, double value) async {
+ await _secureStorage.write(key: key, value: value.toString());
+ }
+
+ @override
+ Future readDouble(String key) async {
+ final value = await _secureStorage.read(key: key);
+ if (value == null) return null;
+ return double.tryParse(value);
+ }
+}
diff --git a/finlog_app/fluttery/lib/src/system/environment/environment_impl.dart b/finlog_app/fluttery/lib/src/system/environment/environment_impl.dart
new file mode 100644
index 0000000..4395af2
--- /dev/null
+++ b/finlog_app/fluttery/lib/src/system/environment/environment_impl.dart
@@ -0,0 +1,42 @@
+import 'dart:io' show Platform;
+import 'package:flutter/foundation.dart'
+ show kDebugMode, kReleaseMode, kProfileMode;
+
+import 'package:fluttery/environment.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+
+class EnvironmentImpl implements Environment {
+ PackageInfo? _packageInfo;
+
+ @override
+ bool get isAndroid => Platform.isAndroid;
+
+ @override
+ bool get isIOS => Platform.isIOS;
+
+ @override
+ Future loadPackageInfo() async {
+ _packageInfo = await PackageInfo.fromPlatform();
+ }
+
+ @override
+ String get appName => _packageInfo?.appName ?? 'Unknown';
+
+ @override
+ String get packageName => _packageInfo?.packageName ?? 'Unknown';
+
+ @override
+ String get version => _packageInfo?.version ?? '0.0.0';
+
+ @override
+ String get buildNumber => _packageInfo?.buildNumber ?? '0';
+
+ @override
+ bool get isDebug => kDebugMode;
+
+ @override
+ bool get isRelease => kReleaseMode;
+
+ @override
+ bool get isProfile => kProfileMode;
+}
diff --git a/finlog_app/fluttery/lib/src/system/worker/worker_impl.dart b/finlog_app/fluttery/lib/src/system/worker/worker_impl.dart
new file mode 100644
index 0000000..6807458
--- /dev/null
+++ b/finlog_app/fluttery/lib/src/system/worker/worker_impl.dart
@@ -0,0 +1,189 @@
+import 'dart:async';
+import 'dart:isolate';
+import 'package:flutter/services.dart'
+ show ServicesBinding, RootIsolateToken, BackgroundIsolateBinaryMessenger;
+import 'package:fluttery/fluttery.dart';
+import 'package:fluttery/logger.dart';
+import 'package:fluttery/worker.dart';
+
+class WorkerImpl implements Worker {
+ final Logger _logger;
+
+ WorkerImpl({
+ this.defaultTimeout,
+ this.maxHistory = 100,
+ RootIsolateToken? rootToken,
+ }) : _rootToken = rootToken ?? ServicesBinding.rootIsolateToken,
+ _logger = App.service();
+
+ final Duration? defaultTimeout;
+ final int maxHistory;
+
+ // Captured from the root isolate (may be null in some test envs)
+ final RootIsolateToken? _rootToken;
+
+ final Map _active = {};
+ final List _history = [];
+
+ @override
+ Future spawn(
+ String debugName,
+ FutureOr Function() task, {
+ void Function()? preTask,
+ Duration? timeout,
+ }) {
+ final id = _generateWorkerId();
+ final started = DateTime.now();
+
+ _logger.debug('Spawning worker "$debugName" ($id)');
+ _registerActiveWorker(id, debugName, started);
+
+ final future = _executeWithTimeout(
+ id,
+ debugName,
+ task,
+ preTask,
+ timeout ?? defaultTimeout,
+ );
+
+ _attachCompletionHandlers(id, debugName, future);
+
+ return future;
+ }
+
+ String _generateWorkerId() {
+ return 'iso-${DateTime.now().millisecondsSinceEpoch}';
+ }
+
+ void _registerActiveWorker(String id, String debugName, DateTime started) {
+ _active[id] = WorkerInfo(
+ id: id,
+ name: debugName,
+ startedAt: started,
+ status: WorkerStatus.running,
+ );
+ _logger.debug('Registered worker "$debugName" ($id)');
+ }
+
+ Future _executeWithTimeout(
+ String id,
+ String debugName,
+ FutureOr Function() task,
+ void Function()? preTask,
+ Duration? timeout,
+ ) {
+ _logger.debug(
+ 'Executing worker "$debugName" ($id) with timeout: ${timeout?.inSeconds ?? "none"} seconds',
+ );
+ final future = _executeInIsolate(debugName, task, preTask);
+
+ return timeout == null ? future : future.timeout(timeout);
+ }
+
+ Future _executeInIsolate(
+ String debugName,
+ FutureOr Function() task,
+ void Function()? preTask,
+ ) {
+ final token = _rootToken; // captured into closure
+ _logger.debug('Starting isolate for worker "$debugName"');
+
+ return Isolate.run(() async {
+ // Initialize platform channels for this background isolate.
+ if (token != null) {
+ BackgroundIsolateBinaryMessenger.ensureInitialized(token);
+ }
+ // Now it's safe to touch plugins (e.g., SharedPreferences).
+ App.registerDefaultServices();
+ if (preTask != null) {
+ _logger.debug('Executing pre-task for worker "$debugName"');
+ preTask();
+ }
+ return await Future.sync(task);
+ }, debugName: debugName);
+ }
+
+ void _attachCompletionHandlers(
+ String id,
+ String debugName,
+ Future future,
+ ) {
+ future
+ .then((_) {
+ _logger.debug('Worker "$debugName" ($id) completed successfully');
+ _finish(id, status: WorkerStatus.completed);
+ })
+ .catchError((e, st) {
+ final status = e is TimeoutException
+ ? WorkerStatus.timedOut
+ : WorkerStatus.failed;
+
+ _finish(id, status: status, error: e, stack: st);
+ _logWorkerError(debugName, id, e, st);
+ });
+ }
+
+ void _logWorkerError(
+ String debugName,
+ String id,
+ Object error,
+ StackTrace stackTrace,
+ ) {
+ // Best-effort logging
+ try {
+ App.service().error(
+ 'Worker job "$debugName" ($id) failed: $error',
+ stackTrace,
+ );
+ } catch (_) {}
+ }
+
+ void _finish(
+ String id, {
+ required WorkerStatus status,
+ Object? error,
+ StackTrace? stack,
+ }) {
+ final prev = _active.remove(id);
+ final endedAt = DateTime.now();
+ final info = WorkerInfo(
+ id: prev?.id ?? id,
+ name: prev?.name ?? 'unknown',
+ startedAt: prev?.startedAt ?? endedAt,
+ status: status,
+ endedAt: endedAt,
+ error: error,
+ stackTrace: stack,
+ );
+ _history.insert(0, info);
+ if (_history.length > maxHistory) {
+ _history.removeRange(maxHistory, _history.length);
+ }
+ _logger.debug('Worker "${prev?.name}" ($id) finished with status: $status');
+ }
+
+ @override
+ List getActiveWorkers() =>
+ _active.values.toList()
+ ..sort((a, b) => a.startedAt.compareTo(b.startedAt));
+
+ @override
+ List getAllWorkers() => [...getActiveWorkers(), ..._history];
+
+ @override
+ WorkerInfo? getWorker(String id) {
+ final active = _active[id];
+ if (active != null) return active;
+ for (final w in _history) {
+ if (w.id == id) return w;
+ }
+ return null;
+ }
+
+ @override
+ void purge({Duration maxAge = const Duration(minutes: 30)}) {
+ final cutoff = DateTime.now().subtract(maxAge);
+ _logger.debug('Purging workers older than $maxAge');
+ _history.removeWhere((w) => (w.endedAt ?? w.startedAt).isBefore(cutoff));
+ }
+}
diff --git a/finlog_app/fluttery/lib/worker.dart b/finlog_app/fluttery/lib/worker.dart
new file mode 100644
index 0000000..afa2fa5
--- /dev/null
+++ b/finlog_app/fluttery/lib/worker.dart
@@ -0,0 +1,47 @@
+import 'dart:async';
+import 'package:fluttery/fluttery.dart';
+
+abstract class Worker extends Service {
+ Future spawn(
+ String debugName,
+ FutureOr Function() task, {
+ void Function()? preTask,
+ Duration? timeout, // per-job override
+ });
+
+ /// Currently running jobs.
+ List getActiveWorkers();
+
+ /// All known jobs (active + completed + failed), up to a capped history.
+ List getAllWorkers();
+
+ /// Optional: get a single worker by id.
+ WorkerInfo? getWorker(String id);
+
+ /// Remove completed/failed jobs older than [maxAge] from history.
+ void purge({Duration maxAge = const Duration(minutes: 30)});
+}
+
+enum WorkerStatus { running, completed, failed, timedOut }
+
+class WorkerInfo {
+ WorkerInfo({
+ required this.id,
+ required this.name,
+ required this.startedAt,
+ required this.status,
+ this.endedAt,
+ this.error,
+ this.stackTrace,
+ });
+
+ final String id;
+ final String name;
+ final DateTime startedAt;
+ final WorkerStatus status;
+ final DateTime? endedAt;
+ final Object? error;
+ final StackTrace? stackTrace;
+
+ Duration get duration => ((endedAt ?? DateTime.now()).difference(startedAt));
+}
diff --git a/finlog_app/fluttery/pubspec.yaml b/finlog_app/fluttery/pubspec.yaml
index 6c6bdb1..5799668 100644
--- a/finlog_app/fluttery/pubspec.yaml
+++ b/finlog_app/fluttery/pubspec.yaml
@@ -11,6 +11,12 @@ environment:
dependencies:
flutter:
sdk: flutter
+ flutter_secure_storage: ^9.2.4
+ kiwi: ^5.0.1
+ logging: ^1.3.0
+ mocktail: ^1.0.4
+ package_info_plus: ^9.0.0
+ shared_preferences: ^2.5.3
dev_dependencies:
flutter_test:
diff --git a/finlog_app/fluttery/test/fluttery_test.dart b/finlog_app/fluttery/test/fluttery_test.dart
index be69fe2..af878ab 100644
--- a/finlog_app/fluttery/test/fluttery_test.dart
+++ b/finlog_app/fluttery/test/fluttery_test.dart
@@ -1,12 +1,27 @@
-import 'package:flutter_test/flutter_test.dart';
-
import 'package:fluttery/fluttery.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fluttery/logger.dart';
+import 'package:kiwi/kiwi.dart';
+
+import 'mocks/mocks.dart';
void main() {
- test('adds one to input values', () {
- final calculator = Calculator();
- expect(calculator.addOne(2), 3);
- expect(calculator.addOne(-7), -6);
- expect(calculator.addOne(0), 1);
+ group('App Service Tests', () {
+ // Clear the singleton state before each test to ensure isolation
+ setUp(() {
+ // KiwiContainer provides a clear method to remove all registered services
+ KiwiContainer().clear();
+ });
+
+ test('should register and resolve a custom service', () {
+ // Register a mock logger service
+ final mockLogger = MockLogger();
+ App.registerService(() => mockLogger);
+
+ // Resolve the service and check if it's the same instance
+ final resolvedLogger = App.service();
+ expect(resolvedLogger, isA());
+ expect(resolvedLogger, same(mockLogger));
+ });
});
}
diff --git a/finlog_app/fluttery/test/logger/logger_test.dart b/finlog_app/fluttery/test/logger/logger_test.dart
new file mode 100644
index 0000000..60ac934
--- /dev/null
+++ b/finlog_app/fluttery/test/logger/logger_test.dart
@@ -0,0 +1,69 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:logging/logging.dart' as lib;
+import 'package:fluttery/src/logger/logger_impl.dart';
+
+// Mock class for the lib.Logger
+class MockLibLogger extends Mock implements lib.Logger {}
+
+void main() {
+ final mockLibLogger = MockLibLogger();
+
+ group('LoggerImpl', () {
+ late LoggerImpl loggerImpl;
+
+ setUp(() {
+ loggerImpl = LoggerImpl.forTest(mockLibLogger);
+ });
+
+ test('info method logs an info message', () {
+ loggerImpl.info('Info message');
+
+ // Verify that the info method was called on the mock logger with the correct message
+ verify(() => mockLibLogger.info('Info message')).called(1);
+ });
+
+ test('warning method logs a warning message', () {
+ loggerImpl.warning('Warning message');
+
+ // Verify that the warning method was called on the mock logger with the correct message
+ verify(() => mockLibLogger.warning('Warning message')).called(1);
+ });
+
+ test('error method logs an error message with optional parameters', () {
+ final exception = Exception('Test exception');
+ final stackTrace = StackTrace.current;
+
+ loggerImpl.error('Error message', exception, stackTrace);
+
+ // Verify that the severe method was called on the mock logger with the correct parameters
+ verify(
+ () => mockLibLogger.severe('Error message', exception, stackTrace),
+ ).called(1);
+ });
+
+ test('debug method logs a debug message', () {
+ loggerImpl.debug('Debug message');
+
+ // Verify that the fine method was called on the mock logger with the correct message
+ verify(() => mockLibLogger.fine('Debug message')).called(1);
+ });
+
+ test('setLogLevel method sets the logger level', () {
+ // This is to capture the change in the logger level
+ var capturedLevel = lib.Level.INFO;
+
+ // Capture the setter call
+ when(() => mockLibLogger.level = any()).thenAnswer((invocation) {
+ capturedLevel = invocation.positionalArguments.first as lib.Level;
+ return capturedLevel;
+ });
+
+ // Set the log level
+ loggerImpl.setLogLevel(lib.Level.WARNING);
+
+ // Verify that the logger level is set to the expected level
+ expect(capturedLevel, lib.Level.WARNING);
+ });
+ });
+}
diff --git a/finlog_app/fluttery/test/mocks/mocks.dart b/finlog_app/fluttery/test/mocks/mocks.dart
new file mode 100644
index 0000000..82c20fa
--- /dev/null
+++ b/finlog_app/fluttery/test/mocks/mocks.dart
@@ -0,0 +1,25 @@
+import 'package:fluttery/logger.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockLogger extends Mock implements Logger {}
+
+class MockUtils {
+ static Logger mockLogger() {
+ final logger = MockLogger();
+
+ when(() => logger.debug(any())).thenAnswer((a) {
+ print("[DEBUG] ${a.positionalArguments[0]}");
+ });
+ when(() => logger.info(any())).thenAnswer((a) {
+ print("[INFO] ${a.positionalArguments[0]}");
+ });
+ when(() => logger.warning(any())).thenAnswer((a) {
+ print("[WARN] ${a.positionalArguments[0]}");
+ });
+ when(() => logger.error(any(), any(), any())).thenAnswer((a) {
+ print("[ERROR] ${a.positionalArguments[0]}\n${a.positionalArguments[2]}");
+ });
+
+ return logger;
+ }
+}
diff --git a/finlog_app/fluttery/test/preferences/preferences_test.dart b/finlog_app/fluttery/test/preferences/preferences_test.dart
new file mode 100644
index 0000000..49fa28c
--- /dev/null
+++ b/finlog_app/fluttery/test/preferences/preferences_test.dart
@@ -0,0 +1,176 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fluttery/src/preferences/preferences_impl.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+void main() {
+ late PreferencesImpl preferences;
+
+ setUp(() async {
+ // Clear any existing data and set up a fresh in-memory instance
+ SharedPreferences.setMockInitialValues({});
+
+ // Create preferences instance that will use the real SharedPreferences
+ // but with in-memory storage for testing
+ preferences = PreferencesImpl();
+
+ // Give time for initialization
+ await Future.delayed(Duration.zero);
+ });
+
+ group('PreferencesImpl Tests', () {
+ test('setString and getString work with real implementation', () async {
+ const key = 'testKey';
+ const value = 'testValue';
+
+ await preferences.setString(key, value);
+ final result = await preferences.getString(key);
+
+ expect(result, value);
+ });
+
+ test('setInt and getInt work with real implementation', () async {
+ const key = 'testKey';
+ const value = 42;
+
+ await preferences.setInt(key, value);
+ final result = await preferences.getInt(key);
+
+ expect(result, value);
+ });
+
+ test('setBool and getBool work with real implementation', () async {
+ const key = 'testKey';
+ const value = true;
+
+ await preferences.setBool(key, value);
+ final result = await preferences.getBool(key);
+
+ expect(result, value);
+ });
+
+ test('setDouble and getDouble work with real implementation', () async {
+ const key = 'testKey';
+ const value = 3.14;
+
+ await preferences.setDouble(key, value);
+ final result = await preferences.getDouble(key);
+
+ expect(result, value);
+ });
+
+ test(
+ 'setStringList and getStringList work with real implementation',
+ () async {
+ const key = 'testKey';
+ const value = ['one', 'two', 'three'];
+
+ await preferences.setStringList(key, value);
+ final result = await preferences.getStringList(key);
+
+ expect(result, value);
+ },
+ );
+
+ test('remove deletes key-value pair', () async {
+ const key = 'testKey';
+ const value = 'testValue';
+
+ // Set a value first
+ await preferences.setString(key, value);
+ expect(await preferences.getString(key), value);
+
+ // Remove it
+ await preferences.remove(key);
+ final result = await preferences.getString(key);
+
+ expect(result, isNull);
+ });
+
+ test('clear removes all data', () async {
+ // Set multiple values
+ await preferences.setString('key1', 'value1');
+ await preferences.setInt('key2', 42);
+ await preferences.setBool('key3', true);
+
+ // Verify they exist
+ expect(await preferences.getString('key1'), 'value1');
+ expect(await preferences.getInt('key2'), 42);
+ expect(await preferences.getBool('key3'), true);
+
+ // Clear all
+ await preferences.clear();
+
+ // Verify they're gone
+ expect(await preferences.getString('key1'), isNull);
+ expect(await preferences.getInt('key2'), isNull);
+ expect(await preferences.getBool('key3'), isNull);
+ });
+
+ test('getting non-existent keys returns null', () async {
+ expect(await preferences.getString('nonExistent'), isNull);
+ expect(await preferences.getInt('nonExistent'), isNull);
+ expect(await preferences.getBool('nonExistent'), isNull);
+ expect(await preferences.getDouble('nonExistent'), isNull);
+ expect(await preferences.getStringList('nonExistent'), isNull);
+ });
+
+ test('can overwrite existing values of the same type', () async {
+ const key = 'testKey';
+ const initialValue = 'initialValue';
+ const newValue = 'newValue';
+
+ await preferences.setString(key, initialValue);
+ expect(await preferences.getString(key), initialValue);
+
+ await preferences.setString(key, newValue);
+ expect(await preferences.getString(key), newValue);
+ });
+
+ test(
+ 'different keys can store different data types simultaneously',
+ () async {
+ await preferences.setString('stringKey', 'value');
+ await preferences.setInt('intKey', 42);
+ await preferences.setBool('boolKey', true);
+ await preferences.setDouble('doubleKey', 3.14);
+ await preferences.setStringList('listKey', ['a', 'b', 'c']);
+
+ expect(await preferences.getString('stringKey'), 'value');
+ expect(await preferences.getInt('intKey'), 42);
+ expect(await preferences.getBool('boolKey'), true);
+ expect(await preferences.getDouble('doubleKey'), 3.14);
+ expect(await preferences.getStringList('listKey'), ['a', 'b', 'c']);
+ },
+ );
+
+ test('values can be overwritten with different data types', () async {
+ const key = 'testKey';
+
+ // Store a string value
+ await preferences.setString(key, 'stringValue');
+ expect(await preferences.getString(key), 'stringValue');
+
+ // Overwrite with an int - this replaces the string value
+ await preferences.setInt(key, 42);
+ expect(await preferences.getInt(key), 42);
+
+ // Overwrite with a bool - this replaces the int value
+ await preferences.setBool(key, true);
+ expect(await preferences.getBool(key), true);
+ });
+
+ test('persistence works across multiple operations', () async {
+ // Test that values persist through multiple set/get operations
+ await preferences.setString('key1', 'value1');
+ await preferences.setInt('key2', 100);
+
+ expect(await preferences.getString('key1'), 'value1');
+ expect(await preferences.getInt('key2'), 100);
+
+ // Modify one value and ensure the other remains
+ await preferences.setString('key1', 'newValue1');
+ expect(await preferences.getString('key1'), 'newValue1');
+ expect(await preferences.getInt('key2'), 100);
+ });
+ });
+}
diff --git a/finlog_app/fluttery/test/storage/secure/secure_storage_test.dart b/finlog_app/fluttery/test/storage/secure/secure_storage_test.dart
new file mode 100644
index 0000000..553046d
--- /dev/null
+++ b/finlog_app/fluttery/test/storage/secure/secure_storage_test.dart
@@ -0,0 +1,352 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:fluttery/src/storage/secure/secure_storage_impl.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockFlutterSecureStorage extends Mock implements FlutterSecureStorage {}
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ late SecureStorageImpl secureStorage;
+ late MockFlutterSecureStorage mockStorage;
+
+ // Single instance of SecureStorageImpl used throughout all tests
+ setUpAll(() {
+ mockStorage = MockFlutterSecureStorage();
+ secureStorage = SecureStorageImpl.forTesting(instance: mockStorage);
+ });
+
+ setUp(() {
+ // Reset mock between tests
+ reset(mockStorage);
+ });
+
+ group('SecureStorageImpl String Tests', () {
+ test('write and read string work', () async {
+ const key = 'testStringKey';
+ const value = 'testStringValue';
+
+ when(
+ () => mockStorage.write(key: key, value: value),
+ ).thenAnswer((_) async {});
+ when(() => mockStorage.read(key: key)).thenAnswer((_) async => value);
+
+ await secureStorage.write(key, value);
+ final result = await secureStorage.read(key);
+
+ expect(result, value);
+ verify(() => mockStorage.write(key: key, value: value)).called(1);
+ verify(() => mockStorage.read(key: key)).called(1);
+ });
+
+ test('read returns null for non-existent string key', () async {
+ const key = 'nonExistentKey';
+
+ when(() => mockStorage.read(key: key)).thenAnswer((_) async => null);
+
+ final result = await secureStorage.read(key);
+
+ expect(result, isNull);
+ verify(() => mockStorage.read(key: key)).called(1);
+ });
+
+ test('can overwrite existing string values', () async {
+ const key = 'overwriteKey';
+ const initialValue = 'initialValue';
+ const newValue = 'newValue';
+
+ when(
+ () => mockStorage.write(key: key, value: initialValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.write(key: key, value: newValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => initialValue);
+
+ await secureStorage.write(key, initialValue);
+ final firstResult = await secureStorage.read(key);
+ expect(firstResult, initialValue);
+
+ when(() => mockStorage.read(key: key)).thenAnswer((_) async => newValue);
+
+ await secureStorage.write(key, newValue);
+ final secondResult = await secureStorage.read(key);
+ expect(secondResult, newValue);
+ });
+ });
+
+ group('SecureStorageImpl Integer Tests', () {
+ test('writeInt and readInt work', () async {
+ const key = 'testIntKey';
+ const value = 42;
+ const stringValue = '42';
+
+ when(
+ () => mockStorage.write(key: key, value: stringValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => stringValue);
+
+ await secureStorage.writeInt(key, value);
+ final result = await secureStorage.readInt(key);
+
+ expect(result, value);
+ verify(() => mockStorage.write(key: key, value: stringValue)).called(1);
+ verify(() => mockStorage.read(key: key)).called(1);
+ });
+
+ test('readInt returns null for non-existent key', () async {
+ const key = 'nonExistentIntKey';
+
+ when(() => mockStorage.read(key: key)).thenAnswer((_) async => null);
+
+ final result = await secureStorage.readInt(key);
+
+ expect(result, isNull);
+ verify(() => mockStorage.read(key: key)).called(1);
+ });
+
+ test('readInt handles negative numbers', () async {
+ const key = 'negativeIntKey';
+ const value = -123;
+ const stringValue = '-123';
+
+ when(
+ () => mockStorage.write(key: key, value: stringValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => stringValue);
+
+ await secureStorage.writeInt(key, value);
+ final result = await secureStorage.readInt(key);
+
+ expect(result, value);
+ });
+
+ test('readInt returns null for invalid integer string', () async {
+ const key = 'invalidIntKey';
+ const invalidValue = 'not_a_number';
+
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => invalidValue);
+
+ final result = await secureStorage.readInt(key);
+
+ expect(result, isNull);
+ });
+ });
+
+ group('SecureStorageImpl Boolean Tests', () {
+ test('writeBool and readBool work with true', () async {
+ const key = 'testBoolTrueKey';
+ const value = true;
+ const stringValue = 'true';
+
+ when(
+ () => mockStorage.write(key: key, value: stringValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => stringValue);
+
+ await secureStorage.writeBool(key, value);
+ final result = await secureStorage.readBool(key);
+
+ expect(result, value);
+ });
+
+ test('writeBool and readBool work with false', () async {
+ const key = 'testBoolFalseKey';
+ const value = false;
+ const stringValue = 'false';
+
+ when(
+ () => mockStorage.write(key: key, value: stringValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => stringValue);
+
+ await secureStorage.writeBool(key, value);
+ final result = await secureStorage.readBool(key);
+
+ expect(result, value);
+ });
+
+ test('readBool handles case insensitive true/false', () async {
+ const key = 'caseInsensitiveKey';
+
+ // Test "TRUE"
+ when(() => mockStorage.read(key: key)).thenAnswer((_) async => 'TRUE');
+ expect(await secureStorage.readBool(key), true);
+
+ // Test "False"
+ when(() => mockStorage.read(key: key)).thenAnswer((_) async => 'False');
+ expect(await secureStorage.readBool(key), false);
+ });
+
+ test('readBool returns null for invalid boolean string', () async {
+ const key = 'invalidBoolKey';
+ const invalidValue = 'maybe';
+
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => invalidValue);
+
+ final result = await secureStorage.readBool(key);
+
+ expect(result, isNull);
+ });
+ });
+
+ group('SecureStorageImpl Double Tests', () {
+ test('writeDouble and readDouble work', () async {
+ const key = 'testDoubleKey';
+ const value = 3.14159;
+ const stringValue = '3.14159';
+
+ when(
+ () => mockStorage.write(key: key, value: stringValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => stringValue);
+
+ await secureStorage.writeDouble(key, value);
+ final result = await secureStorage.readDouble(key);
+
+ expect(result, value);
+ });
+
+ test('readDouble returns null for invalid double string', () async {
+ const key = 'invalidDoubleKey';
+ const invalidValue = 'not_a_double';
+
+ when(
+ () => mockStorage.read(key: key),
+ ).thenAnswer((_) async => invalidValue);
+
+ final result = await secureStorage.readDouble(key);
+
+ expect(result, isNull);
+ });
+ });
+
+ group('SecureStorageImpl Utility Tests', () {
+ test('containsKey returns true for existing key', () async {
+ const key = 'existingKey';
+
+ when(
+ () => mockStorage.containsKey(key: key),
+ ).thenAnswer((_) async => true);
+
+ final exists = await secureStorage.containsKey(key);
+
+ expect(exists, isTrue);
+ verify(() => mockStorage.containsKey(key: key)).called(1);
+ });
+
+ test('containsKey returns false for non-existent key', () async {
+ const key = 'nonExistentKey';
+
+ when(
+ () => mockStorage.containsKey(key: key),
+ ).thenAnswer((_) async => false);
+
+ final exists = await secureStorage.containsKey(key);
+
+ expect(exists, isFalse);
+ });
+
+ test('delete removes key-value pair', () async {
+ const key = 'deleteTestKey';
+
+ when(() => mockStorage.delete(key: key)).thenAnswer((_) async {});
+
+ await secureStorage.delete(key);
+
+ verify(() => mockStorage.delete(key: key)).called(1);
+ });
+
+ test('readAll and readAllKeys return all stored keys', () async {
+ final testData = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'};
+
+ when(() => mockStorage.readAll()).thenAnswer((_) async => testData);
+
+ final allKeys = await secureStorage.readAll();
+ final allKeysAlt = await secureStorage.readAllKeys();
+
+ expect(allKeys, containsAll(testData.keys));
+ expect(allKeys.length, testData.length);
+ expect(allKeysAlt, equals(allKeys));
+ });
+
+ test('deleteAll removes all key-value pairs', () async {
+ when(() => mockStorage.deleteAll()).thenAnswer((_) async {});
+
+ await secureStorage.deleteAll();
+
+ verify(() => mockStorage.deleteAll()).called(1);
+ });
+ });
+
+ group('SecureStorageImpl Mixed Data Type Tests', () {
+ test('different data types work correctly', () async {
+ const stringKey = 'stringKey';
+ const intKey = 'intKey';
+ const boolKey = 'boolKey';
+ const doubleKey = 'doubleKey';
+
+ const stringValue = 'hello world';
+ const intValue = 123;
+ const boolValue = true;
+ const doubleValue = 45.67;
+
+ // Mock all write operations
+ when(
+ () => mockStorage.write(key: stringKey, value: stringValue),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.write(key: intKey, value: intValue.toString()),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.write(key: boolKey, value: boolValue.toString()),
+ ).thenAnswer((_) async {});
+ when(
+ () => mockStorage.write(key: doubleKey, value: doubleValue.toString()),
+ ).thenAnswer((_) async {});
+
+ // Mock all read operations
+ when(
+ () => mockStorage.read(key: stringKey),
+ ).thenAnswer((_) async => stringValue);
+ when(
+ () => mockStorage.read(key: intKey),
+ ).thenAnswer((_) async => intValue.toString());
+ when(
+ () => mockStorage.read(key: boolKey),
+ ).thenAnswer((_) async => boolValue.toString());
+ when(
+ () => mockStorage.read(key: doubleKey),
+ ).thenAnswer((_) async => doubleValue.toString());
+
+ // Store different types
+ await secureStorage.write(stringKey, stringValue);
+ await secureStorage.writeInt(intKey, intValue);
+ await secureStorage.writeBool(boolKey, boolValue);
+ await secureStorage.writeDouble(doubleKey, doubleValue);
+
+ // Retrieve and verify all types
+ expect(await secureStorage.read(stringKey), stringValue);
+ expect(await secureStorage.readInt(intKey), intValue);
+ expect(await secureStorage.readBool(boolKey), boolValue);
+ expect(await secureStorage.readDouble(doubleKey), doubleValue);
+ });
+ });
+}
diff --git a/finlog_app/fluttery/test/system/environment/environment_test.dart b/finlog_app/fluttery/test/system/environment/environment_test.dart
new file mode 100644
index 0000000..142351a
--- /dev/null
+++ b/finlog_app/fluttery/test/system/environment/environment_test.dart
@@ -0,0 +1,54 @@
+import 'dart:io' show Platform;
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fluttery/src/system/environment/environment_impl.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+
+void main() {
+ setUp(() {
+ PackageInfo.setMockInitialValues(
+ appName: 'Test App',
+ packageName: 'com.example.testapp',
+ version: '1.2.3',
+ buildNumber: '42',
+ buildSignature: 'mock-signature',
+ installerStore: 'mock-store',
+ );
+ });
+
+ group('EnvironmentImpl', () {
+ test('defaults before loadPackageInfo()', () {
+ final env = EnvironmentImpl();
+
+ expect(env.appName, equals('Unknown'));
+ expect(env.packageName, equals('Unknown'));
+ expect(env.version, equals('0.0.0'));
+ expect(env.buildNumber, equals('0'));
+ });
+
+ test('loadPackageInfo() populates fields from PackageInfo', () async {
+ final env = EnvironmentImpl();
+ await env.loadPackageInfo();
+
+ expect(env.appName, equals('Test App'));
+ expect(env.packageName, equals('com.example.testapp'));
+ expect(env.version, equals('1.2.3'));
+ expect(env.buildNumber, equals('42'));
+ });
+
+ test('platform flags mirror dart:io Platform', () {
+ final env = EnvironmentImpl();
+
+ expect(env.isAndroid, equals(Platform.isAndroid));
+ expect(env.isIOS, equals(Platform.isIOS));
+ });
+
+ test('build mode flags reflect Flutter constants', () {
+ final env = EnvironmentImpl();
+
+ // In unit tests, this should typically be: debug=true, release/profile=false.
+ expect(env.isDebug, isTrue);
+ expect(env.isRelease, isFalse);
+ expect(env.isProfile, isFalse);
+ });
+ });
+}
diff --git a/finlog_app/fluttery/test/system/worker/worker_impl_test.dart b/finlog_app/fluttery/test/system/worker/worker_impl_test.dart
new file mode 100644
index 0000000..87d1719
--- /dev/null
+++ b/finlog_app/fluttery/test/system/worker/worker_impl_test.dart
@@ -0,0 +1,413 @@
+import 'dart:async';
+import 'dart:isolate';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fluttery/fluttery.dart';
+import 'package:fluttery/logger.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:fluttery/src/system/worker/worker_impl.dart';
+import 'package:fluttery/worker.dart';
+
+import '../../mocks/mocks.dart';
+
+Future pumpMicro([int times = 10]) => pumpEventQueue(times: times);
+
+Future waitFor(
+ bool Function() predicate, {
+ Duration timeout = const Duration(seconds: 2),
+ Duration step = const Duration(milliseconds: 20),
+}) async {
+ final deadline = DateTime.now().add(timeout);
+ while (DateTime.now().isBefore(deadline)) {
+ if (predicate()) return;
+ await Future.delayed(step);
+ }
+ fail('Condition not met within $timeout');
+}
+
+void main() {
+ setUpAll(() async {
+ TestWidgetsFlutterBinding.ensureInitialized();
+ SharedPreferences.setMockInitialValues({});
+ expect(ServicesBinding.rootIsolateToken, isNotNull);
+
+ App.registerService(() => MockUtils.mockLogger());
+ });
+
+ group('WorkerImpl', () {
+ late WorkerImpl worker;
+
+ setUp(() {
+ worker = WorkerImpl();
+ SharedPreferences.setMockInitialValues({});
+ });
+
+ test('spawn returns value; preTask runs; active->history tracking', () async {
+ // We'll verify preTask runs by checking the task itself can access
+ // what the preTask sets up (SharedPreferences mock)
+ final future = worker.spawn(
+ 'successful_task',
+ () async {
+ await Future.delayed(const Duration(milliseconds: 20));
+
+ // This would fail if preTask didn't run to set up SharedPreferences mock
+ SharedPreferences.setMockInitialValues({'test': 'verified'});
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString('preTaskRan', 'true');
+
+ return 42;
+ },
+ preTask: () {
+ // Set up the SharedPreferences mock so the task can use it
+ SharedPreferences.setMockInitialValues({});
+ },
+ );
+
+ // Verify worker is registered as active shortly after spawn
+ await Future.delayed(const Duration(milliseconds: 10));
+ expect(worker.getActiveWorkers().length, 1);
+
+ final activeWorkers = worker.getActiveWorkers();
+ expect(activeWorkers.first.name, 'successful_task');
+ expect(activeWorkers.first.status, WorkerStatus.running);
+
+ // Wait for completion
+ final result = await future;
+ expect(result, 42);
+
+ // The fact that the task completed successfully without throwing an exception
+ // when trying to use SharedPreferences proves that preTask ran
+
+ // Wait for the completion handlers to run and move worker to history
+ await waitFor(() => worker.getActiveWorkers().isEmpty);
+
+ // Verify the worker was moved to history with completed status
+ final historyWorkers = worker.getAllWorkers();
+ expect(historyWorkers.length, 1);
+ expect(historyWorkers.first.status, WorkerStatus.completed);
+ expect(historyWorkers.first.name, 'successful_task');
+ expect(historyWorkers.first.endedAt, isNotNull);
+ });
+ test('timeout marks job as timedOut and throws TimeoutException', () async {
+ final timedWorker = WorkerImpl(
+ defaultTimeout: const Duration(milliseconds: 50),
+ );
+
+ await expectLater(
+ timedWorker.spawn(
+ 'timeout_task',
+ () async => Future.delayed(const Duration(milliseconds: 200)),
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ ),
+ throwsA(isA()),
+ );
+
+ // Wait for worker to update history
+ await waitFor(() => timedWorker.getAllWorkers().isNotEmpty);
+
+ final allWorkers = timedWorker.getAllWorkers();
+ expect(allWorkers.first.status, WorkerStatus.timedOut);
+ expect(allWorkers.first.name, 'timeout_task');
+ });
+
+ test('custom timeout overrides default timeout', () async {
+ final timedWorker = WorkerImpl(
+ defaultTimeout: const Duration(milliseconds: 200), // Long default
+ );
+
+ await expectLater(
+ timedWorker.spawn(
+ 'custom_timeout_task',
+ () async => Future.delayed(const Duration(milliseconds: 100)),
+ timeout: const Duration(milliseconds: 50), // Short custom timeout
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ ),
+ throwsA(isA()),
+ );
+
+ await waitFor(() => timedWorker.getAllWorkers().isNotEmpty);
+ expect(timedWorker.getAllWorkers().first.status, WorkerStatus.timedOut);
+ });
+
+ test('failure marks job as failed and surfaces exception', () async {
+ // Create a variable to capture the actual exception
+ Object? caughtException;
+
+ try {
+ await worker.spawn('failing_task', () async {
+ await Future.delayed(const Duration(milliseconds: 10));
+ throw StateError('intentional failure');
+ }, preTask: () => SharedPreferences.setMockInitialValues({}));
+ fail('Expected an exception to be thrown');
+ } catch (e) {
+ caughtException = e;
+ }
+
+ // Verify that an exception was thrown (could be RemoteError or the original StateError)
+ expect(caughtException, isNotNull);
+ expect(
+ caughtException is RemoteError || caughtException is StateError,
+ isTrue,
+ reason:
+ 'Should throw either RemoteError or StateError, got: ${caughtException.runtimeType}',
+ );
+
+ // Wait for worker to update history
+ await waitFor(() => worker.getAllWorkers().isNotEmpty);
+
+ final allWorkers = worker.getAllWorkers();
+ expect(allWorkers.first.status, WorkerStatus.failed);
+ expect(allWorkers.first.name, 'failing_task');
+ expect(allWorkers.first.error, isNotNull);
+ });
+
+ test('getWorker finds active and completed workers by ID', () async {
+ final future = worker.spawn('trackable_task', () async {
+ await Future.delayed(const Duration(milliseconds: 50));
+ return 123;
+ }, preTask: () => SharedPreferences.setMockInitialValues({}));
+
+ // Find worker while active
+ await Future.delayed(const Duration(milliseconds: 10));
+ final activeWorkers = worker.getActiveWorkers();
+ expect(activeWorkers.length, 1);
+
+ final workerId = activeWorkers.first.id;
+ final activeWorker = worker.getWorker(workerId);
+ expect(activeWorker, isNotNull);
+ expect(activeWorker!.status, WorkerStatus.running);
+
+ // Wait for completion
+ await future;
+ await waitFor(() => worker.getActiveWorkers().isEmpty);
+
+ // Find worker in history
+ final completedWorker = worker.getWorker(workerId);
+ expect(completedWorker, isNotNull);
+ expect(completedWorker!.status, WorkerStatus.completed);
+ });
+
+ test('getWorker returns null for non-existent ID', () {
+ expect(worker.getWorker('non-existent'), isNull);
+ expect(worker.getWorker('999999'), isNull);
+ });
+
+ test('worker ID generation uses timestamp format', () async {
+ final futures = [];
+ final Set generatedIds = {};
+
+ // Spawn multiple workers with sufficient delay to ensure unique timestamps
+ for (int i = 0; i < 3; i++) {
+ futures.add(
+ worker.spawn(
+ 'task_$i',
+ () async => Future.delayed(const Duration(milliseconds: 10)),
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ ),
+ );
+ // Ensure sufficient delay for different timestamps
+ await Future.delayed(const Duration(milliseconds: 1));
+ }
+
+ // Wait for all workers to be registered as active
+ await waitFor(() => worker.getActiveWorkers().length == 3);
+
+ final activeWorkers = worker.getActiveWorkers();
+ expect(activeWorkers.length, 3);
+
+ // Verify each ID follows the timestamp format and is unique
+ for (final workerInfo in activeWorkers) {
+ expect(workerInfo.id, startsWith('iso-'));
+ expect(
+ generatedIds.contains(workerInfo.id),
+ isFalse,
+ reason: 'Worker ID should be unique: ${workerInfo.id}',
+ );
+ generatedIds.add(workerInfo.id);
+
+ final timestampPart = workerInfo.id.substring(
+ 4,
+ ); // Remove 'iso-' prefix
+ final timestamp = int.tryParse(timestampPart);
+ expect(
+ timestamp,
+ isNotNull,
+ reason: 'Timestamp part should be a valid integer: $timestampPart',
+ );
+ expect(
+ timestamp,
+ greaterThan(0),
+ reason: 'Timestamp should be positive: $timestamp',
+ );
+
+ // Verify timestamp is reasonable (not too old, not in future)
+ final now = DateTime.now().millisecondsSinceEpoch;
+ expect(
+ timestamp,
+ lessThanOrEqualTo(now),
+ reason: 'Timestamp should not be in the future',
+ );
+ expect(
+ timestamp,
+ greaterThan(now - 10000),
+ reason: 'Timestamp should be recent (within 10 seconds)',
+ );
+ }
+
+ // Verify we generated 3 unique IDs
+ expect(generatedIds.length, 3);
+
+ await Future.wait(futures);
+ });
+
+ test('getAllWorkers combines active and history workers', () async {
+ // Spawn and complete a short task first
+ await worker.spawn(
+ 'short_task',
+ () async => Future.delayed(const Duration(milliseconds: 10)),
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ );
+
+ // Wait for short task to complete and move to history
+ await waitFor(() => worker.getAllWorkers().length == 1);
+ await waitFor(() => worker.getActiveWorkers().isEmpty);
+
+ // Verify we have one completed worker in history
+ expect(worker.getAllWorkers().length, 1);
+ expect(worker.getAllWorkers().first.status, WorkerStatus.completed);
+ expect(worker.getAllWorkers().first.name, 'short_task');
+
+ // Now spawn a long-running task
+ final longTask = worker.spawn(
+ 'long_task',
+ () async => Future.delayed(const Duration(milliseconds: 100)),
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ );
+
+ // Wait briefly for long task to be registered as active
+ await Future.delayed(const Duration(milliseconds: 10));
+
+ // Now we should have 2 workers: 1 active (long_task) and 1 in history (short_task)
+ final allWorkers = worker.getAllWorkers();
+ expect(allWorkers.length, 2);
+
+ final activeCount = allWorkers
+ .where((w) => w.status == WorkerStatus.running)
+ .length;
+ final completedCount = allWorkers
+ .where((w) => w.status == WorkerStatus.completed)
+ .length;
+
+ expect(activeCount, 1);
+ expect(completedCount, 1);
+
+ // Verify the active worker is the long task
+ final activeWorkers = worker.getActiveWorkers();
+ expect(activeWorkers.length, 1);
+ expect(activeWorkers.first.name, 'long_task');
+
+ await longTask;
+ });
+
+ test('purge removes old workers from history', () async {
+ // Complete a task
+ await worker.spawn(
+ 'old_task',
+ () async => Future.delayed(const Duration(milliseconds: 10)),
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ );
+
+ await waitFor(() => worker.getAllWorkers().isNotEmpty);
+ expect(worker.getAllWorkers().length, 1);
+
+ // Purge with zero max age (removes everything)
+ worker.purge(maxAge: Duration.zero);
+ await pumpMicro();
+
+ expect(worker.getAllWorkers(), isEmpty);
+ });
+
+ test('maxHistory limit is enforced', () async {
+ final limitedWorker = WorkerImpl(maxHistory: 2);
+
+ // Complete 3 tasks
+ for (int i = 0; i < 3; i++) {
+ await limitedWorker.spawn(
+ 'task_$i',
+ () async => Future.delayed(const Duration(milliseconds: 5)),
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ );
+ await waitFor(() => limitedWorker.getActiveWorkers().isEmpty);
+ }
+
+ // Should only keep the last 2 in history
+ final allWorkers = limitedWorker.getAllWorkers();
+ expect(allWorkers.length, 2);
+
+ // Should be the most recent tasks (task_1 and task_2)
+ final names = allWorkers.map((w) => w.name).toSet();
+ expect(names.contains('task_1'), isTrue);
+ expect(names.contains('task_2'), isTrue);
+ expect(names.contains('task_0'), isFalse);
+ });
+
+ test(
+ 'no timeout when defaultTimeout is null and timeout is null',
+ () async {
+ final noTimeoutWorker = WorkerImpl(defaultTimeout: null);
+
+ final result = await noTimeoutWorker.spawn(
+ 'no_timeout_task',
+ () async {
+ await Future.delayed(const Duration(milliseconds: 50));
+ return 999;
+ },
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ );
+
+ expect(result, 999);
+
+ await waitFor(() => noTimeoutWorker.getAllWorkers().isNotEmpty);
+ expect(
+ noTimeoutWorker.getAllWorkers().first.status,
+ WorkerStatus.completed,
+ );
+ },
+ );
+
+ test('active workers are sorted by start time', () async {
+ final futures = [];
+
+ // Spawn workers with small delays between them
+ for (int i = 0; i < 3; i++) {
+ futures.add(
+ worker.spawn(
+ 'timed_task_$i',
+ () async => Future.delayed(const Duration(milliseconds: 100)),
+ preTask: () => SharedPreferences.setMockInitialValues({}),
+ ),
+ );
+ await Future.delayed(const Duration(milliseconds: 10));
+ }
+
+ final activeWorkers = worker.getActiveWorkers();
+ expect(activeWorkers.length, 3);
+
+ // Verify they are sorted by start time (earliest first)
+ for (int i = 1; i < activeWorkers.length; i++) {
+ expect(
+ activeWorkers[i - 1].startedAt.isBefore(activeWorkers[i].startedAt) ||
+ activeWorkers[i - 1].startedAt.isAtSameMomentAs(
+ activeWorkers[i].startedAt,
+ ),
+ isTrue,
+ reason: 'Workers should be sorted by start time',
+ );
+ }
+
+ await Future.wait(futures);
+ });
+ });
+}