diff --git a/packages/at_persistence_secondary_server/lib/src/config/at_config.dart b/packages/at_persistence_secondary_server/lib/src/config/at_config.dart index 432d84f16..cb71d5c00 100644 --- a/packages/at_persistence_secondary_server/lib/src/config/at_config.dart +++ b/packages/at_persistence_secondary_server/lib/src/config/at_config.dart @@ -1,18 +1,19 @@ import 'dart:convert'; +import 'package:at_commons/at_commons.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; import 'package:at_persistence_secondary_server/src/config/configuration.dart'; import 'package:at_persistence_secondary_server/src/keystore/hive_keystore_helper.dart'; import 'package:at_utils/at_logger.dart'; import 'package:hive/hive.dart'; -/// Class to configure blocklist for atconnections. +/// Class to configure blocklist for atConnections. class AtConfig { var logger = AtSignLogger('AtConfig'); - ///stores 'Configuration' type under [configkey] in secondary. - String configKey = 'configKey'; - var keyStoreHelper = HiveKeyStoreHelper.getInstance(); + ///stores 'Configuration' type under [configKey] in secondary. + final oldConfigKey = HiveKeyStoreHelper.getInstance().prepareKey('configKey'); + late final String configKey; final String? _atSign; AtCommitLog? _commitLog; late HivePersistenceManager persistenceManager; @@ -25,20 +26,22 @@ class AtConfig { persistenceManager = SecondaryPersistenceStoreFactory.getInstance() .getSecondaryPersistenceStore(_atSign)! .getHivePersistenceManager()!; + configKey = HiveKeyStoreHelper.getInstance().prepareKey('private:blocklist$_atSign'); } - ///Returns 'success' on adding unique [data] into blocklist. - Future addToBlockList(Set data) async { - String result; + ///Returns 'success' on adding unique [blockList] into blocklist. + Future addToBlockList(Set blockList) async { + String? result; + if (blockList.isEmpty) { + throw IllegalArgumentException( + 'Provided list of atsigns to block is empty'); + } try { - assert(data.isNotEmpty); - var existingData = await get(configKey); - var blockList = await getBlockList(); - var uniqueBlockList = Set.from(blockList); - uniqueBlockList.addAll(data); + AtData? existingData = await _getExistingData(); + Set uniqueBlockList = await getBlockList(); + uniqueBlockList.addAll(blockList); var config = Configuration(List.from(uniqueBlockList)); result = await prepareAndStoreData(config, existingData); - return result; } on Exception catch (e) { throw DataStoreException( 'Exception adding to commit log:${e.toString()}'); @@ -46,40 +49,43 @@ class AtConfig { throw DataStoreException( 'Hive error adding to commit log:${e.toString()}'); } + return result; } - ///removes [data] from blocklist if satisfies basic conditions. - Future removeFromBlockList(Set data) async { + /// Removes [unblockAtsignsList] from blocklist if satisfies basic conditions. + Future removeFromBlockList(Set unblockAtsignsList) async { String? result; + if (unblockAtsignsList.isEmpty) { + throw IllegalArgumentException( + 'Provided list of atsigns to unblock is empty'); + } try { - assert(data.isNotEmpty); - var existingData = await get(configKey); - if (existingData != null) { - var blockList = await getBlockList(); - var config = Configuration( - List.from(Set.from(blockList).difference(Set.from(data)))); + var existingData = await _getExistingData(); + Set blockedAtsignsSet = await getBlockList(); + // remove the atsign in unblockAtsignList from the existing blockedAtsignsSet + if (blockedAtsignsSet.isNotEmpty) { + var config = Configuration(List.from( + blockedAtsignsSet.difference(Set.from(unblockAtsignsList)))); result = await prepareAndStoreData(config, existingData); } - return result; } on Exception catch (e) { throw DataStoreException( 'Exception adding to commit log:${e.toString()}'); } on HiveError catch (e) { - throw DataStoreException( - 'Hive error adding to commit log:${e.toString()}'); + throw DataStoreException('Hive error adding to commit log:${e.message}'); } + return result; } ///Returns blocklist by fetching from atsign's secondary. Future> getBlockList() async { - var result = {}; + var blockList = {}; try { - var existingData = await get(configKey); - if (existingData != null) { + var existingData = await _getExistingData(); + if (existingData != null && existingData.data != null) { var config = jsonDecode(existingData.data!); - result = Set.from(config['blockList']); + blockList = Set.from(config['blockList']); } - return result; } on Exception catch (e) { throw DataStoreException( 'Exception adding to commit log:${e.toString()}'); @@ -87,15 +93,14 @@ class AtConfig { throw DataStoreException( 'Hive error adding to commit log:${e.toString()}'); } + return blockList; } ///Returns [AtData] value for given [key]. Future get(String key) async { AtData? value; try { - var hiveKey = keyStoreHelper.prepareKey(key); - value = await (persistenceManager.getBox() as LazyBox).get(hiveKey); - return value; + value = await (persistenceManager.getBox() as LazyBox).get(key); } on Exception catch (exception) { logger.severe('HiveKeystore get exception: $exception'); throw DataStoreException('exception in get: ${exception.toString()}'); @@ -103,6 +108,7 @@ class AtConfig { logger.severe('HiveKeystore get error: $error'); throw DataStoreException(error.message); } + return value; } ///Returns 'true' if blocklist contains [atsign]. @@ -111,7 +117,6 @@ class AtConfig { try { var blockList = await getBlockList(); result = blockList.contains(atsign); - return result; } on Exception catch (e) { throw DataStoreException( 'Exception adding to commit log:${e.toString()}'); @@ -119,22 +124,62 @@ class AtConfig { throw DataStoreException( 'Hive error adding to commit log:${e.toString()}'); } + return result; } ///Returns 'success' after successfully persisting data into secondary. Future prepareAndStoreData(config, [existingData]) async { - String result; - configKey = keyStoreHelper.prepareKey(configKey); var newData = AtData(); newData.data = jsonEncode(config); - newData = keyStoreHelper.prepareDataForKeystoreOperation(newData, - existingAtData: existingData); + newData = HiveKeyStoreHelper.getInstance() + .prepareDataForKeystoreOperation(newData, existingAtData: existingData); logger.finest('Storing the config key:$configKey | Value: $newData'); await persistenceManager.getBox().put(configKey, newData); await _commitLog!.commit(configKey, CommitOp.UPDATE); - result = 'success'; - return result; + return 'success'; + } + + /// Fetches existing Config data from the keystore + /// + /// Tries fetching data with [configKey] which is the new config key + /// + /// For backward-compatability, if data could not be fetched with new key + /// tries fetching data with [oldConfigKey] + Future _getExistingData() async { + AtData? existingData; + try { + // try to fetch data using the new config-key format + existingData = await get(configKey); + } on KeyNotFoundException catch (e) { + logger.finer('Could not fetch data with NEW config-key | ${e.message}'); + } on Exception catch (e) { + logger.finer('Could not fetch data with NEW config-key | $e'); + rethrow; + } + if (existingData == null) { + // If data could not be fetched with the new config-key, try fetching the data + // using the old config-key and delete the old key from keystore + try { + existingData = await get(oldConfigKey); + if (existingData != null && existingData.data != null) { + AtData newAtData = AtData()..data = existingData.data; + HiveKeyStoreHelper.getInstance().prepareDataForKeystoreOperation( + newAtData, + existingAtData: existingData); + // store the existing data with the new key + await persistenceManager.getBox().put(configKey, newAtData); + logger.info('Successfully migrated configKey data to new key format'); + await persistenceManager.getBox().delete(oldConfigKey); + } + } on KeyNotFoundException catch (e) { + logger.finer('Could not fetch data with OLD config-key | ${e.message}'); + } on Exception catch (e) { + logger.finer('Could not fetch data with OLD config-key | $e'); + rethrow; + } + } + return existingData; } } diff --git a/packages/at_persistence_secondary_server/test/at_config_test.dart b/packages/at_persistence_secondary_server/test/at_config_test.dart index c65f49538..3fc81677a 100644 --- a/packages/at_persistence_secondary_server/test/at_config_test.dart +++ b/packages/at_persistence_secondary_server/test/at_config_test.dart @@ -1,74 +1,118 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:at_commons/at_commons.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; +import 'package:at_persistence_secondary_server/src/config/configuration.dart'; +import 'package:at_persistence_secondary_server/src/keystore/hive_keystore_helper.dart'; +import 'package:hive/hive.dart'; import 'package:test/test.dart'; +var storageDir = '${Directory.current.path}/test/hive'; void main() async { - var storageDir = '${Directory.current.path}/test/hive'; - setUp(() async => await setUpFunc(storageDir)); + group('Verify blocklist configuration behaviour', () { + setUp(() async => await setUpFunc(storageDir)); - test('test for adding data to blocklist', () async { - var data = {'@alice', '@bob'}; - var atConfigInstance = AtConfig( - await AtCommitLogManagerImpl.getInstance().getCommitLog('@test_user_1'), - '@test_user_1'); - var result = await atConfigInstance.addToBlockList(data); - expect(result, 'success'); - }); + test('test for adding data to blocklist', () async { + var atsignsToBeBlocked = {'@alice', '@bob'}; + var atConfigInstance = AtConfig( + await AtCommitLogManagerImpl.getInstance() + .getCommitLog('@test_user_1'), + '@test_user_1'); + var result = await atConfigInstance.addToBlockList(atsignsToBeBlocked); + expect(result, 'success'); + }); - test('test for fetching blocklist', () async { - var atConfigInstance = AtConfig( - await AtCommitLogManagerImpl.getInstance().getCommitLog('@test_user_1'), - '@test_user_1'); - var data = {'@alice', '@bob'}; - await atConfigInstance.addToBlockList(data); - var result = await atConfigInstance.getBlockList(); - expect(result, {'@alice', '@bob'}); - }, timeout: Timeout(Duration(minutes: 10))); + test('test for fetching blocklist', () async { + var atConfigInstance = AtConfig( + await AtCommitLogManagerImpl.getInstance() + .getCommitLog('@test_user_1'), + '@test_user_1'); + var atsignsToBeBlocked = {'@alice', '@bob'}; + await atConfigInstance.addToBlockList(atsignsToBeBlocked); + var result = await atConfigInstance.getBlockList(); + expect(result, atsignsToBeBlocked); + }); - test('test for removing blocklist data', () async { - var atConfigInstance = AtConfig( - await AtCommitLogManagerImpl.getInstance().getCommitLog('@test_user_1'), - '@test_user_1'); - var data = {'@alice', '@bob'}; - await atConfigInstance.addToBlockList(data); - var result = await atConfigInstance.removeFromBlockList(data); - expect(result, 'success'); - }); + test('test for removing blocklist data', () async { + var atConfigInstance = AtConfig( + await AtCommitLogManagerImpl.getInstance() + .getCommitLog('@test_user_1'), + '@test_user_1'); + var atsignsToBeBlocked = {'@alice', '@bob', '@charlie'}; + await atConfigInstance.addToBlockList(atsignsToBeBlocked); + var atsignsToBeUnblocked = {'@alice', '@bob'}; + var result = + await atConfigInstance.removeFromBlockList(atsignsToBeUnblocked); + expect(result, 'success'); + // get block list + var blockList = await atConfigInstance.getBlockList(); + expect(blockList, {'@charlie'}); + }); - test('test for removing non existing data from blocklist', () async { - var data = {'@alice', '@bob'}; - var atConfigInstance = AtConfig( - await AtCommitLogManagerImpl.getInstance().getCommitLog('@test_user_1'), - '@test_user_1'); - await atConfigInstance.addToBlockList(data); - var removeData = {'@colin'}; - var result = await atConfigInstance.removeFromBlockList(removeData); - expect(result, 'success'); - }); + test('test for removing non existing data from blocklist', () async { + var data = {'@alice', '@bob'}; + var atConfigInstance = AtConfig( + await AtCommitLogManagerImpl.getInstance() + .getCommitLog('@test_user_1'), + '@test_user_1'); + await atConfigInstance.addToBlockList(data); + var removeData = {'@colin'}; + var result = await atConfigInstance.removeFromBlockList(removeData); + expect(result, 'success'); + }); - test('test for removing empty data', () async { - var removeData = {}; - var atConfigInstance = AtConfig( - await AtCommitLogManagerImpl.getInstance().getCommitLog('@test_user_1'), - '@test_user_1'); - expect(() async => await atConfigInstance.removeFromBlockList(removeData), - throwsA(predicate((dynamic e) => e is AssertionError))); - }); + test('test for removing empty data', () async { + var removeData = {}; + var atConfigInstance = AtConfig( + await AtCommitLogManagerImpl.getInstance() + .getCommitLog('@test_user_1'), + '@test_user_1'); + expect(() async => await atConfigInstance.removeFromBlockList(removeData), + throwsA(predicate((dynamic e) => e is IllegalArgumentException))); + }); - test('test for removing null data', () async { - var atConfigInstance = AtConfig( - await AtCommitLogManagerImpl.getInstance().getCommitLog('@test_user_1'), - '@test_user_1'); - expect(() async => await atConfigInstance.removeFromBlockList({}), - throwsA(predicate((dynamic e) => e is AssertionError))); - }); + test('test for removing null data', () async { + var atConfigInstance = AtConfig( + await AtCommitLogManagerImpl.getInstance() + .getCommitLog('@test_user_1'), + '@test_user_1'); + expect(() async => await atConfigInstance.removeFromBlockList({}), + throwsA(predicate((dynamic e) => e is IllegalArgumentException))); + }); + + // Manually insert block-list into keystore under the old config-key + // Successfully fetch block-list with new config-key indicating that the code + // is backwards compatible + // Verify that the old-config key has been deleted + test('verify backwards compatibility of blocklist with new config-key', + () async { + AtConfig atConfig = AtConfig( + await AtCommitLogManagerImpl.getInstance() + .getCommitLog('@test_user_1'), + '@test_user_1'); + LazyBox box = atConfig.persistenceManager.getBox() as LazyBox; + List blockedAtsigns = [ + '@blocked_user_1', + '@blocked_user_2', + '@blocked_user_3' + ]; + var blockedConfig = Configuration(blockedAtsigns); + AtData atData = AtData()..data = jsonEncode(blockedConfig); + atData = HiveKeyStoreHelper.getInstance() + .prepareDataForKeystoreOperation(atData); + await box.put(atConfig.oldConfigKey, atData); + // fetch the data that has been put into the keystore using the new config key + var blockList = await atConfig.getBlockList(); + expect(blockList.toList(), blockedAtsigns); + // verify that the new config key has been put into the keystore + expect(box.containsKey(atConfig.configKey), true); + // verify that the oldConfigKey has been deleted + expect(box.containsKey(atConfig.oldConfigKey), false); + }); - try { tearDown(() async => await tearDownFunc()); - } on Exception catch (e) { - print('error in tear down:${e.toString()}'); - } + }); } Future setUpFunc(storageDir) async { @@ -79,7 +123,8 @@ Future setUpFunc(storageDir) async { var persistenceManager = secondaryPersistenceStore.getHivePersistenceManager()!; await persistenceManager.init(storageDir); -// persistenceManager.scheduleKeyExpireTask(1); //commented this line for coverage test + // commented this line for coverage test + // persistenceManager.scheduleKeyExpireTask(1); var hiveKeyStore = secondaryPersistenceStore.getSecondaryKeyStore()!; hiveKeyStore.commitLog = commitLogInstance; var keyStoreManager = @@ -89,8 +134,13 @@ Future setUpFunc(storageDir) async { } Future tearDownFunc() async { + // closes the instance of hive keystore + await SecondaryPersistenceStoreFactory.getInstance() + .getSecondaryPersistenceStore('@test_user_1')! + .getHivePersistenceManager()?.close(); + var isExists = await Directory('test/hive/').exists(); if (isExists) { - Directory('test/hive/').deleteSync(recursive: true); + await Directory('test/hive/').delete(recursive: true); } } diff --git a/packages/at_secondary_server/lib/src/verb/handler/config_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/config_verb_handler.dart index a069c3516..f88302079 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/config_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/config_verb_handler.dart @@ -47,104 +47,94 @@ class ConfigVerbHandler extends AbstractVerbHandler { Response response, HashMap verbParams, InboundConnection atConnection) async { - try { - var currentAtSign = AtSecondaryServerImpl.getInstance().currentAtSign; - atConfigInstance = AtConfig( - await AtCommitLogManagerImpl.getInstance() - .getCommitLog(currentAtSign), - currentAtSign); - dynamic result; - var operation = verbParams[AT_OPERATION]; - var atsigns = verbParams[AT_SIGN]; - String? setOperation = verbParams[SET_OPERATION]; + var currentAtSign = AtSecondaryServerImpl.getInstance().currentAtSign; + atConfigInstance = AtConfig( + await AtCommitLogManagerImpl.getInstance().getCommitLog(currentAtSign), + currentAtSign); + dynamic result; + var operation = verbParams[AtConstants.atOperation]; + var atsigns = verbParams[AtConstants.atSign]; + String? setOperation = verbParams[AtConstants.setOperation]; - if (operation != null) { - switch (operation) { - case 'show': - var blockList = await atConfigInstance.getBlockList(); - result = (blockList.isNotEmpty) ? _toJsonResponse(blockList) : null; - break; - case 'add': - var nonCurrentAtSignList = - _retainNonCurrentAtsign(currentAtSign, atsigns!); - if (nonCurrentAtSignList.isNotEmpty) { - result = - await atConfigInstance.addToBlockList(nonCurrentAtSignList); - } - - ///if list contains only currentAtSign - else { - result = 'success'; - } - break; - case 'remove': + if (operation != null) { + switch (operation) { + case 'show': + var blockList = await atConfigInstance.getBlockList(); + result = (blockList.isNotEmpty) ? _toJsonResponse(blockList) : null; + break; + case 'add': + var nonCurrentAtSignList = + _retainNonCurrentAtsign(currentAtSign, atsigns!); + if (nonCurrentAtSignList.isNotEmpty) { result = - await atConfigInstance.removeFromBlockList(_toSet(atsigns!)); - break; - default: - result = 'unknown operation'; - break; - } + await atConfigInstance.addToBlockList(nonCurrentAtSignList); + } else { + ///if list contains only currentAtSign + result = 'success'; + } + break; + case 'remove': + result = await atConfigInstance.removeFromBlockList(_toSet(atsigns!)); + break; + default: + result = 'unknown operation'; + break; + } + } else { + //in case of config:set the config input received is in the form of 'config=value'. The below if condition splits that and separates config name and config value + if (setOperation == 'set') { + //split 'config=value' to array of strings + var newConfig = verbParams[AtConstants.configNew]?.split('='); + //first element of array is config name + setConfigName = ModifiableConfigs.values.byName(newConfig![0]); + //second element of array is config value + setConfigValue = newConfig[1]; } else { - //in case of config:set the config input received is in the form of 'config=value'. The below if condition splits that and separates config name and config value - if (setOperation == 'set') { - //split 'config=value' to array of strings - var newConfig = verbParams[CONFIG_NEW]?.split('='); - //first element of array is config name - setConfigName = ModifiableConfigs.values.byName(newConfig![0]); - //second element of array is config value - setConfigValue = newConfig[1]; - } else { - //in other cases reset/print only config name is received - setConfigName = - ModifiableConfigs.values.byName(verbParams[CONFIG_NEW]!); - } + //in other cases reset/print only config name is received + setConfigName = + ModifiableConfigs.values.byName(verbParams[AtConstants.configNew]!); + } - //implementation for config:set - switch (setOperation) { - case 'set': - if (AtSecondaryConfig.testingMode) { - //broadcast new config change - try { - AtSecondaryConfig.broadcastConfigChange( - setConfigName!, int.parse(setConfigValue!)); - } catch (e) { - AtSecondaryConfig.broadcastConfigChange( - setConfigName!, setConfigValue!); - } - result = 'ok'; - } else { - result = 'testing mode disabled by default'; - } - break; - case 'reset': - if (AtSecondaryConfig.testingMode) { - //broadcast reset - AtSecondaryConfig.broadcastConfigChange(setConfigName!, null, - isReset: true); - result = 'ok'; - } else { - result = 'testing mode disabled by default'; - } - break; - case 'print': - if (AtSecondaryConfig.testingMode) { - result = AtSecondaryConfig.getLatestConfigValue(setConfigName!); - } else { - result = 'testing mode disabled by default'; + //implementation for config:set + switch (setOperation) { + case 'set': + if (AtSecondaryConfig.testingMode) { + //broadcast new config change + try { + AtSecondaryConfig.broadcastConfigChange( + setConfigName!, int.parse(setConfigValue!)); + } catch (e) { + AtSecondaryConfig.broadcastConfigChange( + setConfigName!, setConfigValue!); } - break; - default: - result = 'invalid setOperation'; - break; - } + result = 'ok'; + } else { + result = 'testing mode disabled by default'; + } + break; + case 'reset': + if (AtSecondaryConfig.testingMode) { + //broadcast reset + AtSecondaryConfig.broadcastConfigChange(setConfigName!, null, + isReset: true); + result = 'ok'; + } else { + result = 'testing mode disabled by default'; + } + break; + case 'print': + if (AtSecondaryConfig.testingMode) { + result = AtSecondaryConfig.getLatestConfigValue(setConfigName!); + } else { + result = 'testing mode disabled by default'; + } + break; + default: + result = 'invalid setOperation'; + break; } - response.data = result?.toString(); - } catch (exception) { - response.isError = true; - response.errorMessage = exception.toString(); - rethrow; } + response.data = result?.toString(); } }