diff --git a/lib/src/commands/create/create_app_command.dart b/lib/src/commands/create/create_app_command.dart index 4fd9b8c..76d0f1d 100644 --- a/lib/src/commands/create/create_app_command.dart +++ b/lib/src/commands/create/create_app_command.dart @@ -15,7 +15,7 @@ import 'package:stacked_cli/src/services/template_service.dart'; import 'package:stacked_cli/src/templates/template_constants.dart'; class CreateAppCommand extends Command { - final _cLog = locator(); + final _log = locator(); final _configService = locator(); final _fileService = locator(); final _processService = locator(); @@ -57,14 +57,16 @@ class CreateAppCommand extends Command { argParser.addOption( ksConfigPath, abbr: 'c', - help: kCommandHelpCreateAppConfigFile, + help: kCommandHelpConfigFilePath, ); } @override Future run() async { try { - await _configService.loadConfig(path: argResults![ksConfigPath]); + await _configService.findAndLoadConfigFile( + configFilePath: argResults![ksConfigPath], + ); final appName = argResults!.rest.first; final appNameWithoutPath = appName.split('/').last; @@ -74,7 +76,7 @@ class CreateAppCommand extends Command { _processService.formattingLineLength = argResults![ksLineLength]; await _processService.runCreateApp(appName: appName); - _cLog.stackedOutput(message: 'Add Stacked Magic ... ', isBold: true); + _log.stackedOutput(message: 'Add Stacked Magic ... ', isBold: true); await _templateService.renderTemplate( templateName: name, @@ -91,7 +93,7 @@ class CreateAppCommand extends Command { await _processService.runFormat(appName: appName); await _clean(appName: appName); } catch (e) { - _cLog.warn(message: e.toString()); + _log.warn(message: e.toString()); } } @@ -100,13 +102,17 @@ class CreateAppCommand extends Command { /// - Deletes widget_test.dart file /// - Removes unused imports Future _clean({required String appName}) async { - _cLog.stackedOutput(message: 'Cleaning project...'); + _log.stackedOutput(message: 'Cleaning project...'); // Removes `widget_test` file to avoid failing unit tests on created app - await _fileService.deleteFile( + if (await _fileService.fileExists( filePath: '$appName/test/widget_test.dart', - verbose: false, - ); + )) { + await _fileService.deleteFile( + filePath: '$appName/test/widget_test.dart', + verbose: false, + ); + } // Analyze the project and return output lines final issues = await _processService.runAnalyze(appName: appName); @@ -122,7 +128,7 @@ class CreateAppCommand extends Command { ); } - _cLog.stackedOutput(message: 'Project cleaned.'); + _log.stackedOutput(message: 'Project cleaned.'); } /// Replaces configuration file in the project created. diff --git a/lib/src/commands/create/create_bottom_sheet_command.dart b/lib/src/commands/create/create_bottom_sheet_command.dart index 44d4a89..b1f7ff7 100644 --- a/lib/src/commands/create/create_bottom_sheet_command.dart +++ b/lib/src/commands/create/create_bottom_sheet_command.dart @@ -6,6 +6,7 @@ import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; import 'package:stacked_cli/src/services/analytics_service.dart'; +import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; @@ -13,6 +14,7 @@ import 'package:stacked_cli/src/services/template_service.dart'; import 'package:stacked_cli/src/templates/template_constants.dart'; class CreateBottomSheetCommand extends Command with ProjectStructureValidator { + final _log = locator(); final _configService = locator(); final _processService = locator(); final _pubspecService = locator(); @@ -53,29 +55,44 @@ class CreateBottomSheetCommand extends Command with ProjectStructureValidator { defaultsTo: 'empty', help: kCommandHelpCreateBottomSheetTemplate, ); + + argParser.addOption( + ksConfigPath, + abbr: 'c', + help: kCommandHelpConfigFilePath, + ); } @override Future run() async { - final bottomSheetName = argResults!.rest.first; - final templateType = argResults![ksTemplateType]; - unawaited(_analyticsService.createBottomSheetEvent(name: bottomSheetName)); - final outputPath = argResults!.rest.length > 1 ? argResults!.rest[1] : null; - await _configService.loadConfig(path: outputPath); - _processService.formattingLineLength = argResults![ksLineLength]; - await _pubspecService.initialise(workingDirectory: outputPath); - await validateStructure(outputPath: outputPath); + try { + final bottomSheetName = argResults!.rest.first; + final templateType = argResults![ksTemplateType]; + unawaited( + _analyticsService.createBottomSheetEvent(name: bottomSheetName)); + final outputPath = + argResults!.rest.length > 1 ? argResults!.rest[1] : null; + await _configService.composeAndLoadConfigFile( + configFilePath: argResults![ksConfigPath], + projectPath: outputPath, + ); + _processService.formattingLineLength = argResults![ksLineLength]; + await _pubspecService.initialise(workingDirectory: outputPath); + await validateStructure(outputPath: outputPath); - await _templateService.renderTemplate( - templateName: name, - name: bottomSheetName, - outputPath: outputPath, - verbose: true, - excludeRoute: argResults![ksExcludeRoute], - hasModel: argResults![ksModel], - templateType: templateType, - ); + await _templateService.renderTemplate( + templateName: name, + name: bottomSheetName, + outputPath: outputPath, + verbose: true, + excludeRoute: argResults![ksExcludeRoute], + hasModel: argResults![ksModel], + templateType: templateType, + ); - await _processService.runBuildRunner(appName: outputPath); + await _processService.runBuildRunner(appName: outputPath); + } catch (e) { + _log.warn(message: e.toString()); + } } } diff --git a/lib/src/commands/create/create_dialog_command.dart b/lib/src/commands/create/create_dialog_command.dart index b0266a4..14d5881 100644 --- a/lib/src/commands/create/create_dialog_command.dart +++ b/lib/src/commands/create/create_dialog_command.dart @@ -6,6 +6,7 @@ import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; import 'package:stacked_cli/src/services/analytics_service.dart'; +import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; @@ -13,6 +14,7 @@ import 'package:stacked_cli/src/services/template_service.dart'; import 'package:stacked_cli/src/templates/template_constants.dart'; class CreateDialogCommand extends Command with ProjectStructureValidator { + final _log = locator(); final _configService = locator(); final _processService = locator(); final _pubspecService = locator(); @@ -53,29 +55,43 @@ class CreateDialogCommand extends Command with ProjectStructureValidator { defaultsTo: 'empty', help: kCommandHelpCreateDialogTemplate, ); + + argParser.addOption( + ksConfigPath, + abbr: 'c', + help: kCommandHelpConfigFilePath, + ); } @override Future run() async { - final dialogName = argResults!.rest.first; - final templateType = argResults![ksTemplateType]; - unawaited(_analyticsService.createDialogEvent(name: dialogName)); - final outputPath = argResults!.rest.length > 1 ? argResults!.rest[1] : null; - await _configService.loadConfig(path: outputPath); - _processService.formattingLineLength = argResults![ksLineLength]; - await _pubspecService.initialise(workingDirectory: outputPath); - await validateStructure(outputPath: outputPath); + try { + final dialogName = argResults!.rest.first; + final templateType = argResults![ksTemplateType]; + unawaited(_analyticsService.createDialogEvent(name: dialogName)); + final outputPath = + argResults!.rest.length > 1 ? argResults!.rest[1] : null; + await _configService.composeAndLoadConfigFile( + configFilePath: argResults![ksConfigPath], + projectPath: outputPath, + ); + _processService.formattingLineLength = argResults![ksLineLength]; + await _pubspecService.initialise(workingDirectory: outputPath); + await validateStructure(outputPath: outputPath); - await _templateService.renderTemplate( - templateName: name, - name: dialogName, - outputPath: outputPath, - verbose: true, - excludeRoute: argResults![ksExcludeRoute], - hasModel: argResults![ksModel], - templateType: templateType, - ); + await _templateService.renderTemplate( + templateName: name, + name: dialogName, + outputPath: outputPath, + verbose: true, + excludeRoute: argResults![ksExcludeRoute], + hasModel: argResults![ksModel], + templateType: templateType, + ); - await _processService.runBuildRunner(appName: outputPath); + await _processService.runBuildRunner(appName: outputPath); + } catch (e) { + _log.warn(message: e.toString()); + } } } diff --git a/lib/src/commands/create/create_service_command.dart b/lib/src/commands/create/create_service_command.dart index 72d9c9d..82d17a1 100644 --- a/lib/src/commands/create/create_service_command.dart +++ b/lib/src/commands/create/create_service_command.dart @@ -6,6 +6,7 @@ import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; import 'package:stacked_cli/src/services/analytics_service.dart'; +import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; @@ -13,6 +14,7 @@ import 'package:stacked_cli/src/services/template_service.dart'; import 'package:stacked_cli/src/templates/template_constants.dart'; class CreateServiceCommand extends Command with ProjectStructureValidator { + final _log = locator(); final _configService = locator(); final _processService = locator(); final _pubspecService = locator(); @@ -48,27 +50,41 @@ class CreateServiceCommand extends Command with ProjectStructureValidator { defaultsTo: 'empty', help: kCommandHelpCreateAppTemplate, ); + + argParser.addOption( + ksConfigPath, + abbr: 'c', + help: kCommandHelpConfigFilePath, + ); } @override Future run() async { - final serviceName = argResults!.rest.first; - final templateType = argResults![ksTemplateType]; - unawaited(_analyticsService.createServiceEvent(name: serviceName)); - final outputPath = argResults!.rest.length > 1 ? argResults!.rest[1] : null; - await _configService.loadConfig(path: outputPath); - _processService.formattingLineLength = argResults?[ksLineLength]; - await _pubspecService.initialise(workingDirectory: outputPath); - await validateStructure(outputPath: outputPath); + try { + final serviceName = argResults!.rest.first; + final templateType = argResults![ksTemplateType]; + unawaited(_analyticsService.createServiceEvent(name: serviceName)); + final outputPath = + argResults!.rest.length > 1 ? argResults!.rest[1] : null; + await _configService.composeAndLoadConfigFile( + configFilePath: argResults![ksConfigPath], + projectPath: outputPath, + ); + _processService.formattingLineLength = argResults?[ksLineLength]; + await _pubspecService.initialise(workingDirectory: outputPath); + await validateStructure(outputPath: outputPath); - await _templateService.renderTemplate( - templateName: name, - name: serviceName, - outputPath: outputPath, - verbose: true, - excludeRoute: argResults![ksExcludeDependency], - templateType: templateType, - ); - await _processService.runBuildRunner(appName: outputPath); + await _templateService.renderTemplate( + templateName: name, + name: serviceName, + outputPath: outputPath, + verbose: true, + excludeRoute: argResults![ksExcludeDependency], + templateType: templateType, + ); + await _processService.runBuildRunner(appName: outputPath); + } catch (e) { + _log.warn(message: e.toString()); + } } } diff --git a/lib/src/commands/create/create_view_command.dart b/lib/src/commands/create/create_view_command.dart index 7832d2f..69e51d5 100644 --- a/lib/src/commands/create/create_view_command.dart +++ b/lib/src/commands/create/create_view_command.dart @@ -6,6 +6,7 @@ import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; import 'package:stacked_cli/src/services/analytics_service.dart'; +import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; @@ -13,6 +14,7 @@ import 'package:stacked_cli/src/services/template_service.dart'; import 'package:stacked_cli/src/templates/template_constants.dart'; class CreateViewCommand extends Command with ProjectStructureValidator { + final _log = locator(); final _configService = locator(); final _processService = locator(); final _pubspecService = locator(); @@ -53,34 +55,48 @@ class CreateViewCommand extends Command with ProjectStructureValidator { allowed: ['empty', 'web'], help: kCommandHelpCreateViewTemplate, ); + + argParser.addOption( + ksConfigPath, + abbr: 'c', + help: kCommandHelpConfigFilePath, + ); } @override Future run() async { - final viewName = argResults!.rest.first; - var templateType = argResults![ksTemplateType] as String?; - unawaited(_analyticsService.createViewEvent(name: viewName)); - final outputPath = argResults!.rest.length > 1 ? argResults!.rest[1] : null; - await _configService.loadConfig(path: outputPath); - _processService.formattingLineLength = argResults![ksLineLength]; - await _pubspecService.initialise(workingDirectory: outputPath); - await validateStructure(outputPath: outputPath); + try { + final viewName = argResults!.rest.first; + var templateType = argResults![ksTemplateType] as String?; + unawaited(_analyticsService.createViewEvent(name: viewName)); + final outputPath = + argResults!.rest.length > 1 ? argResults!.rest[1] : null; + await _configService.composeAndLoadConfigFile( + configFilePath: argResults![ksConfigPath], + projectPath: outputPath, + ); + _processService.formattingLineLength = argResults![ksLineLength]; + await _pubspecService.initialise(workingDirectory: outputPath); + await validateStructure(outputPath: outputPath); - // Determine which template to use with the following rules: - // 1. If the template is supplied we use that template - // 2. If the template is null use config web to decide - print('templateType:$templateType preferWeb:${_configService.preferWeb}'); - templateType ??= _configService.preferWeb ? 'web' : 'empty'; + // Determine which template to use with the following rules: + // 1. If the template is supplied we use that template + // 2. If the template is null use config web to decide + print('templateType:$templateType preferWeb:${_configService.preferWeb}'); + templateType ??= _configService.preferWeb ? 'web' : 'empty'; - await _templateService.renderTemplate( - templateName: name, - name: viewName, - outputPath: outputPath, - verbose: true, - excludeRoute: argResults![ksExcludeRoute], - useBuilder: argResults![ksV1] ?? _configService.v1, - templateType: templateType, - ); - await _processService.runBuildRunner(appName: outputPath); + await _templateService.renderTemplate( + templateName: name, + name: viewName, + outputPath: outputPath, + verbose: true, + excludeRoute: argResults![ksExcludeRoute], + useBuilder: argResults![ksV1] ?? _configService.v1, + templateType: templateType, + ); + await _processService.runBuildRunner(appName: outputPath); + } catch (e) { + _log.warn(message: e.toString()); + } } } diff --git a/lib/src/commands/delete/delete_service_command.dart b/lib/src/commands/delete/delete_service_command.dart index 695a949..ec5fcb6 100644 --- a/lib/src/commands/delete/delete_service_command.dart +++ b/lib/src/commands/delete/delete_service_command.dart @@ -41,6 +41,12 @@ class DeleteServiceCommand extends Command with ProjectStructureValidator { help: 'The length of the line that is used for formatting', valueHelp: '80', ); + + argParser.addOption( + ksConfigPath, + abbr: 'c', + help: kCommandHelpConfigFilePath, + ); } @override @@ -49,7 +55,10 @@ class DeleteServiceCommand extends Command with ProjectStructureValidator { name: argResults!.rest.first, )); final outputPath = argResults!.rest.length > 1 ? argResults!.rest[1] : null; - await _configService.loadConfig(path: outputPath); + await _configService.composeAndLoadConfigFile( + configFilePath: argResults![ksConfigPath], + projectPath: outputPath, + ); _processService.formattingLineLength = argResults?[ksLineLength]; await _pubspecService.initialise(workingDirectory: outputPath); await validateStructure(outputPath: outputPath); @@ -105,8 +114,9 @@ class DeleteServiceCommand extends Command with ProjectStructureValidator { outputFolder: outputPath, ); await _fileService.removeSpecificFileLines( - filePath: filePath, - removedContent: argResults!.rest.first, - type: kTemplateNameService); + filePath: filePath, + removedContent: argResults!.rest.first, + type: kTemplateNameService, + ); } } diff --git a/lib/src/commands/delete/delete_view_commad.dart b/lib/src/commands/delete/delete_view_commad.dart index 998a4fe..5e5222e 100644 --- a/lib/src/commands/delete/delete_view_commad.dart +++ b/lib/src/commands/delete/delete_view_commad.dart @@ -41,13 +41,22 @@ class DeleteViewCommand extends Command with ProjectStructureValidator { help: 'The length of the line that is used for formatting', valueHelp: '80', ); + + argParser.addOption( + ksConfigPath, + abbr: 'c', + help: kCommandHelpConfigFilePath, + ); } @override Future run() async { unawaited(_analyticsService.deleteViewEvent(name: argResults!.rest.first)); final outputPath = argResults!.rest.length > 1 ? argResults!.rest[1] : null; - await _configService.loadConfig(path: outputPath); + await _configService.composeAndLoadConfigFile( + configFilePath: argResults![ksConfigPath], + projectPath: outputPath, + ); _processService.formattingLineLength = argResults?[ksLineLength]; await _pubspecService.initialise(workingDirectory: outputPath); await validateStructure(outputPath: outputPath); diff --git a/lib/src/constants/message_constants.dart b/lib/src/constants/message_constants.dart index 98dc1d8..7be67ce 100644 --- a/lib/src/constants/message_constants.dart +++ b/lib/src/constants/message_constants.dart @@ -44,7 +44,7 @@ const String kCommandHelpLineLength = const String kCommandHelpCreateAppTemplate = 'Selects the type of starter template to use when creating a new app. One oriented for mobile first or web first'; -const String kCommandHelpCreateAppConfigFile = +const String kCommandHelpConfigFilePath = 'Sets the file path for the custom config'; const String kCommandHelpCreateViewTemplate = @@ -69,7 +69,7 @@ const String kConfigFileNotFound = 'No configuration file found. Default Stacked values will be used.'; const String kConfigFileNotFoundRetry = - 'No configuration file found. Please, correct the config path passed as argument.'; + 'No configuration file found. Please, verify the config path passed as argument.'; const String kConfigFileMalformed = 'Your configuration file is malformed. Double check to make sure you have properly formatted json.'; diff --git a/lib/src/services/config_service.dart b/lib/src/services/config_service.dart index e083412..32c4e11 100644 --- a/lib/src/services/config_service.dart +++ b/lib/src/services/config_service.dart @@ -99,6 +99,73 @@ class ConfigService { /// Returns boolean to indicate if the project prefers web templates bool get preferWeb => _customConfig.preferWeb; + /// Finds configuration file and loads it into memory. + /// + /// Generally used when creating an app to determine the configuration to + /// export to the project. + Future findAndLoadConfigFile({ + String? configFilePath, + }) async { + try { + final configPath = await resolveConfigFile( + configFilePath: configFilePath, + ); + + await loadConfig(configPath); + } on ConfigFileNotFoundException catch (e, s) { + if (e.shouldHaltCommand) rethrow; + + _log.warn(message: e.message); + _analyticsService.logExceptionEvent( + level: Level.warning, + runtimeType: e.runtimeType.toString(), + message: e.message, + stackTrace: s.toString(), + ); + } catch (e, s) { + _log.error(message: e.toString()); + _analyticsService.logExceptionEvent( + runtimeType: e.runtimeType.toString(), + message: e.toString(), + stackTrace: s.toString(), + ); + } + } + + /// Composes configuration file and loads it into memory. + /// + /// Generally used to load the configuration file at root of the project. + Future composeAndLoadConfigFile({ + String? configFilePath, + String? projectPath, + }) async { + try { + final configPath = await composeConfigFile( + configFilePath: configFilePath, + projectPath: projectPath, + ); + + await loadConfig(configPath); + } on ConfigFileNotFoundException catch (e, s) { + if (e.shouldHaltCommand) rethrow; + + _log.warn(message: e.message); + _analyticsService.logExceptionEvent( + level: Level.warning, + runtimeType: e.runtimeType.toString(), + message: e.message, + stackTrace: s.toString(), + ); + } catch (e, s) { + _log.error(message: e.toString()); + _analyticsService.logExceptionEvent( + runtimeType: e.runtimeType.toString(), + message: e.toString(), + stackTrace: s.toString(), + ); + } + } + /// Resolves configuration file path. /// /// Looks for the configuration file in different locations depending their @@ -109,16 +176,16 @@ class ConfigService { /// - $path /// - $XDG_CONFIG_HOME/stacked/stacked.json @visibleForTesting - Future resolveConfigFile({String? path}) async { - if (path != null) { - if (await _fileService.fileExists(filePath: path)) { - return path; + Future resolveConfigFile({String? configFilePath}) async { + if (configFilePath != null) { + if (!await _fileService.fileExists(filePath: configFilePath)) { + throw ConfigFileNotFoundException( + kConfigFileNotFoundRetry, + shouldHaltCommand: true, + ); } - throw ConfigFileNotFoundException( - kConfigFileNotFoundRetry, - shouldHaltCommand: true, - ); + return configFilePath; } try { @@ -131,18 +198,46 @@ class ConfigService { // This error is Thrown when HOME environment variable is not set. } - return null; + throw ConfigFileNotFoundException(kConfigFileNotFound); } - /// Reads configuration file and sets data to [_customConfig] map. - Future loadConfig({String? path}) async { - try { - final configPath = await resolveConfigFile(path: path); - if (configPath == null) { - throw ConfigFileNotFoundException(kConfigFileNotFound); + /// Returns configuration file path. + /// + /// When configFilePath is NOT null should returns configFilePath unless the + /// file does NOT exists where should throw a ConfigFileNotFoundException. + /// + /// When configFilePath is null should returns [kConfigFileName] or with the + /// []projectPath] included if it was passed through arguments. + @visibleForTesting + Future composeConfigFile({ + String? configFilePath, + String? projectPath, + }) async { + if (configFilePath != null) { + if (await _fileService.fileExists(filePath: configFilePath)) { + return configFilePath; } - final data = await _fileService.readFileAsString(filePath: configPath); + throw ConfigFileNotFoundException( + kConfigFileNotFoundRetry, + shouldHaltCommand: true, + ); + } + + if (projectPath != null) { + return '$projectPath/$kConfigFileName'; + } + + return kConfigFileName; + } + + /// Reads configuration file and sets data to [_customConfig] map. + @visibleForTesting + Future loadConfig(String configFilePath) async { + try { + final data = await _fileService.readFileAsString( + filePath: configFilePath, + ); _customConfig = Config.fromJson(jsonDecode(data)); _hasCustomConfig = true; _sanitizeCustomConfig(); diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart index 86fb813..67ab8fc 100644 --- a/test/helpers/test_helpers.dart +++ b/test/helpers/test_helpers.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stacked_cli/src/constants/message_constants.dart'; +import 'package:stacked_cli/src/exceptions/config_file_not_found_exception.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; @@ -37,6 +39,8 @@ MockFileService getAndRegisterFileService({ int retryUntilFileExists = 0, String readFileResult = 'file_content', List getFilesInDirectoryResult = const [], + bool throwStateError = false, + bool throwConfigFileNotFoundException = false, }) { _removeRegistrationIfExists(); final service = MockFileService(); @@ -49,11 +53,25 @@ MockFileService getAndRegisterFileService({ return Future.value(false); } + if (throwStateError) { + throw StateError; + } + return Future.value(fileExistsResult); }); - when(service.readFileAsString(filePath: anyNamed('filePath'))) - .thenAnswer((realInvocation) => Future.value(readFileResult)); + when(service.readFileAsString(filePath: anyNamed('filePath'))).thenAnswer(( + realInvocation, + ) { + if (throwConfigFileNotFoundException) { + throw ConfigFileNotFoundException( + kConfigFileNotFoundRetry, + shouldHaltCommand: true, + ); + } + + return Future.value(readFileResult); + }); when(service.getFilesInDirectory(directoryPath: anyNamed('directoryPath'))) .thenAnswer((realInvocation) => Future.value(getFilesInDirectoryResult)); @@ -161,7 +179,9 @@ MockConfigService getAndRegisterConfigService({ (invocation) => customPath ?? invocation.positionalArguments[0], ); - when(service.resolveConfigFile(path: anyNamed('path'))).thenAnswer( + when(service.resolveConfigFile( + configFilePath: anyNamed('configFilePath'), + )).thenAnswer( (invocation) => customPath ?? invocation.namedArguments[0], ); diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart index 0aaa27b..94192a5 100644 --- a/test/helpers/test_helpers.mocks.dart +++ b/test/helpers/test_helpers.mocks.dart @@ -1002,21 +1002,66 @@ class MockConfigService extends _i1.Mock implements _i15.ConfigService { returnValueForMissingStub: false, ) as bool); @override - _i6.Future resolveConfigFile({String? path}) => (super.noSuchMethod( + _i6.Future findAndLoadConfigFile({String? configFilePath}) => + (super.noSuchMethod( + Invocation.method( + #findAndLoadConfigFile, + [], + {#configFilePath: configFilePath}, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future composeAndLoadConfigFile({ + String? configFilePath, + String? projectPath, + }) => + (super.noSuchMethod( + Invocation.method( + #composeAndLoadConfigFile, + [], + { + #configFilePath: configFilePath, + #projectPath: projectPath, + }, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future resolveConfigFile({String? configFilePath}) => + (super.noSuchMethod( Invocation.method( #resolveConfigFile, [], - {#path: path}, + {#configFilePath: configFilePath}, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i6.Future.value(''), + returnValueForMissingStub: _i6.Future.value(''), + ) as _i6.Future); @override - _i6.Future loadConfig({String? path}) => (super.noSuchMethod( + _i6.Future composeConfigFile({ + String? configFilePath, + String? projectPath, + }) => + (super.noSuchMethod( Invocation.method( - #loadConfig, + #composeConfigFile, [], - {#path: path}, + { + #configFilePath: configFilePath, + #projectPath: projectPath, + }, + ), + returnValue: _i6.Future.value(''), + returnValueForMissingStub: _i6.Future.value(''), + ) as _i6.Future); + @override + _i6.Future loadConfig(String? configFilePath) => (super.noSuchMethod( + Invocation.method( + #loadConfig, + [configFilePath], ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), diff --git a/test/services/config_service_test.dart b/test/services/config_service_test.dart index 68d3a96..c1538c8 100644 --- a/test/services/config_service_test.dart +++ b/test/services/config_service_test.dart @@ -21,9 +21,6 @@ void main() { const stackedAppFilePath = 'src/app/core.dart'; const testHelpersFilePath = 'lib/src/test/helpers/core_test.helpers.dart'; - const correctConfigPath = - 'No configuration file found. Please, correct the config path passed as argument.'; - const String customConfig = ''' { "stacked_app_file_path": "$stackedAppFilePath", @@ -40,44 +37,43 @@ void main() { '''; group('resolveConfigFile -', () { - test( - 'when called with config filepath and file is present on path should call fileExists on custom config filepath', + test('when called with configFilePath should return configFilePath', () async { - final fileService = getAndRegisterFileService(); + getAndRegisterFileService(); final service = _getService(); - await service.resolveConfigFile(path: customConfigFilePath); - verify(fileService.fileExists(filePath: customConfigFilePath)); + final path = await service.resolveConfigFile( + configFilePath: customConfigFilePath, + ); + expect(path, customConfigFilePath); }); test( - 'when called with config filepath and file is present on path should return customConfigFilePath', + 'when called with configFilePath should call fileExists on configFilePath', () async { - getAndRegisterFileService(); + final fileService = getAndRegisterFileService(); final service = _getService(); - final path = await service.resolveConfigFile( - path: customConfigFilePath, - ); - expect(path, customConfigFilePath); + await service.resolveConfigFile(configFilePath: customConfigFilePath); + verify(fileService.fileExists(filePath: customConfigFilePath)); }); test( - 'when called with config filepath and file is NOT present on path should throw ConfigFileNotFoundException', + 'when called with configFilePath and file does NOT exists should throw ConfigFileNotFoundException with message equal kConfigFileNotFoundRetry and shouldHaltCommand equal true', () async { getAndRegisterFileService(fileExistsResult: false); final service = _getService(); expect( - () => service.resolveConfigFile(path: customConfigFilePath), + () => service.resolveConfigFile(configFilePath: customConfigFilePath), throwsA(predicate( (e) => e is ConfigFileNotFoundException && - e.message == correctConfigPath && + e.message == kConfigFileNotFoundRetry && e.shouldHaltCommand == true, )), ); }); test( - 'when called without config filepath should call fileExists on XDG_CONFIG_HOME', + 'when called without configFilePath should call fileExists on XDG_CONFIG_HOME', () async { final fileService = getAndRegisterFileService(); final service = _getService(); @@ -85,8 +81,7 @@ void main() { verify(fileService.fileExists(filePath: xdgConfigFilePath)); }); - test( - 'when called without config filepath and file is present on XDG_CONFIG_HOME should return xdgConfigFilePath', + test('when called without configFilePath should return xdgConfigFilePath', () async { getAndRegisterFileService(); final service = _getService(); @@ -95,66 +90,146 @@ void main() { }); test( - 'when called without config filepath and file is NOT present on XDG_CONFIG_HOME should return null', + 'when called without configFilePath and Home environment variable is not set should throw ConfigFileNotFoundException with message equal kConfigFileNotFound and shouldHaltCommand equal false', + () async { + getAndRegisterFileService(throwStateError: true); + final service = _getService(); + expect( + () => service.resolveConfigFile(), + throwsA(predicate( + (e) => + e is ConfigFileNotFoundException && + e.message == kConfigFileNotFound && + e.shouldHaltCommand == false, + )), + ); + }, + skip: + 'Should throw ConfigFileNotFoundException because StateError exception is catched', + ); + + test( + 'when called without configFilePath and file does NOT exists should throw ConfigFileNotFoundException with message equal kConfigFileNotFound and shouldHaltCommand equal false', () async { getAndRegisterFileService(fileExistsResult: false); final service = _getService(); - final path = await service.resolveConfigFile(); - expect(path, isNull); + expect( + () => service.resolveConfigFile(), + throwsA(predicate( + (e) => + e is ConfigFileNotFoundException && + e.message == kConfigFileNotFound && + e.shouldHaltCommand == false, + )), + ); }); }); - group('loadConfig -', () { - test( - 'when called, should call fileExists on FileService for XDG_CONFIG_HOME', + group('composeConfigFile -', () { + test('when called with configFilePath should return configFilePath', () async { - final fileService = getAndRegisterFileService(readFileResult: '{}'); + getAndRegisterFileService(); final service = _getService(); - await service.loadConfig(); - verify(fileService.fileExists(filePath: xdgConfigFilePath)); + final path = await service.composeConfigFile( + configFilePath: customConfigFilePath, + ); + expect(path, customConfigFilePath); }); test( - 'when called with filepath, should call fileExists on FileService with filepath', + 'when called with configFilePath should call fileExists on configFilePath', () async { - final fileService = getAndRegisterFileService(readFileResult: '{}'); + final fileService = getAndRegisterFileService(); final service = _getService(); - await service.loadConfig(path: customConfigFilePath); + await service.composeConfigFile(configFilePath: customConfigFilePath); verify(fileService.fileExists(filePath: customConfigFilePath)); }); test( - 'when called and config json is malformed, should write error to console', + 'when called with configFilePath and file does NOT exists should throw ConfigFileNotFoundException with message equal kConfigFileNotFoundRetry and shouldHaltCommand equal true', + () async { + getAndRegisterFileService(fileExistsResult: false); + final service = _getService(); + expect( + () => service.composeConfigFile(configFilePath: customConfigFilePath), + throwsA(predicate( + (e) => + e is ConfigFileNotFoundException && + e.message == kConfigFileNotFoundRetry && + e.shouldHaltCommand == true, + )), + ); + }); + + test('when called without configFilePath should return kConfigFileName', () async { getAndRegisterFileService(); - final log = getAndRegisterColorizedLogService(); final service = _getService(); - await service.loadConfig(); - verify(log.warn(message: kConfigFileMalformed)); + final path = await service.composeConfigFile(); + expect(path, kConfigFileName); }); test( - 'when called and config file not available, should NOT call fileExists on FileService', + 'when called without configFilePath and projectPath is NOT null should return kConfigFileName with projectPath', () async { - final fileService = getAndRegisterFileService( - fileExistsResult: false, - ); + getAndRegisterFileService(); + final projectPath = 'example'; final service = _getService(); - await service.loadConfig(); - verifyNever( - fileService.readFileAsString(filePath: kConfigFileName), + final path = await service.composeConfigFile(projectPath: projectPath); + expect(path, '$projectPath/$kConfigFileName'); + }); + }); + + group('loadConfig -', () { + test('when called should call readFileAsString on FileService', () async { + final fileService = getAndRegisterFileService(readFileResult: '{}'); + final service = _getService(); + await service.loadConfig(customConfigFilePath); + verify(fileService.readFileAsString(filePath: customConfigFilePath)); + }); + + test('when called should set hasCustomConfig as true', () async { + getAndRegisterFileService(readFileResult: '{}'); + final service = _getService(); + await service.loadConfig(customConfigFilePath); + expect(service.hasCustomConfig, isTrue); + }); + + test('when called should sanitize path', () async { + final configToBeSanitize = {"services_path": "lib/services"}; + getAndRegisterFileService( + readFileResult: configToBeSanitize.toString(), ); + final service = _getService(); + await service.loadConfig(customConfigFilePath); + expect(service.servicePath, 'services'); }); test( - 'when called and config file not available, should write error to console', + 'when called and file not found should throw ConfigFileNotFoundException', () async { - getAndRegisterFileService(fileExistsResult: false); - final log = getAndRegisterColorizedLogService(); + getAndRegisterFileService(throwConfigFileNotFoundException: true); final service = _getService(); - await service.loadConfig(); - verify(log.warn(message: kConfigFileNotFound)); + expect( + () => service.loadConfig(customConfigFilePath), + throwsA(predicate( + (e) => + e is ConfigFileNotFoundException && + e.message == kConfigFileNotFoundRetry && + e.shouldHaltCommand == true, + )), + ); }); + + test('when called and file is malformed should throw FormatException', + () async { + getAndRegisterFileService(); + final service = _getService(); + expect( + () => service.loadConfig(customConfigFilePath), + throwsA(predicate((e) => e is FormatException)), + ); + }, skip: 'How can we trigger a FormatException from jsonDecode?'); }); group('replaceCustomPaths -', () { @@ -163,7 +238,6 @@ void main() { final path = 'test/services/generic_service_test.dart.stk'; getAndRegisterFileService(readFileResult: '{}'); final service = _getService(); - await service.loadConfig(); final customPath = service.replaceCustomPaths(path); expect(customPath, path); }); @@ -173,7 +247,7 @@ void main() { final path = 'test/services/generic_service_test.dart.stk'; getAndRegisterFileService(readFileResult: customConfig); final service = _getService(); - await service.loadConfig(); + await service.loadConfig(customConfigFilePath); final customPath = service.replaceCustomPaths(path); expect(customPath, isNot(path)); expect( @@ -188,7 +262,7 @@ void main() { final path = 'app/app.dart'; getAndRegisterFileService(readFileResult: customConfig); final service = _getService(); - await service.loadConfig(); + await service.loadConfig(customConfigFilePath); final customPath = service.replaceCustomPaths(path); expect(customPath, isNot(path)); expect(customPath, stackedAppFilePath); @@ -200,7 +274,7 @@ void main() { final path = 'helpers/test_helpers.dart'; getAndRegisterFileService(readFileResult: customConfig); final service = _getService(); - await service.loadConfig(); + await service.loadConfig(customConfigFilePath); final customPath = service.replaceCustomPaths(path); expect(customPath, isNot(path)); expect(customPath, testHelpersFilePath);