From 9122d0fc1cddebbcf9aedb725c2eb573b0e4fcdc Mon Sep 17 00:00:00 2001 From: David Garcia Ruiz Date: Thu, 17 Jul 2025 12:48:55 +0200 Subject: [PATCH 1/6] feat(exceptions): introduce structured exception handling for policy SDK with detailed context --- .../exceptions/i_policy_sdk_exceptions.dart | 51 ++++++++ lib/src/exceptions/json_parse_exception.dart | 53 ++++++++ .../exceptions/json_serialize_exception.dart | 52 ++++++++ .../policy_not_initialized_exception.dart | 30 +++++ lib/src/exceptions/policy_sdk_exceptions.dart | 114 ------------------ lib/src/utils/json_handler.dart | 3 +- test/utils/json_handler_test.dart | 3 +- 7 files changed, 190 insertions(+), 116 deletions(-) create mode 100644 lib/src/exceptions/i_policy_sdk_exceptions.dart create mode 100644 lib/src/exceptions/json_parse_exception.dart create mode 100644 lib/src/exceptions/json_serialize_exception.dart create mode 100644 lib/src/exceptions/policy_not_initialized_exception.dart delete mode 100644 lib/src/exceptions/policy_sdk_exceptions.dart diff --git a/lib/src/exceptions/i_policy_sdk_exceptions.dart b/lib/src/exceptions/i_policy_sdk_exceptions.dart new file mode 100644 index 0000000..6ebb544 --- /dev/null +++ b/lib/src/exceptions/i_policy_sdk_exceptions.dart @@ -0,0 +1,51 @@ +/// Base exception class for all policy SDK related errors. +/// +/// This abstract class provides a common interface for all exceptions +/// thrown by the policy engine, ensuring consistent error handling +/// and messaging across the SDK. +abstract class IPolicySDKException implements Exception { + /// Creates a new PolicySDKException with the given error message. + /// + /// [message] should provide a clear description of what went wrong. + const IPolicySDKException(this.message); + + /// The error message describing the exception. + final String message; + + @override + String toString() => 'PolicySDKException: $message'; +} + +/// Abstract exception for detailed policy SDK errors with contextual information. +/// +/// This class extends [IPolicySDKException] to provide additional context for +/// errors that occur within the policy engine, such as the specific key involved, +/// the original error thrown, and a map of field-specific validation errors. +/// Subclasses should use this to represent exceptions where more granular +/// diagnostic information is valuable for debugging or reporting. +abstract class IDetailPolicySDKException implements IPolicySDKException { + /// Creates a new [IDetailPolicySDKException] with an error [message] and optional details. + /// + /// [message] provides a human-readable description of the error. + /// [key] optionally identifies the specific key or field related to the error. + /// [originalError] optionally contains the original error object that triggered this exception. + /// [errors] optionally provides a map of field-specific validation errors. + const IDetailPolicySDKException( + this.message, { + this.key, + this.originalError, + this.errors, + }); + + @override + final String message; + + /// The specific key or field that caused the error, if applicable. + final String? key; + + /// The original error object that led to this exception, if available. + final Object? originalError; + + /// A map of field-specific validation errors encountered during processing, if any. + final Map? errors; +} diff --git a/lib/src/exceptions/json_parse_exception.dart b/lib/src/exceptions/json_parse_exception.dart new file mode 100644 index 0000000..37de4f7 --- /dev/null +++ b/lib/src/exceptions/json_parse_exception.dart @@ -0,0 +1,53 @@ +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +/// Exception thrown when JSON parsing fails during policy evaluation. +/// +/// This exception is raised when the policy engine encounters malformed +/// or invalid JSON data that cannot be parsed into the expected format. +/// It provides detailed context about the parsing failure including +/// the specific key that failed, the original error, and any additional +/// validation errors. +class JsonParseException implements IDetailPolicySDKException { + @override + final String message; + + /// The specific JSON key that caused the parsing failure, if applicable. + @override + final String? key; + + /// The original error object from the JSON parsing library. + @override + final Object? originalError; + + /// A map of field-specific validation errors encountered during parsing. + @override + final Map? errors; + + /// Creates a new JsonParseException. + /// + /// [message] should describe the parsing failure. + /// [key] optionally specifies which JSON key caused the failure. + /// [originalError] optionally provides the original parsing error. + /// [errors] optionally provides a map of field-specific validation errors. + JsonParseException( + this.message, { + this.key, + this.originalError, + this.errors, + }); + + @override + String toString() { + final buffer = StringBuffer('JsonParseException: $message'); + if (key != null) { + buffer.write(' (key: $key)'); + } + if (originalError != null) { + buffer.write(' (original: $originalError)'); + } + if (errors != null && errors!.isNotEmpty) { + buffer.write(' (${errors!.length} total errors)'); + } + return buffer.toString(); + } +} diff --git a/lib/src/exceptions/json_serialize_exception.dart b/lib/src/exceptions/json_serialize_exception.dart new file mode 100644 index 0000000..06f4604 --- /dev/null +++ b/lib/src/exceptions/json_serialize_exception.dart @@ -0,0 +1,52 @@ +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +/// Exception thrown when JSON serialization fails during policy operations. +/// +/// This exception is raised when the policy engine cannot serialize +/// policy objects or data structures to JSON format. This typically +/// occurs when policy objects contain non-serializable data types +/// or circular references. +class JsonSerializeException implements IDetailPolicySDKException { + @override + final String message; + + /// The specific object key that caused the serialization failure, if applicable. + @override + final String? key; + + /// The original error object from the JSON serialization library. + @override + final Object? originalError; + + /// A map of field-specific serialization errors encountered. + @override + final Map? errors; + + /// Creates a new JsonSerializeException. + /// + /// [message] should describe the serialization failure. + /// [key] optionally specifies which object key caused the failure. + /// [originalError] optionally provides the original serialization error. + /// [errors] optionally provides a map of field-specific serialization errors. + JsonSerializeException( + this.message, { + this.key, + this.originalError, + this.errors, + }); + + @override + String toString() { + final buffer = StringBuffer('JsonSerializeException: $message'); + if (key != null) { + buffer.write(' (key: $key)'); + } + if (originalError != null) { + buffer.write(' (original: $originalError)'); + } + if (errors != null && errors!.isNotEmpty) { + buffer.write(' (${errors!.length} total errors)'); + } + return buffer.toString(); + } +} diff --git a/lib/src/exceptions/policy_not_initialized_exception.dart b/lib/src/exceptions/policy_not_initialized_exception.dart new file mode 100644 index 0000000..b9ea0e2 --- /dev/null +++ b/lib/src/exceptions/policy_not_initialized_exception.dart @@ -0,0 +1,30 @@ +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +/// Exception thrown when a policy engine operation is attempted before initialization. +/// +/// This exception indicates that a policy-related operation was invoked +/// before the policy engine or evaluator was properly initialized. This +/// typically occurs if you attempt to evaluate policies, check permissions, +/// or perform other policy operations before calling the required +/// initialization or setup methods. +/// +/// Example: +/// ```dart +/// if (!_isInitialized) { +/// throw PolicyNotInitializedException('Policy engine must be initialized before use.'); +/// } +/// ``` +class PolicyNotInitializedException implements IPolicySDKException { + /// A message describing the initialization error. + @override + final String message; + + /// Creates a new [PolicyNotInitializedException] with the given [message]. + const PolicyNotInitializedException(this.message); + + @override + String toString() { + final buffer = StringBuffer('PolicyNotInitializedException: $message'); + return buffer.toString(); + } +} diff --git a/lib/src/exceptions/policy_sdk_exceptions.dart b/lib/src/exceptions/policy_sdk_exceptions.dart deleted file mode 100644 index 5444788..0000000 --- a/lib/src/exceptions/policy_sdk_exceptions.dart +++ /dev/null @@ -1,114 +0,0 @@ -/// Base exception class for all policy SDK related errors. -/// -/// This abstract class provides a common interface for all exceptions -/// thrown by the policy engine, ensuring consistent error handling -/// and messaging across the SDK. -abstract class PolicySDKException implements Exception { - /// Creates a new PolicySDKException with the given error message. - /// - /// [message] should provide a clear description of what went wrong. - const PolicySDKException(this.message); - - /// The error message describing the exception. - final String message; - - @override - String toString() => 'PolicySDKException: $message'; -} - -/// Exception thrown when JSON parsing fails during policy evaluation. -/// -/// This exception is raised when the policy engine encounters malformed -/// or invalid JSON data that cannot be parsed into the expected format. -/// It provides detailed context about the parsing failure including -/// the specific key that failed, the original error, and any additional -/// validation errors. -class JsonParseException implements PolicySDKException { - @override - final String message; - - /// The specific JSON key that caused the parsing failure, if applicable. - final String? key; - - /// The original error object from the JSON parsing library. - final Object? originalError; - - /// A map of field-specific validation errors encountered during parsing. - final Map? errors; - - /// Creates a new JsonParseException. - /// - /// [message] should describe the parsing failure. - /// [key] optionally specifies which JSON key caused the failure. - /// [originalError] optionally provides the original parsing error. - /// [errors] optionally provides a map of field-specific validation errors. - JsonParseException( - this.message, { - this.key, - this.originalError, - this.errors, - }); - - @override - String toString() { - final buffer = StringBuffer('JsonParseException: $message'); - if (key != null) { - buffer.write(' (key: $key)'); - } - if (originalError != null) { - buffer.write(' (original: $originalError)'); - } - if (errors != null && errors!.isNotEmpty) { - buffer.write(' (${errors!.length} total errors)'); - } - return buffer.toString(); - } -} - -/// Exception thrown when JSON serialization fails during policy operations. -/// -/// This exception is raised when the policy engine cannot serialize -/// policy objects or data structures to JSON format. This typically -/// occurs when policy objects contain non-serializable data types -/// or circular references. -class JsonSerializeException implements PolicySDKException { - @override - final String message; - - /// The specific object key that caused the serialization failure, if applicable. - final String? key; - - /// The original error object from the JSON serialization library. - final Object? originalError; - - /// A map of field-specific serialization errors encountered. - final Map? errors; - - /// Creates a new JsonSerializeException. - /// - /// [message] should describe the serialization failure. - /// [key] optionally specifies which object key caused the failure. - /// [originalError] optionally provides the original serialization error. - /// [errors] optionally provides a map of field-specific serialization errors. - JsonSerializeException( - this.message, { - this.key, - this.originalError, - this.errors, - }); - - @override - String toString() { - final buffer = StringBuffer('JsonSerializeException: $message'); - if (key != null) { - buffer.write(' (key: $key)'); - } - if (originalError != null) { - buffer.write(' (original: $originalError)'); - } - if (errors != null && errors!.isNotEmpty) { - buffer.write(' (${errors!.length} total errors)'); - } - return buffer.toString(); - } -} diff --git a/lib/src/utils/json_handler.dart b/lib/src/utils/json_handler.dart index 1afd265..71e033d 100644 --- a/lib/src/utils/json_handler.dart +++ b/lib/src/utils/json_handler.dart @@ -1,4 +1,5 @@ -import 'package:flutter_policy_engine/src/exceptions/policy_sdk_exceptions.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_parse_exception.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_serialize_exception.dart'; import 'package:flutter_policy_engine/src/utils/log_handler.dart'; /// Utility for type-safe JSON conversions with generic support. diff --git a/test/utils/json_handler_test.dart b/test/utils/json_handler_test.dart index e6f95aa..260ca7f 100644 --- a/test/utils/json_handler_test.dart +++ b/test/utils/json_handler_test.dart @@ -1,4 +1,5 @@ -import 'package:flutter_policy_engine/src/exceptions/policy_sdk_exceptions.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_parse_exception.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_serialize_exception.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_policy_engine/src/utils/json_handler.dart'; From e8039b558cbea4a6ba0b3263bfbdca2f59b962db Mon Sep 17 00:00:00 2001 From: David Garcia Ruiz Date: Thu, 17 Jul 2025 12:51:24 +0200 Subject: [PATCH 2/6] feat(policy): add PolicyProvider and PolicyWidget for access control management --- lib/flutter_policy_engine.dart | 2 + lib/src/core/policy_manager.dart | 20 +++++++ lib/src/core/policy_provider.dart | 95 ++++++++++++++++++++++++++++++ lib/src/widgets/policy_widget.dart | 83 ++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 lib/src/core/policy_provider.dart create mode 100644 lib/src/widgets/policy_widget.dart diff --git a/lib/flutter_policy_engine.dart b/lib/flutter_policy_engine.dart index b026fb8..160efed 100644 --- a/lib/flutter_policy_engine.dart +++ b/lib/flutter_policy_engine.dart @@ -1,3 +1,5 @@ library flutter_policy_engine; export 'src/core/policy_manager.dart'; +export 'src/widgets/policy_widget.dart'; +export 'src/core/policy_provider.dart'; diff --git a/lib/src/core/policy_manager.dart b/lib/src/core/policy_manager.dart index 5d2ac66..fab0bc6 100644 --- a/lib/src/core/policy_manager.dart +++ b/lib/src/core/policy_manager.dart @@ -185,4 +185,24 @@ class PolicyManager extends ChangeNotifier { rethrow; } } + + /// Checks if the specified [role] has access to the given [content]. + /// + /// Returns `true` if the policy manager is initialized and the evaluator + /// determines that the [role] is permitted to access the [content]. + /// Returns `false` if the policy manager is not initialized, the evaluator + /// is not set, or if access is denied. + /// + /// Logs an error if called before initialization or if the evaluator is missing. + bool hasAccess(String role, String content) { + if (!_isInitialized || _evaluator == null) { + LogHandler.error( + 'Policy manager not initialized or evaluator not set', + context: {'role': role, 'content': content}, + operation: 'policy_manager_access_check', + ); + return false; + } + return _evaluator!.evaluate(role, content); + } } diff --git a/lib/src/core/policy_provider.dart b/lib/src/core/policy_provider.dart new file mode 100644 index 0000000..140641c --- /dev/null +++ b/lib/src/core/policy_provider.dart @@ -0,0 +1,95 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_policy_engine/src/core/policy_manager.dart'; + +/// A widget that provides a [PolicyManager] instance to its descendant widgets. +/// +/// This widget uses Flutter's [InheritedWidget] pattern to make the [PolicyManager] +/// available throughout the widget tree without having to pass it explicitly +/// through constructors. +/// +/// Example usage: +/// ```dart +/// PolicyProvider( +/// policyManager: myPolicyManager, +/// child: MyApp(), +/// ) +/// ``` +/// +/// To access the [PolicyManager] in descendant widgets: +/// ```dart +/// final policyManager = PolicyProvider.policyManagerOf(context); +/// ``` +class PolicyProvider extends InheritedWidget { + /// Creates a [PolicyProvider] widget. + /// + /// The [policyManager] parameter is required and will be made available + /// to all descendant widgets in the widget tree. + /// + /// The [child] parameter is required and represents the widget subtree + /// that will have access to the provided [PolicyManager]. + const PolicyProvider({ + required this.policyManager, + required super.child, + super.key, + }); + + /// The [PolicyManager] instance that will be provided to descendant widgets. + final PolicyManager policyManager; + + /// Returns the nearest [PolicyProvider] widget in the widget tree. + /// + /// This method uses [BuildContext.dependOnInheritedWidgetOfExactType] to + /// establish a dependency on the [PolicyProvider] widget, which means + /// the calling widget will rebuild when the [PolicyProvider] changes. + /// + /// Returns `null` if no [PolicyProvider] is found in the widget tree. + /// + /// Example: + /// ```dart + /// final provider = PolicyProvider.of(context); + /// if (provider != null) { + /// // Use provider.policyManager + /// } + /// ``` + static PolicyProvider? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + /// Returns the [PolicyManager] from the nearest [PolicyProvider] widget. + /// + /// This is a convenience method that combines finding the [PolicyProvider] + /// and accessing its [policyManager] property. It throws a [StateError] + /// if no [PolicyProvider] is found in the widget tree. + /// + /// This method establishes a dependency on the [PolicyProvider] widget, + /// causing the calling widget to rebuild when the provider changes. + /// + /// Throws: + /// - [StateError] if no [PolicyProvider] is found in the widget tree. + /// + /// Example: + /// ```dart + /// final policyManager = PolicyProvider.policyManagerOf(context); + /// // Use policyManager directly + /// ``` + static PolicyManager policyManagerOf(BuildContext context) { + final provider = of(context); + if (provider == null) { + throw StateError('PolicyProvider not found in context'); + } + return provider.policyManager; + } + + /// Determines whether this widget should notify its dependents of changes. + /// + /// Returns `true` if the [policyManager] has changed, indicating that + /// dependent widgets should rebuild. This ensures that widgets using + /// the [PolicyManager] are updated when the manager instance changes. + /// + /// The comparison is done by reference equality, so a new [PolicyManager] + /// instance will trigger a rebuild even if it has the same configuration. + @override + bool updateShouldNotify(PolicyProvider oldWidget) { + return policyManager != oldWidget.policyManager; + } +} diff --git a/lib/src/widgets/policy_widget.dart b/lib/src/widgets/policy_widget.dart new file mode 100644 index 0000000..0dd9da7 --- /dev/null +++ b/lib/src/widgets/policy_widget.dart @@ -0,0 +1,83 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_policy_engine/src/core/policy_provider.dart'; +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +/// A widget that conditionally displays its [child] based on policy access control. +/// +/// [PolicyWidget] checks if the given [role] has access to the specified [content] +/// using the nearest [PolicyProvider] in the widget tree. If access is granted, +/// [child] is rendered. If access is denied, [fallback] is rendered (or an empty +/// [SizedBox] if [fallback] is null), and [onAccessDenied] is called if provided. +/// +/// If a [IPolicySDKException] is thrown during access evaluation, an assertion +/// error is thrown in debug mode; in release mode, access is denied silently. +/// +/// Example usage: +/// ```dart +/// PolicyWidget( +/// role: 'admin', +/// content: 'dashboard', +/// child: DashboardWidget(), +/// fallback: AccessDeniedWidget(), +/// onAccessDenied: () => log('Access denied'), +/// ) +/// ``` +class PolicyWidget extends StatelessWidget { + /// Creates a [PolicyWidget]. + /// + /// [role] is the user or entity role to check. + /// [content] is the resource or content identifier to check access for. + /// [child] is the widget to display if access is granted. + /// [fallback] is the widget to display if access is denied (optional). + /// [onAccessDenied] is a callback invoked when access is denied (optional). + const PolicyWidget({ + required this.role, + required this.content, + required this.child, + this.fallback, + this.onAccessDenied, + super.key, + }); + + /// The role to check for access. + final String role; + + /// The content or resource identifier to check access for. + final String content; + + /// The widget to display if access is granted. + final Widget child; + + /// The widget to display if access is denied. If null, an empty [SizedBox] is shown. + final Widget? fallback; + + /// Callback invoked when access is denied. + final VoidCallback? onAccessDenied; + + @override + Widget build(BuildContext context) { + final policyManager = PolicyProvider.policyManagerOf(context); + + try { + final hasAccess = policyManager.hasAccess(role, content); + + if (hasAccess) { + return child; + } else { + onAccessDenied?.call(); + return fallback ?? const SizedBox.shrink(); + } + } catch (e) { + if (e is IPolicySDKException) { + assert(() { + throw FlutterError('Error en PolicyWidget: ${e.message}'); + }()); + + // On production, deny access silently + onAccessDenied?.call(); + return fallback ?? const SizedBox.shrink(); + } + rethrow; + } + } +} From eca8ed4686d3878e74579c1d80f0ca2d86214dcf Mon Sep 17 00:00:00 2001 From: David Garcia Ruiz Date: Thu, 17 Jul 2025 12:53:55 +0200 Subject: [PATCH 3/6] chore(demo): implement DemoContent widget for role-based access control demonstration --- example/lib/demo_content.dart | 101 ++++++++++++++++++++++++++++ example/lib/main.dart | 68 +------------------ example/lib/policy_engine_demo.dart | 75 +++++++++++++++++++++ 3 files changed, 177 insertions(+), 67 deletions(-) create mode 100644 example/lib/demo_content.dart create mode 100644 example/lib/policy_engine_demo.dart diff --git a/example/lib/demo_content.dart b/example/lib/demo_content.dart new file mode 100644 index 0000000..6863f01 --- /dev/null +++ b/example/lib/demo_content.dart @@ -0,0 +1,101 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; + +class DemoContent extends StatefulWidget { + const DemoContent({super.key}); + + @override + State createState() => _DemoContentState(); +} + +class _DemoContentState extends State { + String _currentRole = 'guest'; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Rol actual: $_currentRole', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + + // Selector de rol + Row( + children: [ + _buildRoleButton('guest'), + const SizedBox(width: 8), + _buildRoleButton('user'), + const SizedBox(width: 8), + _buildRoleButton('admin'), + ], + ), + + const SizedBox(height: 32), + + // Ejemplos de PolicyWidget + _buildPolicyExample('LoginPage', 'Página de Login'), + _buildPolicyExample('Dashboard', 'Dashboard'), + _buildPolicyExample('UserManagement', 'Gestión de Usuarios'), + _buildPolicyExample('Settings', 'Configuración'), + + const SizedBox(height: 32), + ], + ), + ); + } + + Widget _buildRoleButton(String role) { + return ElevatedButton( + onPressed: () { + setState(() { + _currentRole = role; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _currentRole == role ? Colors.blue : Colors.grey, + ), + child: Text(role), + ); + } + + Widget _buildPolicyExample(String content, String displayName) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$displayName:'), + const SizedBox(height: 4), + PolicyWidget( + role: _currentRole, + content: content, + fallback: Card( + color: Colors.red[100], + child: Padding( + padding: const EdgeInsets.all(16), + child: Text('Acceso denegado para $displayName'), + ), + ), + onAccessDenied: () { + log('Acceso denegado para $_currentRole a $content'); + }, + child: Card( + color: Colors.green[100], + child: Padding( + padding: const EdgeInsets.all(16), + child: Text('Acceso permitido a $displayName'), + ), + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 160515e..e51e6db 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,5 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; -import 'package:flutter_policy_engine/flutter_policy_engine.dart'; +import 'package:flutter_policy_engine_example/policy_engine_demo.dart'; void main() { runApp(const MyApp()); @@ -22,67 +20,3 @@ class MyApp extends StatelessWidget { ); } } - -class PolicyEngineDemo extends StatefulWidget { - const PolicyEngineDemo({super.key}); - - @override - State createState() { - return _PolicyEngineDemoState(); - } -} - -class _PolicyEngineDemoState extends State { - late PolicyManager policyManager; - bool _isInitialized = false; - - @override - void initState() { - super.initState(); - _initializePolicyManager(); - } - - Future _initializePolicyManager() async { - policyManager = PolicyManager(); - final policies = { - "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], - "user": ["LoginPage", "Dashboard"], - "guest": ["LoginPage"] - }; - try { - await policyManager.initialize(policies); - setState(() { - _isInitialized = true; - }); - } catch (e) { - log(e.toString()); - setState(() { - _isInitialized = false; - }); - } - } - - @override - Widget build(BuildContext context) { - if (!_isInitialized) { - return Scaffold( - appBar: AppBar( - title: const Text('Policy Engine Demo'), - ), - body: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Loading policies...'), - ], - ), - ), - ); - } - return const Center( - child: Text('Policies loaded'), - ); - } -} diff --git a/example/lib/policy_engine_demo.dart b/example/lib/policy_engine_demo.dart new file mode 100644 index 0000000..8577f5f --- /dev/null +++ b/example/lib/policy_engine_demo.dart @@ -0,0 +1,75 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; +import 'package:flutter_policy_engine_example/demo_content.dart'; + +class PolicyEngineDemo extends StatefulWidget { + const PolicyEngineDemo({super.key}); + + @override + State createState() { + return _PolicyEngineDemoState(); + } +} + +class _PolicyEngineDemoState extends State { + late PolicyManager policyManager; + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + _initializePolicyManager(); + } + + Future _initializePolicyManager() async { + policyManager = PolicyManager(); + final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] + }; + try { + await policyManager.initialize(policies); + setState(() { + _isInitialized = true; + }); + } catch (e) { + log(e.toString()); + setState(() { + _isInitialized = false; + }); + } + } + + @override + Widget build(BuildContext context) { + if (!_isInitialized) { + return Scaffold( + appBar: AppBar( + title: const Text('Policy Engine Demo'), + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading policies...'), + ], + ), + ), + ); + } + return PolicyProvider( + policyManager: policyManager, + child: Scaffold( + appBar: AppBar( + title: const Text('Policy Engine Demo'), + ), + body: const DemoContent(), + ), + ); + } +} From 0ac6d59096e990176448d2098a995d1ce1af4c73 Mon Sep 17 00:00:00 2001 From: David Garcia Ruiz Date: Thu, 17 Jul 2025 17:37:57 +0200 Subject: [PATCH 4/6] chore(dependencies): add comprehensive integration tests for exception handling in the policy SDK - downgrade collection package to version 1.18.0 --- pubspec.yaml | 2 +- .../exceptions_integration_test.dart | 210 ++++++++++++++++++ .../i_policy_sdk_exceptions_test.dart | 116 ++++++++++ .../exceptions/json_parse_exception_test.dart | 152 +++++++++++++ .../json_serialize_exception_test.dart | 180 +++++++++++++++ ...policy_not_initialized_exception_test.dart | 137 ++++++++++++ test/{core => models}/policy_test.dart | 0 7 files changed, 796 insertions(+), 1 deletion(-) create mode 100644 test/exceptions/exceptions_integration_test.dart create mode 100644 test/exceptions/i_policy_sdk_exceptions_test.dart create mode 100644 test/exceptions/json_parse_exception_test.dart create mode 100644 test/exceptions/json_serialize_exception_test.dart create mode 100644 test/exceptions/policy_not_initialized_exception_test.dart rename test/{core => models}/policy_test.dart (100%) diff --git a/pubspec.yaml b/pubspec.yaml index 8e5a958..50d9c13 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ environment: flutter: ">=1.17.0" dependencies: - collection: ^1.19.1 + collection: ^1.18.0 flutter: sdk: flutter meta: ^1.12.0 diff --git a/test/exceptions/exceptions_integration_test.dart b/test/exceptions/exceptions_integration_test.dart new file mode 100644 index 0000000..966d5b3 --- /dev/null +++ b/test/exceptions/exceptions_integration_test.dart @@ -0,0 +1,210 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_parse_exception.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_serialize_exception.dart'; +import 'package:flutter_policy_engine/src/exceptions/policy_not_initialized_exception.dart'; + +void main() { + group('Exceptions Integration Tests', () { + test('all exceptions should implement IPolicySDKException', () { + final exceptions = [ + const PolicyNotInitializedException('Test message'), + JsonParseException('Test message'), + JsonSerializeException('Test message'), + ]; + + for (final exception in exceptions) { + expect(exception, isA()); + expect(exception, isA()); + } + }); + + test('detail exceptions should implement IDetailPolicySDKException', () { + final detailExceptions = [ + JsonParseException('Test message'), + JsonSerializeException('Test message'), + ]; + + for (final exception in detailExceptions) { + expect(exception, isA()); + expect(exception, isA()); + } + }); + + test('basic exceptions should not implement IDetailPolicySDKException', () { + const basicException = PolicyNotInitializedException('Test message'); + + expect(basicException, isA()); + expect(basicException, isNot(isA())); + }); + + test('all exceptions should have consistent message handling', () { + const testMessage = 'Test error message'; + + final exceptions = [ + const PolicyNotInitializedException(testMessage), + JsonParseException(testMessage), + JsonSerializeException(testMessage), + ]; + + for (final exception in exceptions) { + expect(exception.message, equals(testMessage)); + expect(exception.toString(), contains(testMessage)); + } + }); + + test('detail exceptions should handle optional parameters consistently', + () { + const message = 'Test error message'; + const key = 'test_key'; + const originalError = 'test_original_error'; + final errors = {'field1': 'error1', 'field2': 'error2'}; + + final detailExceptions = [ + JsonParseException( + message, + key: key, + originalError: originalError, + errors: errors, + ), + JsonSerializeException( + message, + key: key, + originalError: originalError, + errors: errors, + ), + ]; + + for (final exception in detailExceptions) { + expect(exception.message, equals(message)); + expect(exception.key, equals(key)); + expect(exception.originalError, equals(originalError)); + expect(exception.errors, equals(errors)); + } + }); + + test('exceptions should have distinct type names in toString', () { + const message = 'Test error message'; + + final exceptions = [ + const PolicyNotInitializedException(message), + JsonParseException(message), + JsonSerializeException(message), + ]; + + final typeNames = + exceptions.map((e) => e.toString().split(':')[0]).toSet(); + expect(typeNames.length, equals(3)); + expect(typeNames, contains('PolicyNotInitializedException')); + expect(typeNames, contains('JsonParseException')); + expect(typeNames, contains('JsonSerializeException')); + }); + + test('exceptions should handle null optional parameters gracefully', () { + const message = 'Test error message'; + + final detailExceptions = [ + JsonParseException( + message, + key: null, + originalError: null, + errors: null, + ), + JsonSerializeException( + message, + key: null, + originalError: null, + errors: null, + ), + ]; + + for (final exception in detailExceptions) { + expect(exception.message, equals(message)); + expect(exception.key, isNull); + expect(exception.originalError, isNull); + expect(exception.errors, isNull); + expect(exception.toString(), contains(message)); + expect(exception.toString(), isNot(contains('key:'))); + expect(exception.toString(), isNot(contains('original:'))); + expect(exception.toString(), isNot(contains('total errors'))); + } + }); + + test('exceptions should handle empty errors map correctly', () { + const message = 'Test error message'; + final emptyErrors = {}; + + final detailExceptions = [ + JsonParseException(message, errors: emptyErrors), + JsonSerializeException(message, errors: emptyErrors), + ]; + + for (final exception in detailExceptions) { + expect(exception.errors, equals(emptyErrors)); + expect(exception.toString(), isNot(contains('total errors'))); + } + }); + + test('exceptions should be throwable and catchable', () { + const message = 'Test error message'; + + final exceptions = [ + const PolicyNotInitializedException(message), + JsonParseException(message), + JsonSerializeException(message), + ]; + + for (final exception in exceptions) { + expect(() => throw exception, throwsA(isA())); + expect(() => throw exception, throwsA(isA())); + } + }); + + test('detail exceptions should provide rich error information', () { + const message = 'Test error message'; + const key = 'test_key'; + const originalError = 'test_original_error'; + final errors = {'field1': 'error1', 'field2': 'error2'}; + + final detailExceptions = [ + JsonParseException( + message, + key: key, + originalError: originalError, + errors: errors, + ), + JsonSerializeException( + message, + key: key, + originalError: originalError, + errors: errors, + ), + ]; + + for (final exception in detailExceptions) { + final stringRep = exception.toString(); + expect(stringRep, contains(message)); + expect(stringRep, contains(key)); + expect(stringRep, contains(originalError)); + expect(stringRep, contains('2 total errors')); + } + }); + + test('exceptions should maintain immutability', () { + const message = 'Test error message'; + + final exceptions = [ + const PolicyNotInitializedException(message), + JsonParseException(message), + JsonSerializeException(message), + ]; + + for (final exception in exceptions) { + expect(exception.message, equals(message)); + // Verify that the message field is final and immutable + expect(exception.message, isA()); + expect(exception.message, isNot(equals('Modified message'))); + } + }); + }); +} diff --git a/test/exceptions/i_policy_sdk_exceptions_test.dart b/test/exceptions/i_policy_sdk_exceptions_test.dart new file mode 100644 index 0000000..6675777 --- /dev/null +++ b/test/exceptions/i_policy_sdk_exceptions_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +void main() { + group('IPolicySDKException', () { + test('should create exception with message', () { + const message = 'Test error message'; + const exception = TestPolicySDKException(message); + + expect(exception.message, equals(message)); + }); + + test('should return correct string representation', () { + const message = 'Test error message'; + const exception = TestPolicySDKException(message); + + expect(exception.toString(), equals('PolicySDKException: $message')); + }); + + test('should implement Exception interface', () { + const message = 'Test error message'; + const exception = TestPolicySDKException(message); + + expect(exception, isA()); + }); + }); + + group('IDetailPolicySDKException', () { + test('should create exception with required message', () { + const message = 'Test error message'; + const exception = TestDetailPolicySDKException(message); + + expect(exception.message, equals(message)); + expect(exception.key, isNull); + expect(exception.originalError, isNull); + expect(exception.errors, isNull); + }); + + test('should create exception with all optional parameters', () { + const message = 'Test error message'; + const key = 'test_key'; + const originalError = 'original error'; + final errors = {'field1': 'error1', 'field2': 'error2'}; + + final exception = TestDetailPolicySDKException( + message, + key: key, + originalError: originalError, + errors: errors, + ); + + expect(exception.message, equals(message)); + expect(exception.key, equals(key)); + expect(exception.originalError, equals(originalError)); + expect(exception.errors, equals(errors)); + }); + + test('should implement IPolicySDKException interface', () { + const message = 'Test error message'; + const exception = TestDetailPolicySDKException(message); + + expect(exception, isA()); + }); + + test('should handle null optional parameters', () { + const message = 'Test error message'; + const exception = TestDetailPolicySDKException( + message, + key: null, + originalError: null, + errors: null, + ); + + expect(exception.message, equals(message)); + expect(exception.key, isNull); + expect(exception.originalError, isNull); + expect(exception.errors, isNull); + }); + }); +} + +/// Test implementation of IPolicySDKException for testing purposes +class TestPolicySDKException implements IPolicySDKException { + const TestPolicySDKException(this.message); + + @override + final String message; + + @override + String toString() => 'PolicySDKException: $message'; +} + +/// Test implementation of IDetailPolicySDKException for testing purposes +class TestDetailPolicySDKException implements IDetailPolicySDKException { + const TestDetailPolicySDKException( + this.message, { + this.key, + this.originalError, + this.errors, + }); + + @override + final String message; + + @override + final String? key; + + @override + final Object? originalError; + + @override + final Map? errors; + + @override + String toString() => 'TestDetailPolicySDKException: $message'; +} diff --git a/test/exceptions/json_parse_exception_test.dart b/test/exceptions/json_parse_exception_test.dart new file mode 100644 index 0000000..4e304c5 --- /dev/null +++ b/test/exceptions/json_parse_exception_test.dart @@ -0,0 +1,152 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_parse_exception.dart'; +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +void main() { + group('JsonParseException', () { + test('should create exception with required message', () { + const message = 'Failed to parse JSON'; + final exception = JsonParseException(message); + + expect(exception.message, equals(message)); + expect(exception.key, isNull); + expect(exception.originalError, isNull); + expect(exception.errors, isNull); + }); + + test('should create exception with all optional parameters', () { + const message = 'Failed to parse JSON'; + const key = 'policy_data'; + const originalError = 'Invalid JSON format'; + final errors = { + 'field1': 'Invalid type', + 'field2': 'Missing required field' + }; + + final exception = JsonParseException( + message, + key: key, + originalError: originalError, + errors: errors, + ); + + expect(exception.message, equals(message)); + expect(exception.key, equals(key)); + expect(exception.originalError, equals(originalError)); + expect(exception.errors, equals(errors)); + }); + + test('should implement IDetailPolicySDKException interface', () { + const message = 'Failed to parse JSON'; + final exception = JsonParseException(message); + + expect(exception, isA()); + expect(exception, isA()); + }); + + test('should return correct string representation with only message', () { + const message = 'Failed to parse JSON'; + final exception = JsonParseException(message); + + expect(exception.toString(), equals('JsonParseException: $message')); + }); + + test('should return correct string representation with key', () { + const message = 'Failed to parse JSON'; + const key = 'policy_data'; + final exception = JsonParseException(message, key: key); + + expect(exception.toString(), + equals('JsonParseException: $message (key: $key)')); + }); + + test('should return correct string representation with original error', () { + const message = 'Failed to parse JSON'; + const originalError = 'Invalid JSON format'; + final exception = + JsonParseException(message, originalError: originalError); + + expect(exception.toString(), + equals('JsonParseException: $message (original: $originalError)')); + }); + + test('should return correct string representation with errors', () { + const message = 'Failed to parse JSON'; + final errors = { + 'field1': 'Invalid type', + 'field2': 'Missing required field' + }; + final exception = JsonParseException(message, errors: errors); + + expect(exception.toString(), + equals('JsonParseException: $message (2 total errors)')); + }); + + test('should return correct string representation with all parameters', () { + const message = 'Failed to parse JSON'; + const key = 'policy_data'; + const originalError = 'Invalid JSON format'; + final errors = { + 'field1': 'Invalid type', + 'field2': 'Missing required field' + }; + + final exception = JsonParseException( + message, + key: key, + originalError: originalError, + errors: errors, + ); + + expect( + exception.toString(), + equals( + 'JsonParseException: $message (key: $key) (original: $originalError) (2 total errors)')); + }); + + test('should handle empty errors map', () { + const message = 'Failed to parse JSON'; + final errors = {}; + final exception = JsonParseException(message, errors: errors); + + expect(exception.toString(), equals('JsonParseException: $message')); + }); + + test('should handle null optional parameters', () { + const message = 'Failed to parse JSON'; + final exception = JsonParseException( + message, + key: null, + originalError: null, + errors: null, + ); + + expect(exception.message, equals(message)); + expect(exception.key, isNull); + expect(exception.originalError, isNull); + expect(exception.errors, isNull); + expect(exception.toString(), equals('JsonParseException: $message')); + }); + + test('should handle complex original error objects', () { + const message = 'Failed to parse JSON'; + final originalError = Exception('Complex error object'); + final exception = + JsonParseException(message, originalError: originalError); + + expect(exception.originalError, equals(originalError)); + expect(exception.toString(), contains('Exception: Complex error object')); + }); + + test('should handle special characters in message and key', () { + const message = 'Failed to parse JSON with special chars: !@#\$%^&*()'; + const key = 'policy_data_with_special_chars: !@#\$%^&*()'; + final exception = JsonParseException(message, key: key); + + expect(exception.message, equals(message)); + expect(exception.key, equals(key)); + expect(exception.toString(), + equals('JsonParseException: $message (key: $key)')); + }); + }); +} diff --git a/test/exceptions/json_serialize_exception_test.dart b/test/exceptions/json_serialize_exception_test.dart new file mode 100644 index 0000000..10af2ab --- /dev/null +++ b/test/exceptions/json_serialize_exception_test.dart @@ -0,0 +1,180 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_serialize_exception.dart'; +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +void main() { + group('JsonSerializeException', () { + test('should create exception with required message', () { + const message = 'Failed to serialize object to JSON'; + final exception = JsonSerializeException(message); + + expect(exception.message, equals(message)); + expect(exception.key, isNull); + expect(exception.originalError, isNull); + expect(exception.errors, isNull); + }); + + test('should create exception with all optional parameters', () { + const message = 'Failed to serialize object to JSON'; + const key = 'policy_object'; + const originalError = 'Circular reference detected'; + final errors = { + 'field1': 'Non-serializable type', + 'field2': 'Missing toJson method' + }; + + final exception = JsonSerializeException( + message, + key: key, + originalError: originalError, + errors: errors, + ); + + expect(exception.message, equals(message)); + expect(exception.key, equals(key)); + expect(exception.originalError, equals(originalError)); + expect(exception.errors, equals(errors)); + }); + + test('should implement IDetailPolicySDKException interface', () { + const message = 'Failed to serialize object to JSON'; + final exception = JsonSerializeException(message); + + expect(exception, isA()); + expect(exception, isA()); + }); + + test('should return correct string representation with only message', () { + const message = 'Failed to serialize object to JSON'; + final exception = JsonSerializeException(message); + + expect(exception.toString(), equals('JsonSerializeException: $message')); + }); + + test('should return correct string representation with key', () { + const message = 'Failed to serialize object to JSON'; + const key = 'policy_object'; + final exception = JsonSerializeException(message, key: key); + + expect(exception.toString(), + equals('JsonSerializeException: $message (key: $key)')); + }); + + test('should return correct string representation with original error', () { + const message = 'Failed to serialize object to JSON'; + const originalError = 'Circular reference detected'; + final exception = + JsonSerializeException(message, originalError: originalError); + + expect( + exception.toString(), + equals( + 'JsonSerializeException: $message (original: $originalError)')); + }); + + test('should return correct string representation with errors', () { + const message = 'Failed to serialize object to JSON'; + final errors = { + 'field1': 'Non-serializable type', + 'field2': 'Missing toJson method' + }; + final exception = JsonSerializeException(message, errors: errors); + + expect(exception.toString(), + equals('JsonSerializeException: $message (2 total errors)')); + }); + + test('should return correct string representation with all parameters', () { + const message = 'Failed to serialize object to JSON'; + const key = 'policy_object'; + const originalError = 'Circular reference detected'; + final errors = { + 'field1': 'Non-serializable type', + 'field2': 'Missing toJson method' + }; + + final exception = JsonSerializeException( + message, + key: key, + originalError: originalError, + errors: errors, + ); + + expect( + exception.toString(), + equals( + 'JsonSerializeException: $message (key: $key) (original: $originalError) (2 total errors)'), + ); + }); + + test('should handle empty errors map', () { + const message = 'Failed to serialize object to JSON'; + final errors = {}; + final exception = JsonSerializeException(message, errors: errors); + + expect(exception.toString(), equals('JsonSerializeException: $message')); + }); + + test('should handle null optional parameters', () { + const message = 'Failed to serialize object to JSON'; + final exception = JsonSerializeException( + message, + key: null, + originalError: null, + errors: null, + ); + + expect(exception.message, equals(message)); + expect(exception.key, isNull); + expect(exception.originalError, isNull); + expect(exception.errors, isNull); + expect(exception.toString(), equals('JsonSerializeException: $message')); + }); + + test('should handle complex original error objects', () { + const message = 'Failed to serialize object to JSON'; + final originalError = Exception('Complex serialization error'); + final exception = + JsonSerializeException(message, originalError: originalError); + + expect(exception.originalError, equals(originalError)); + expect(exception.toString(), + contains('Exception: Complex serialization error')); + }); + + test('should handle special characters in message and key', () { + const message = + 'Failed to serialize object with special chars: !@#\$%^&*()'; + const key = 'policy_object_with_special_chars: !@#\$%^&*()'; + final exception = JsonSerializeException(message, key: key); + + expect(exception.message, equals(message)); + expect(exception.key, equals(key)); + expect(exception.toString(), + equals('JsonSerializeException: $message (key: $key)')); + }); + + test('should handle single error in errors map', () { + const message = 'Failed to serialize object to JSON'; + final errors = {'field1': 'Non-serializable type'}; + final exception = JsonSerializeException(message, errors: errors); + + expect(exception.toString(), + equals('JsonSerializeException: $message (1 total errors)')); + }); + + test('should handle multiple errors in errors map', () { + const message = 'Failed to serialize object to JSON'; + final errors = { + 'field1': 'Non-serializable type', + 'field2': 'Missing toJson method', + 'field3': 'Circular reference', + 'field4': 'Invalid data type', + }; + final exception = JsonSerializeException(message, errors: errors); + + expect(exception.toString(), + equals('JsonSerializeException: $message (4 total errors)')); + }); + }); +} diff --git a/test/exceptions/policy_not_initialized_exception_test.dart b/test/exceptions/policy_not_initialized_exception_test.dart new file mode 100644 index 0000000..9598142 --- /dev/null +++ b/test/exceptions/policy_not_initialized_exception_test.dart @@ -0,0 +1,137 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_policy_engine/src/exceptions/policy_not_initialized_exception.dart'; +import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; + +void main() { + group('PolicyNotInitializedException', () { + test('should create exception with required message', () { + const message = 'Policy engine must be initialized before use'; + const exception = PolicyNotInitializedException(message); + + expect(exception.message, equals(message)); + }); + + test('should implement IPolicySDKException interface', () { + const message = 'Policy engine must be initialized before use'; + const exception = PolicyNotInitializedException(message); + + expect(exception, isA()); + expect(exception, isA()); + }); + + test('should return correct string representation', () { + const message = 'Policy engine must be initialized before use'; + const exception = PolicyNotInitializedException(message); + + expect(exception.toString(), + equals('PolicyNotInitializedException: $message')); + }); + + test('should handle empty message', () { + const message = ''; + const exception = PolicyNotInitializedException(message); + + expect(exception.message, equals(message)); + expect(exception.toString(), + equals('PolicyNotInitializedException: $message')); + }); + + test('should handle special characters in message', () { + const message = + 'Policy engine must be initialized before use! @#\$%^&*()'; + const exception = PolicyNotInitializedException(message); + + expect(exception.message, equals(message)); + expect(exception.toString(), + equals('PolicyNotInitializedException: $message')); + }); + + test('should handle long message', () { + const message = + 'This is a very long error message that describes in detail why the policy engine ' + 'must be properly initialized before any operations can be performed, including policy ' + 'evaluation, permission checks, and other related functionality.'; + const exception = PolicyNotInitializedException(message); + + expect(exception.message, equals(message)); + expect(exception.toString(), + equals('PolicyNotInitializedException: $message')); + }); + + test('should handle message with newlines', () { + const message = 'Policy engine must be initialized before use.\n' + 'Please call initialize() method first.'; + const exception = PolicyNotInitializedException(message); + + expect(exception.message, equals(message)); + expect(exception.toString(), + equals('PolicyNotInitializedException: $message')); + }); + + test('should handle message with unicode characters', () { + const message = 'Policy engine must be initialized before use: 🚀✨🎯'; + const exception = PolicyNotInitializedException(message); + + expect(exception.message, equals(message)); + expect(exception.toString(), + equals('PolicyNotInitializedException: $message')); + }); + + test('should be const constructible', () { + const message = 'Policy engine must be initialized before use'; + const exception1 = PolicyNotInitializedException(message); + const exception2 = PolicyNotInitializedException(message); + + expect(identical(exception1, exception2), isTrue); + }); + + test('should have consistent hash codes for same message', () { + const message = 'Policy engine must be initialized before use'; + const exception1 = PolicyNotInitializedException(message); + const exception2 = PolicyNotInitializedException(message); + + expect(exception1.hashCode, equals(exception2.hashCode)); + }); + + test('should have different hash codes for different messages', () { + const message1 = 'Policy engine must be initialized before use'; + const message2 = 'Different error message'; + const exception1 = PolicyNotInitializedException(message1); + const exception2 = PolicyNotInitializedException(message2); + + expect(exception1.hashCode, isNot(equals(exception2.hashCode))); + }); + + test('should be equal to itself', () { + const message = 'Policy engine must be initialized before use'; + const exception = PolicyNotInitializedException(message); + + expect(exception, equals(exception)); + }); + + test('should be equal to another exception with same message', () { + const message = 'Policy engine must be initialized before use'; + const exception1 = PolicyNotInitializedException(message); + const exception2 = PolicyNotInitializedException(message); + + expect(exception1, equals(exception2)); + }); + + test('should not be equal to exception with different message', () { + const message1 = 'Policy engine must be initialized before use'; + const message2 = 'Different error message'; + const exception1 = PolicyNotInitializedException(message1); + const exception2 = PolicyNotInitializedException(message2); + + expect(exception1, isNot(equals(exception2))); + }); + + test('should not be equal to different exception types', () { + const message = 'Policy engine must be initialized before use'; + const exception = PolicyNotInitializedException(message); + final otherException = Exception(message); + + expect(exception, isNot(equals(otherException))); + }); + }); +} diff --git a/test/core/policy_test.dart b/test/models/policy_test.dart similarity index 100% rename from test/core/policy_test.dart rename to test/models/policy_test.dart From e8b5dbbec53387c7a14873e8519e907db3ca2895 Mon Sep 17 00:00:00 2001 From: David Garcia Ruiz Date: Thu, 17 Jul 2025 18:58:39 +0200 Subject: [PATCH 5/6] test(policy): enhance policy manager tests for robust error handling and edge cases --- test/core/policy_manager_test.dart | 114 +++++++++++++++++++++++++++++ test/models/policy_test.dart | 58 +++++++++++++++ test/utils/json_handler_test.dart | 65 ++++++++++++++++ 3 files changed, 237 insertions(+) diff --git a/test/core/policy_manager_test.dart b/test/core/policy_manager_test.dart index 636dcaf..ec6be4c 100644 --- a/test/core/policy_manager_test.dart +++ b/test/core/policy_manager_test.dart @@ -4,6 +4,8 @@ import 'package:flutter_policy_engine/src/core/policy_manager.dart'; import 'package:flutter_policy_engine/src/core/interfaces/i_policy_evaluator.dart'; import 'package:flutter_policy_engine/src/core/interfaces/i_policy_storage.dart'; import 'package:flutter_policy_engine/src/models/policy.dart'; +import 'package:flutter_policy_engine/src/utils/json_handler.dart'; +import 'package:flutter_policy_engine/src/exceptions/json_parse_exception.dart'; /// Mock implementation of IPolicyStorage for testing // ignore: must_be_immutable @@ -63,6 +65,16 @@ class MockPolicyEvaluator implements IPolicyEvaluator { } } +class ThrowingPolicy extends Policy { + const ThrowingPolicy( + {required String roleName, required List allowedContent}) + : super(roleName: roleName, allowedContent: allowedContent); + + static Policy fromJson(Map json) { + throw StateError('Forced error in fromJson'); + } +} + void main() { group('PolicyManager', () { late PolicyManager policyManager; @@ -264,6 +276,25 @@ void main() { expect(policyManager.isInitialized, isTrue); expect(policyManager.policies, isEmpty); }); + + test('should handle exception in Policy.fromJson during initialization', + () async { + // Prepare a validPolicies map that will be passed to parseMap + final validPolicies = { + 'admin': + const Policy(roleName: 'admin', allowedContent: ['read', 'write']) + .toJson(), + }; + + expect( + () => JsonHandler.parseMap( + validPolicies, + (json) => ThrowingPolicy.fromJson(json), + allowPartialSuccess: false, + ), + throwsA(isA()), + ); + }); }); group('Edge cases', () { @@ -334,6 +365,89 @@ void main() { expect(policyManager.isInitialized, isTrue); expect(policyManager.policies.length, equals(1)); }); + + test('should handle policies with non-string content items', () async { + final jsonPolicies = { + 'admin': ['read', 123, 'write'], // contains non-string + }; + + await policyManager.initialize(jsonPolicies); + + // Should initialize successfully but skip invalid policies + expect(policyManager.isInitialized, isTrue); + expect(policyManager.policies, isEmpty); + }); + + test('should handle policies with non-list values', () async { + final jsonPolicies = { + 'admin': 'not_a_list', // should be List + }; + + await policyManager.initialize(jsonPolicies); + + // Should initialize successfully but skip invalid policies + expect(policyManager.isInitialized, isTrue); + expect(policyManager.policies, isEmpty); + }); + + test('should handle complete initialization failure gracefully', + () async { + final jsonPolicies = { + 'admin': null, + 'user': null, + 'guest': null, + }; + + await policyManager.initialize(jsonPolicies); + + // Should still mark as initialized even with no valid policies + expect(policyManager.isInitialized, isTrue); + expect(policyManager.policies, isEmpty); + }); + + test('should handle hasAccess when not initialized', () { + expect(policyManager.hasAccess('admin', 'read'), isFalse); + }); + + test('should handle hasAccess when evaluator is null', () async { + // Initialize with empty policies to create null evaluator + await policyManager.initialize({}); + + expect(policyManager.hasAccess('admin', 'read'), isFalse); + }); + + test('should handle initialization error and rethrow', () async { + // Create a mock storage that throws on save + mockStorage.setShouldThrowOnSave(true); + + final jsonPolicies = { + 'admin': ['read', 'write'], + }; + + expect( + () => policyManager.initialize(jsonPolicies), + throwsA(isA()), + ); + + expect(policyManager.isInitialized, isFalse); + }); + + test('should handle JsonParseException during initialization', () async { + // Create policies that will cause JsonParseException + final jsonPolicies = { + 'admin': ['read', 'write'], + }; + + // Mock the JsonHandler to throw an exception + // This is a bit tricky to test directly, so we'll test the error handling + // by creating a scenario where the storage throws during save + mockStorage.setShouldThrowOnSave(true); + + expect( + () => policyManager.initialize(jsonPolicies), + throwsA(isA()), + ); + }); }); }); } diff --git a/test/models/policy_test.dart b/test/models/policy_test.dart index 02e800b..80ffc62 100644 --- a/test/models/policy_test.dart +++ b/test/models/policy_test.dart @@ -528,6 +528,64 @@ void main() { expect(() => Policy.fromJson(json), throwsA(isA())); }); + + test('should handle non-string items in allowedContent', () { + final json = { + 'roleName': 'admin', + 'allowedContent': ['read', 123, 'write'], // contains non-string + }; + + expect(() => Policy.fromJson(json), throwsA(isA())); + }); + }); + + group('hashCode', () { + test('should generate consistent hash codes for equal policies', () { + const policy1 = Policy( + roleName: 'admin', + allowedContent: ['read', 'write'], + ); + const policy2 = Policy( + roleName: 'admin', + allowedContent: ['write', 'read'], // different order + ); + + expect(policy1.hashCode, equals(policy2.hashCode)); + }); + + test('should generate different hash codes for different policies', () { + const policy1 = Policy( + roleName: 'admin', + allowedContent: ['read', 'write'], + ); + const policy2 = Policy( + roleName: 'user', + allowedContent: ['read', 'write'], + ); + + expect(policy1.hashCode, isNot(equals(policy2.hashCode))); + }); + + test('should handle empty allowedContent in hashCode', () { + const policy = Policy( + roleName: 'admin', + allowedContent: [], + ); + + expect(policy.hashCode, isA()); + expect(policy.hashCode, isNot(equals(0))); + }); + + test('should handle large allowedContent lists in hashCode', () { + final largeContentList = List.generate(100, (i) => 'content_$i'); + final policy = Policy( + roleName: 'admin', + allowedContent: largeContentList, + ); + + expect(policy.hashCode, isA()); + expect(policy.hashCode, isNot(equals(0))); + }); }); }); } diff --git a/test/utils/json_handler_test.dart b/test/utils/json_handler_test.dart index 260ca7f..004bb90 100644 --- a/test/utils/json_handler_test.dart +++ b/test/utils/json_handler_test.dart @@ -646,6 +646,71 @@ void main() { expect(convertedUsers.containsKey('corrupted_user'), false); expect(convertedUsers.containsKey('null_user'), false); }); + + test( + 'should handle complete parsing failure with allowPartialSuccess false', + () { + final invalidJsonMap = { + 'user1': 'not_a_map', + 'user2': null, + 'user3': 123, + }; + + expect( + () => JsonHandler.parseMap( + invalidJsonMap, + (json) => TestUser.fromJson(json), + allowPartialSuccess: false, + ), + throwsA(isA()), + ); + }); + + test( + 'should handle complete serialization failure with allowPartialSuccess false', + () { + final problematicUsers = { + 'user1': ProblematicUser('User1', 25), + 'user2': ProblematicUser('User2', 30), + }; + + expect( + () => JsonHandler.mapToJson( + problematicUsers, + (user) => user.toJson(), + allowPartialSuccess: false, + ), + throwsA(isA()), + ); + }); + + test('should handle empty map with allowPartialSuccess false', () { + final emptyMap = {}; + + expect( + () => JsonHandler.parseMap( + emptyMap, + (json) => TestUser.fromJson(json), + allowPartialSuccess: false, + ), + throwsA(isA()), + ); + }); + + test( + 'should handle empty map serialization with allowPartialSuccess false', + () { + final emptyMap = {}; + + expect( + () => JsonHandler.mapToJson( + emptyMap, + (user) => user.toJson(), + allowPartialSuccess: false, + ), + throwsA(isA()), + ); + }); }); }); } From 16043ea3753a32e2d71e58a1bc2d38fc60224563 Mon Sep 17 00:00:00 2001 From: David Garcia Ruiz Date: Thu, 17 Jul 2025 19:14:09 +0200 Subject: [PATCH 6/6] refactor(policy): streamline policy validation logic and enhance exception interface --- lib/src/core/policy_manager.dart | 77 +++++------- .../exceptions/i_policy_sdk_exceptions.dart | 40 ++---- test/core/policy_manager_test.dart | 3 +- .../i_policy_sdk_exceptions_test.dart | 116 ------------------ 4 files changed, 45 insertions(+), 191 deletions(-) delete mode 100644 test/exceptions/i_policy_sdk_exceptions_test.dart diff --git a/lib/src/core/policy_manager.dart b/lib/src/core/policy_manager.dart index fab0bc6..1aa5921 100644 --- a/lib/src/core/policy_manager.dart +++ b/lib/src/core/policy_manager.dart @@ -86,54 +86,43 @@ class PolicyManager extends ChangeNotifier { final key = entry.key; final value = entry.value; - try { - if (value == null) { - LogHandler.warning( - 'Skipping null policy value', - context: {'role': key}, - operation: 'policy_validation_skip', - ); - continue; - } - - if (value is! List) { - LogHandler.warning( - 'Skipping invalid policy value type', - context: { - 'role': key, - 'expected_type': 'List', - 'actual_type': value.runtimeType.toString(), - }, - operation: 'policy_validation_skip', - ); - continue; - } - - if (value.any((item) => item is! String)) { - LogHandler.warning( - 'Skipping policy with non-string content items', - context: {'role': key}, - operation: 'policy_validation_skip', - ); - continue; - } - - // Create the policy and add to valid policies - final policy = Policy( - roleName: key, - allowedContent: value.cast(), + if (value == null) { + LogHandler.warning( + 'Skipping null policy value', + context: {'role': key}, + operation: 'policy_validation_skip', + ); + continue; + } + + if (value is! List) { + LogHandler.warning( + 'Skipping invalid policy value type', + context: { + 'role': key, + 'expected_type': 'List', + 'actual_type': value.runtimeType.toString(), + }, + operation: 'policy_validation_skip', ); - validPolicies[key] = policy.toJson(); - } catch (e, stackTrace) { - LogHandler.error( - 'Failed to process policy', - error: e, - stackTrace: stackTrace, + continue; + } + + if (value.any((item) => item is! String)) { + LogHandler.warning( + 'Skipping policy with non-string content items', context: {'role': key}, - operation: 'policy_processing_error', + operation: 'policy_validation_skip', ); - // Continue with other policies + continue; } + + // Create the policy and add to valid policies + final policy = Policy( + roleName: key, + allowedContent: value.cast(), + ); + validPolicies[key] = policy.toJson(); } _policies = JsonHandler.parseMap( diff --git a/lib/src/exceptions/i_policy_sdk_exceptions.dart b/lib/src/exceptions/i_policy_sdk_exceptions.dart index 6ebb544..d87a59f 100644 --- a/lib/src/exceptions/i_policy_sdk_exceptions.dart +++ b/lib/src/exceptions/i_policy_sdk_exceptions.dart @@ -1,51 +1,33 @@ -/// Base exception class for all policy SDK related errors. +/// Base exception interface for all policy SDK related errors. /// -/// This abstract class provides a common interface for all exceptions +/// This abstract interface provides a common contract for all exceptions /// thrown by the policy engine, ensuring consistent error handling /// and messaging across the SDK. abstract class IPolicySDKException implements Exception { - /// Creates a new PolicySDKException with the given error message. - /// - /// [message] should provide a clear description of what went wrong. - const IPolicySDKException(this.message); - /// The error message describing the exception. - final String message; + String get message; @override - String toString() => 'PolicySDKException: $message'; + String toString(); } -/// Abstract exception for detailed policy SDK errors with contextual information. +/// Abstract interface for detailed policy SDK errors with contextual information. /// -/// This class extends [IPolicySDKException] to provide additional context for +/// This interface extends [IPolicySDKException] to provide additional context for /// errors that occur within the policy engine, such as the specific key involved, /// the original error thrown, and a map of field-specific validation errors. -/// Subclasses should use this to represent exceptions where more granular +/// Implementations should use this to represent exceptions where more granular /// diagnostic information is valuable for debugging or reporting. abstract class IDetailPolicySDKException implements IPolicySDKException { - /// Creates a new [IDetailPolicySDKException] with an error [message] and optional details. - /// - /// [message] provides a human-readable description of the error. - /// [key] optionally identifies the specific key or field related to the error. - /// [originalError] optionally contains the original error object that triggered this exception. - /// [errors] optionally provides a map of field-specific validation errors. - const IDetailPolicySDKException( - this.message, { - this.key, - this.originalError, - this.errors, - }); - @override - final String message; + String get message; /// The specific key or field that caused the error, if applicable. - final String? key; + String? get key; /// The original error object that led to this exception, if available. - final Object? originalError; + Object? get originalError; /// A map of field-specific validation errors encountered during processing, if any. - final Map? errors; + Map? get errors; } diff --git a/test/core/policy_manager_test.dart b/test/core/policy_manager_test.dart index ec6be4c..d995628 100644 --- a/test/core/policy_manager_test.dart +++ b/test/core/policy_manager_test.dart @@ -67,8 +67,7 @@ class MockPolicyEvaluator implements IPolicyEvaluator { class ThrowingPolicy extends Policy { const ThrowingPolicy( - {required String roleName, required List allowedContent}) - : super(roleName: roleName, allowedContent: allowedContent); + {required super.roleName, required super.allowedContent}); static Policy fromJson(Map json) { throw StateError('Forced error in fromJson'); diff --git a/test/exceptions/i_policy_sdk_exceptions_test.dart b/test/exceptions/i_policy_sdk_exceptions_test.dart deleted file mode 100644 index 6675777..0000000 --- a/test/exceptions/i_policy_sdk_exceptions_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart'; - -void main() { - group('IPolicySDKException', () { - test('should create exception with message', () { - const message = 'Test error message'; - const exception = TestPolicySDKException(message); - - expect(exception.message, equals(message)); - }); - - test('should return correct string representation', () { - const message = 'Test error message'; - const exception = TestPolicySDKException(message); - - expect(exception.toString(), equals('PolicySDKException: $message')); - }); - - test('should implement Exception interface', () { - const message = 'Test error message'; - const exception = TestPolicySDKException(message); - - expect(exception, isA()); - }); - }); - - group('IDetailPolicySDKException', () { - test('should create exception with required message', () { - const message = 'Test error message'; - const exception = TestDetailPolicySDKException(message); - - expect(exception.message, equals(message)); - expect(exception.key, isNull); - expect(exception.originalError, isNull); - expect(exception.errors, isNull); - }); - - test('should create exception with all optional parameters', () { - const message = 'Test error message'; - const key = 'test_key'; - const originalError = 'original error'; - final errors = {'field1': 'error1', 'field2': 'error2'}; - - final exception = TestDetailPolicySDKException( - message, - key: key, - originalError: originalError, - errors: errors, - ); - - expect(exception.message, equals(message)); - expect(exception.key, equals(key)); - expect(exception.originalError, equals(originalError)); - expect(exception.errors, equals(errors)); - }); - - test('should implement IPolicySDKException interface', () { - const message = 'Test error message'; - const exception = TestDetailPolicySDKException(message); - - expect(exception, isA()); - }); - - test('should handle null optional parameters', () { - const message = 'Test error message'; - const exception = TestDetailPolicySDKException( - message, - key: null, - originalError: null, - errors: null, - ); - - expect(exception.message, equals(message)); - expect(exception.key, isNull); - expect(exception.originalError, isNull); - expect(exception.errors, isNull); - }); - }); -} - -/// Test implementation of IPolicySDKException for testing purposes -class TestPolicySDKException implements IPolicySDKException { - const TestPolicySDKException(this.message); - - @override - final String message; - - @override - String toString() => 'PolicySDKException: $message'; -} - -/// Test implementation of IDetailPolicySDKException for testing purposes -class TestDetailPolicySDKException implements IDetailPolicySDKException { - const TestDetailPolicySDKException( - this.message, { - this.key, - this.originalError, - this.errors, - }); - - @override - final String message; - - @override - final String? key; - - @override - final Object? originalError; - - @override - final Map? errors; - - @override - String toString() => 'TestDetailPolicySDKException: $message'; -}