diff --git a/packages/logging_cloudwatch/amplify_logging_cloudwatch/lib/src/path_provider/flutter_path_provider.dart b/packages/logging_cloudwatch/amplify_logging_cloudwatch/lib/src/path_provider/flutter_path_provider.dart new file mode 100644 index 0000000000..d19c258c22 --- /dev/null +++ b/packages/logging_cloudwatch/amplify_logging_cloudwatch/lib/src/path_provider/flutter_path_provider.dart @@ -0,0 +1,23 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; +import 'package:path_provider/path_provider.dart'; + +/// {@template flutter_path_provider} +/// A [AppPathProvider] that uses the [path_provider](https://pub.dev/packages/path_provider) +/// package to get the application support and temporary paths. +/// {@endtemplate} +class FlutterPathProvider implements AppPathProvider { + @override + Future getApplicationSupportPath() async { + final directory = await getApplicationSupportDirectory(); + return directory.path; + } + + @override + Future getTemporaryPath() async { + final directory = await getTemporaryDirectory(); + return directory.path; + } +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart index 22fe2c1cbe..b3eecf74bb 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/cloudwatch_logger_plugin.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'dart:math'; -import 'package:aws_common/aws_common.dart'; +import 'package:amplify_core/amplify_core.dart'; import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; import 'package:aws_logging_cloudwatch/src/sdk/cloud_watch_logs.dart'; import 'package:aws_logging_cloudwatch/src/stoppable_timer.dart'; @@ -76,9 +76,9 @@ class CloudWatchLoggerPlugin extends AWSLoggerPlugin region: pluginConfig.region, credentialsProvider: credentialsProvider, ) { - _timer = pluginConfig.flushIntervalInSeconds > Duration.zero + _timer = pluginConfig.flushInterval > Duration.zero ? StoppableTimer( - duration: pluginConfig.flushIntervalInSeconds, + duration: pluginConfig.flushInterval, callback: _startSyncingIfNotInProgress, onError: _onTimerError, ) diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart new file mode 100644 index 0000000000..94627270bc --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage.dart @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage_stub.dart' + if (dart.library.io) 'file_storage_vm.dart' + if (dart.library.html) 'file_storage_web.dart'; + +/// File storage interface for saving and loading constraint locally +abstract interface class FileStorage { + /// Default Constructor or FileStorage + factory FileStorage(AppPathProvider pathProvider) = FileStorageImpl; + + /// Save constraint locally to file + Future save(String fileName, String data); + + /// Load constraint from file + Future load(String fileName); +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_stub.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_stub.dart new file mode 100644 index 0000000000..6b8971325c --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_stub.dart @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; + +/// File storage implementation for saving and loading constraint locally +class FileStorageImpl implements FileStorage { + /// File storage implementation for saving and loading constraint locally + FileStorageImpl(this.pathProvider); + + /// Path provider to get the application support path + final AppPathProvider pathProvider; + + @override + Future load(String fileName) async { + throw UnimplementedError(); + } + + @override + Future save(String fileName, String content) async { + throw UnimplementedError(); + } +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart new file mode 100644 index 0000000000..30c39773e0 --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_vm.dart @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:io'; + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; +import 'package:path/path.dart' as p; + +/// File storage implementation for saving and loading constraint locally +class FileStorageImpl implements FileStorage { + /// File storage implementation for saving and loading constraint locally + FileStorageImpl(this.pathProvider); + + /// Path provider to get the application support path + final AppPathProvider pathProvider; + + @override + Future load(String fileName) async { + final file = + File(p.join(await pathProvider.getApplicationSupportPath(), fileName)); + if (await file.exists()) { + return file.readAsString(); + } + return null; + } + + @override + Future save(String fileName, String content) async { + final file = + File(p.join(await pathProvider.getApplicationSupportPath(), fileName)); + await file.writeAsString(content); + } +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart new file mode 100644 index 0000000000..4475a00b62 --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/file_storage/file_storage_web.dart @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:html'; + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; + +/// File storage implementation for saving and loading constraint locally +class FileStorageImpl implements FileStorage { + /// File storage implementation for saving and loading constraint locally + // ignore: avoid_unused_constructor_parameters + FileStorageImpl(AppPathProvider pathProvider); + + static const _prefix = 'aws.cloudwatch'; + + @override + Future load(String fileName) async { + return window.localStorage['$_prefix.$fileName']; + } + + @override + Future save(String fileName, String content) async { + window.localStorage['$_prefix.$fileName'] = content; + } +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart index 9fd10d18d0..3367eb505c 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.dart @@ -1,9 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import 'package:aws_common/aws_common.dart'; +import 'package:amplify_core/amplify_core.dart'; import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; +part 'plugin_config.g.dart'; + /// {@template aws_logging_cloudwatch.cloudwatch_logger_plugin_configuration} /// The configuration for `CloudWatchLoggerPlugin`. /// {@endtemplate} @@ -15,7 +17,7 @@ class CloudWatchLoggerPluginConfiguration with AWSDebuggable { required this.localLoggingConstraint, this.enable = true, this.localStoreMaxSizeInMB = 5, - this.flushIntervalInSeconds = const Duration(seconds: 60), + this.flushInterval = const Duration(seconds: 60), this.defaultRemoteConfiguration, }); @@ -31,8 +33,8 @@ class CloudWatchLoggerPluginConfiguration with AWSDebuggable { /// The max size of the local store in MB to be used for storing logs locally. final int localStoreMaxSizeInMB; - /// The duration in seconds for sending locally stored logs to CloudWatch. - final Duration flushIntervalInSeconds; + /// The duration for sending locally stored logs to CloudWatch. + final Duration flushInterval; /// {@macro aws_logging_cloudwatch.logging_constraint} final LoggingConstraint localLoggingConstraint; @@ -47,13 +49,59 @@ class CloudWatchLoggerPluginConfiguration with AWSDebuggable { /// {@template aws_logging_cloudwatch.logging_constraint} /// The logging constraint for sending logs to CloudWatch. /// {@endtemplate} -class LoggingConstraint with AWSDebuggable { +@zAmplifySerializable +class LoggingConstraint with AWSDebuggable, AWSSerializable { /// {@macro aws_logging_cloudwatch.logging_constraint} - const LoggingConstraint({this.defaultLogLevel = LogLevel.error}); + const LoggingConstraint({ + this.defaultLogLevel = LogLevel.error, + this.categoryLogLevel, + this.userLogLevel, + }); + + /// Converts a [Map] to an [LoggingConstraint] instance. + factory LoggingConstraint.fromJson(Map json) => + _$LoggingConstraintFromJson(json); + + /// Converts an [LoggingConstraint] instance to a [Map]. + @override + Map toJson() => _$LoggingConstraintToJson(this); /// The default [LogLevel] for sending logs to CloudWatch. final LogLevel defaultLogLevel; + /// The [LogLevel] for different categories. + final Map? categoryLogLevel; + + /// The [LogLevel] for different users. + final Map? userLogLevel; + @override String get runtimeTypeName => 'LoggingConstraint'; } + +/// The logging constraint for user specific log level. +@zAmplifySerializable +class UserLogLevel with AWSDebuggable, AWSSerializable { + /// The logging constraint for user specific log level. + const UserLogLevel({ + this.defaultLogLevel, + this.categoryLogLevel, + }); + + ///Converts a [Map] to a [UserLogLevel] instance. + factory UserLogLevel.fromJson(Map json) => + _$UserLogLevelFromJson(json); + + /// Converts a [UserLogLevel] instance to a [Map]. + @override + Map toJson() => _$UserLogLevelToJson(this); + + /// The default [LogLevel] for sending logs to CloudWatch. + final LogLevel? defaultLogLevel; + + /// The [LogLevel] for different categories. + final Map? categoryLogLevel; + + @override + String get runtimeTypeName => 'UserLogLevel'; +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart new file mode 100644 index 0000000000..64733e29d2 --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/plugin_config.g.dart @@ -0,0 +1,76 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'plugin_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoggingConstraint _$LoggingConstraintFromJson(Map json) => + LoggingConstraint( + defaultLogLevel: + $enumDecodeNullable(_$LogLevelEnumMap, json['defaultLogLevel']) ?? + LogLevel.error, + categoryLogLevel: + (json['categoryLogLevel'] as Map?)?.map( + (k, e) => MapEntry(k, $enumDecode(_$LogLevelEnumMap, e)), + ), + userLogLevel: (json['userLogLevel'] as Map?)?.map( + (k, e) => MapEntry(k, UserLogLevel.fromJson(e as Map)), + ), + ); + +Map _$LoggingConstraintToJson(LoggingConstraint instance) { + final val = { + 'defaultLogLevel': _$LogLevelEnumMap[instance.defaultLogLevel]!, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull( + 'categoryLogLevel', + instance.categoryLogLevel + ?.map((k, e) => MapEntry(k, _$LogLevelEnumMap[e]!))); + writeNotNull('userLogLevel', + instance.userLogLevel?.map((k, e) => MapEntry(k, e.toJson()))); + return val; +} + +const _$LogLevelEnumMap = { + LogLevel.verbose: 'verbose', + LogLevel.debug: 'debug', + LogLevel.info: 'info', + LogLevel.warn: 'warn', + LogLevel.error: 'error', + LogLevel.none: 'none', +}; + +UserLogLevel _$UserLogLevelFromJson(Map json) => UserLogLevel( + defaultLogLevel: + $enumDecodeNullable(_$LogLevelEnumMap, json['defaultLogLevel']), + categoryLogLevel: + (json['categoryLogLevel'] as Map?)?.map( + (k, e) => MapEntry(k, $enumDecode(_$LogLevelEnumMap, e)), + ), + ); + +Map _$UserLogLevelToJson(UserLogLevel instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('defaultLogLevel', _$LogLevelEnumMap[instance.defaultLogLevel]); + writeNotNull( + 'categoryLogLevel', + instance.categoryLogLevel + ?.map((k, e) => MapEntry(k, _$LogLevelEnumMap[e]!))); + return val; +} diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart index 7a8a36332f..ff6e0b31a1 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/lib/src/remote_constraint_provider.dart @@ -1,11 +1,15 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// TODO(nikahsn): remove after implementing the get loggingConstraint. -// ignore_for_file: unused_field +import 'dart:async'; +import 'dart:convert'; +import 'package:amplify_core/amplify_core.dart'; import 'package:aws_common/aws_common.dart'; import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; +import 'package:aws_signature_v4/aws_signature_v4.dart'; +import 'package:meta/meta.dart'; /// {@template aws_logging_cloudwatch.remote_logging_constraint_provider} /// An Interface to provide custom implementation for @@ -17,40 +21,197 @@ abstract class RemoteLoggingConstraintProvider { LoggingConstraint? get loggingConstraint; } +/// {@template aws_logging_cloudwatch.base_remote_constraints_provider} +/// Base class for [RemoteLoggingConstraintProvider] to provide +/// [LoggingConstraint] from a remote location and cache it. +/// {@endtemplate} +base class BaseRemoteLoggingConstraintProvider + with AWSDebuggable, AWSLoggerMixin + implements RemoteLoggingConstraintProvider, Closeable { + /// {@macro aws_logging_cloudwatch.base_remote_constraints_provider} + BaseRemoteLoggingConstraintProvider({ + required DefaultRemoteConfiguration config, + FileStorage? fileStorage, + }) : _fileStorage = fileStorage, + _config = config, + _awsHttpClient = AWSHttpClient() { + _init(); + } + + /// A [BaseRemoteLoggingConstraintProvider] to use only for testing. + @protected + @visibleForTesting + BaseRemoteLoggingConstraintProvider.forTesting({ + required DefaultRemoteConfiguration config, + required AWSHttpClient awsHttpClient, + FileStorage? fileStorage, + }) : _fileStorage = fileStorage, + _config = config, + _awsHttpClient = awsHttpClient { + _init(); + } + + final FileStorage? _fileStorage; + + final DefaultRemoteConfiguration _config; + + LoggingConstraint? _loggingConstraint; + + final AWSHttpClient _awsHttpClient; + + static const _cacheFileName = 'remoteloggingconstraints.json'; + + // The timer to refresh the constraint periodically. + Timer? _timer; + + /// Retrives the runtime type name used for logging. + @override + String get runtimeTypeName => 'BaseRemoteConstraintsProvider'; + + /// Initializes the [BaseRemoteLoggingConstraintProvider] by fetching + /// the constraint from the endpoint initially and then + /// starting the refresh timer afterwards. + void _init() { + _readyCompleter.complete(_refreshConstraintPeriodically()); + } + + final Completer _readyCompleter = Completer(); + + /// A future that completes when the [BaseRemoteLoggingConstraintProvider] + Future get ready => _readyCompleter.future; + + /// Creates a request to fetch the constraint from the endpoint. + /// + /// Can be overridden by subclasses to change how the request is created. + @protected + @visibleForOverriding + Future createRequest() async { + final uri = Uri.parse(_config.endpoint); + return AWSHttpRequest( + method: AWSHttpMethod.get, + uri: uri, + headers: const { + AWSHeaders.accept: 'application/json; charset=utf-8', + }, + ); + } + + /// Fetches the constraint from the endpoint and caches it. + Future _fetchAndCacheConstraintFromEndpoint() async { + try { + final request = await createRequest(); + final operation = _awsHttpClient.send(request); + final response = await operation.response; + final body = await response.decodeBody(); + if (response.statusCode != 200) { + logger + .error('Failed to fetch constraints', (response.statusCode, body)); + return; + } + final fetchedConstraint = LoggingConstraint.fromJson( + jsonDecode(body) as Map, + ); + _loggingConstraint = fetchedConstraint; + + if (_fileStorage != null) { + await _fileStorage?.save( + _cacheFileName, + jsonEncode(fetchedConstraint.toJson()), + ); + } + } on Exception catch (exception) { + logger.error( + 'Failed to fetch logging constraint from ${_config.endpoint}: $exception', + ); + } + } + + /// Returns [LoggingConstraint] from the cache or `null` if the cache is missing + /// or if the constraints could not be retrieved from the remote server. + @override + LoggingConstraint? get loggingConstraint => _loggingConstraint; + + Future _loadConstraintFromLocalStorage() async { + final localConstraint = await _fileStorage?.load(_cacheFileName); + if (localConstraint != null) { + _loggingConstraint = LoggingConstraint.fromJson( + jsonDecode(localConstraint) as Map, + ); + } + } + + /// Refreshes the constraint from the endpoint periodically. + Future _refreshConstraintPeriodically() async { + await _loadConstraintFromLocalStorage(); + _timer = Timer.periodic( + _config.refreshInterval, + (_) => _fetchAndCacheConstraintFromEndpoint(), + ); + await _fetchAndCacheConstraintFromEndpoint(); + } + + @override + void close() { + _timer?.cancel(); + _timer = null; + } +} + /// {@template aws_logging_cloudwatch.default_remote_logging_constraint_provider} /// Default implementation of [RemoteLoggingConstraintProvider] to fetch /// [LoggingConstraint] from an http endpoint periodically. /// {@endtemplate} -class DefaultRemoteLoggingConstraintProvider - implements RemoteLoggingConstraintProvider { +final class DefaultRemoteLoggingConstraintProvider + extends BaseRemoteLoggingConstraintProvider { /// {@macro aws_logging_cloudwatch.default_remote_logging_constraint_provider} DefaultRemoteLoggingConstraintProvider({ - required DefaultRemoteConfiguration config, - required AWSCredentialsProvider credentialsProvider, - }) : _config = config, - _credentialsProvider = credentialsProvider; + required super.config, + required this.credentialsProvider, + super.fileStorage, + }); - final DefaultRemoteConfiguration _config; - final AWSCredentialsProvider _credentialsProvider; + /// The credentials provider to use for signing the request. + final AWSCredentialsProvider credentialsProvider; + + /// The signer to use for signing the request. + late final AWSSigV4Signer _signer = AWSSigV4Signer( + credentialsProvider: credentialsProvider, + ); @override - // TODO(nikahsn): add implementation. - LoggingConstraint get loggingConstraint => throw UnimplementedError(); + Future createRequest() async { + final baseRequest = await super.createRequest(); + final scope = AWSCredentialScope( + region: _config.region, + service: AWSService.apiGatewayManagementApi, + ); + + final signedRequest = await _signer.sign( + baseRequest, + credentialScope: scope, + ); + + return signedRequest; + } } /// {@template aws_logging_cloudwatch.default_remote_configuration} -/// The configuration for [DefaultRemoteLoggingConstraintProvider] +/// The configuration for [BaseRemoteLoggingConstraintProvider] /// {@endtemplate} class DefaultRemoteConfiguration { /// {@macro aws_logging_cloudwatch.default_remote_configuration} const DefaultRemoteConfiguration({ required this.endpoint, - this.refreshIntervalInSeconds = const Duration(seconds: 1200), + this.refreshInterval = const Duration(seconds: 1200), + required this.region, }); /// The endpoint to fetch the `loggingConstraint`. final String endpoint; - /// The referesh interval in seconds to fetch the `loggingConstraint`. - final Duration refreshIntervalInSeconds; + /// The referesh interval to fetch the `loggingConstraint`. + final Duration refreshInterval; + + /// The region of the endpoint. + final String region; } diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml b/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml index 9d2e09f7d3..560eb790c3 100644 --- a/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/pubspec.yaml @@ -16,7 +16,9 @@ dependencies: built_value: ">=8.6.0 <8.7.0" fixnum: ^1.1.0 intl: ">=0.18.0 <1.0.0" + json_annotation: ^4.8.1 meta: ^1.9.1 + path: ^1.8.0 smithy: ^0.5.0+3 smithy_aws: ^0.5.0+3 diff --git a/packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart b/packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart new file mode 100644 index 0000000000..cd1e4ba6ce --- /dev/null +++ b/packages/logging_cloudwatch/aws_logging_cloudwatch/test/remote_constraint_provider_test.dart @@ -0,0 +1,227 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:convert'; + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_common/testing.dart'; +import 'package:aws_logging_cloudwatch/aws_logging_cloudwatch.dart'; +import 'package:aws_logging_cloudwatch/src/file_storage/file_storage.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +const sampleJson = ''' + { + "defaultLogLevel": "error", + "categoryLogLevel": { + "API": "debug", + "AUTH": "debug" + }, + "userLogLevel": { + "cognito-sub-xyz-123": { + "defaultLogLevel": "verbose", + "categoryLogLevel": { + "API": "verbose", + "AUTH": "verbose" + } + } + } + } + '''; + +class MockFileStorage extends Mock implements FileStorage {} + +class MockAWSCredentialsProvider extends Mock + implements AWSCredentialsProvider {} + +class PathProvider implements AppPathProvider { + @override + Future getApplicationSupportPath() async { + return ''; + } + + @override + Future getTemporaryPath() async { + return ''; + } +} + +final fakeRequest = AWSHttpRequest( + method: AWSHttpMethod.get, + uri: Uri.parse('https://fakewebsite.com'), + headers: const {}, + body: utf8.encode('sample'), +); + +void main() { + late BaseRemoteLoggingConstraintProvider provider; + late FileStorage mockFileStorage; + late MockAWSHttpClient mockAWSHttpClient; + test('LoggingConstraint', () { + final sampleJsonMap = jsonDecode(sampleJson) as Map; + final loggingConstraint = LoggingConstraint.fromJson(sampleJsonMap); + expect( + loggingConstraint.toJson(), + sampleJsonMap, + ); + }); + + group('RemoteLoggingConstraintProvider', () { + setUp(() { + mockFileStorage = MockFileStorage(); + + registerFallbackValue(fakeRequest); + }); + + test('initializes loggingConstraint from endpoint', () async { + when(() => mockFileStorage.load(any())) + .thenAnswer((_) async => Future.value(sampleJson)); + + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(sampleJson), + ); + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 1200), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + await provider.ready; + + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(sampleJson)), + ); + }); + + test('uses local storage if endpoint fails', () async { + when(() => mockFileStorage.load(any())) + .thenAnswer((_) async => Future.value(sampleJson)); + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + return AWSHttpResponse( + statusCode: 400, + body: utf8.encode('NO RESPONSE'), + ); + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 10), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + await provider.ready; + + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(sampleJson)), + ); + }); + + test('updates constraints when endpoint returns updated constraints', + () async { + const updatedJson = ''' + { + "defaultLogLevel": "debug", + "categoryLogLevel": { + "API": "debug", + "AUTH": "error" + }, + "userLogLevel": { + "cognito-sub-xyz-123": { + "defaultLogLevel": "verbose", + "categoryLogLevel": { + "API": "error", + "AUTH": "debug" + } + } + } + } + '''; + // Mocking the endpoint to return updated constraints on the second call + var callCount = 0; + var callCount2 = 0; + when(() => mockFileStorage.load(any())).thenAnswer((_) async { + callCount2++; + if (callCount2 == 1) { + return Future.value(sampleJson); + } else { + return Future.value(updatedJson); + } + }); + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + callCount++; + if (callCount == 1) { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(sampleJson), + ); + } else { + return AWSHttpResponse( + statusCode: 200, + body: utf8.encode(updatedJson), + ); + } + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 1), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + // Wait for the provider to refresh the constraints and make the second call to the endpoint + await Future.delayed(const Duration(seconds: 2)); + + await provider.ready; + + expect( + provider.loggingConstraint!.toJson(), + equals(json.decode(updatedJson)), + ); + }); + + test( + 'fetches loggingConstraint from local storage and returns null if there are no constraints in local storage', + () async { + when(() => mockFileStorage.load(any())) + .thenAnswer((_) async => Future.value(null)); + when(() => mockFileStorage.save(any(), any())).thenAnswer((_) async {}); + mockAWSHttpClient = MockAWSHttpClient((request, _) { + return AWSHttpResponse( + statusCode: 400, + body: utf8.encode('NO RESPONSE'), + ); + }); + provider = BaseRemoteLoggingConstraintProvider.forTesting( + config: const DefaultRemoteConfiguration( + refreshInterval: Duration(seconds: 10), + endpoint: 'https://example.com', + region: 'us-west-2', + ), + fileStorage: mockFileStorage, + awsHttpClient: mockAWSHttpClient, + ); + + await provider.ready; + + expect(provider.loggingConstraint, equals(null)); + }); + }); +}