diff --git a/RepoM.sln.DotSettings b/RepoM.sln.DotSettings index 24b37438..4ae60fb1 100644 --- a/RepoM.sln.DotSettings +++ b/RepoM.sln.DotSettings @@ -1,4 +1,5 @@  + True True True True diff --git a/_ref/RepositoryActions.yaml b/_ref/RepositoryActions.yaml deleted file mode 100644 index 3e429858..00000000 --- a/_ref/RepositoryActions.yaml +++ /dev/null @@ -1,93 +0,0 @@ -version: 1 - -variables: -- name: IsRepoM - value: '{empty}{StringContains({Repository.SafePath}, "RepoM")}' - -repository-specific-env-files: -- filename: '{Repository.SafePath}{slash}.git{slash}repom.env' - when: '{var.IsRepoM}' -- filename: '{Repository.SafePath}{slash}repom.env' - when: true - -repository-specific-config-files: -- filename: '{Repository.SafePath}{slash}.git{slash}RepositoryActions.json' -- filename: '{Repository.SafePath}{slash}RepositoryActions.json' - -repository-tags: -- tag: Work - when: '{StringContains({Repository.SafePath}, "Work")}' -- tag: Private - when: '{Not({StringContains({Repository.SafePath}, "Work")})}' - -repository-actions: - variables: - - name: key - value: abc - actions: - - type: command@1 - variables: - - name: Test2 - value: -- T3sT2 - enabled: true - name: '{OpenIn} Windows File Explorer' - command: '"{Repository.SafePath}"' - active: true - - type: command@1 - name: '{OpenIn} Windows Terminal' - command: wt - arguments: -d "{Repository.SafePath}" - active: true - - type: command@1 - name: '{OpenIn} Windows Command Shell' - command: cmd - arguments: /K "cd /d {Repository.SafePath}" - active: false - - type: executable@1 - name: '{OpenIn} Windows PowerShell' - executables: - - '%WINDIR%/System32/WindowsPowerShell/v1.0/powershell.exe' - arguments: -executionpolicy bypass -noexit -command "Set-Location '{Repository.SafePath}'" - active: false - - type: executable@1 - name: '{OpenIn} Visual Studio Code' - executables: - - '%LocalAppData%/Programs/Microsoft VS Code/code.exe' - - '%ProgramW6432%/Microsoft VS Code/code.exe' - arguments: '"{Repository.SafePath}"' - active: true - - type: executable@1 - name: '{OpenIn} Sourcetree' - executables: - - '%LocalAppData%/SourceTree/SourceTree.exe' - - '%PROGRAMFILES(X86)%/Atlassian/SourceTree/SourceTree.exe' - arguments: -f "{Repository.Location}{backslash}{Repository.Name}" - active: true - - type: executable@1 - name: '{OpenIn} Everything' - executables: - - '%ProgramW6432%/Everything/Everything.exe' - arguments: -s """"{Repository.Path}""" " - active: true - - type: executable@1 - name: '{OpenIn} TotalCommander' - executables: - - '%ProgramW6432%/totalcmd/TOTALCMD64.EXE' - - '%SystemDrive%/totalcmd/TOTALCMD64.EXE' - arguments: /O /T /L="{Repository.SafePath}" - active: true - - type: separator@1 - - type: browse-repository@1 - - type: separator@1 - - type: git-fetch@1 - - type: git-pull@1 - - type: git-push@1 - - type: git-checkout@1 - - type: separator@1 - - type: ignore-repositories@1 - - type: separator@1 - - type: associate-file@1 - name: '{Open} Visual Studio solutions' - extension: '*.sln' - command: start - arguments: '"{FilePath}"' diff --git a/_setup/RepoM.nsi b/_setup/RepoM.nsi index e2439f8c..e9737803 100644 --- a/_setup/RepoM.nsi +++ b/_setup/RepoM.nsi @@ -53,7 +53,6 @@ Section "RepoM" File /r ..\_output\win\Assemblies\*.* File ..\_ref\PathEd.exe ; Add PathEd.exe to add the RepoM directory to the system's PATH easily ; File ..\_ref\SendKeys.exe ; Add SendKeys.exe to add the RepoM directory for grr. - File ..\_ref\RepositoryActions.yaml ; Can be copied in-app for the default settings CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" $INSTDIR\${PRODUCT_NAME}.exe ; Add the installation folder to the system PATH -> to enable grr.exe diff --git a/src/RepoM.Api/Common/FilesCompareSettingsService.cs b/src/RepoM.Api/Common/FilesCompareSettingsService.cs index f6f96e3b..3172b696 100644 --- a/src/RepoM.Api/Common/FilesCompareSettingsService.cs +++ b/src/RepoM.Api/Common/FilesCompareSettingsService.cs @@ -73,23 +73,7 @@ private Dictionary LoadInner() if (!_fileSystem.File.Exists(file)) { - var templateFilename = _fileSystem.Path.Combine(_appDataPathProvider.AppResourcesPath, FILENAME); - if (_fileSystem.File.Exists(templateFilename)) - { - try - { - _fileSystem.File.Copy(templateFilename, file); - } - catch (Exception e) - { - _logger.LogError(e, "Could not copy template file '{TemplateFilename}' to '{File}'", templateFilename, file); - } - } - - if (!_fileSystem.File.Exists(file)) - { - throw new FileNotFoundException("Comparer configuration file not found", file); - } + throw new FileNotFoundException("Comparer configuration file not found", file); } try diff --git a/src/RepoM.Api/Common/FilesFilterSettingsService.cs b/src/RepoM.Api/Common/FilesFilterSettingsService.cs index d8f3ac12..ea9b2284 100644 --- a/src/RepoM.Api/Common/FilesFilterSettingsService.cs +++ b/src/RepoM.Api/Common/FilesFilterSettingsService.cs @@ -41,23 +41,7 @@ private Dictionary Load() if (!_fileSystem.File.Exists(file)) { - var templateFilename = _fileSystem.Path.Combine(_appDataPathProvider.AppResourcesPath, FILENAME); - if (_fileSystem.File.Exists(templateFilename)) - { - try - { - _fileSystem.File.Copy(templateFilename, file); - } - catch (Exception e) - { - _logger.LogError(e, "Could not copy template file '{TemplateFilename}' to '{File}'", templateFilename, file); - } - } - - if (!_fileSystem.File.Exists(file)) - { - throw new FileNotFoundException("Filtering configuration file not found", file); - } + throw new FileNotFoundException("Filtering configuration file not found", file); } try diff --git a/src/RepoM.Api/EnsureStartup.cs b/src/RepoM.Api/EnsureStartup.cs new file mode 100644 index 00000000..6c3a1d22 --- /dev/null +++ b/src/RepoM.Api/EnsureStartup.cs @@ -0,0 +1,53 @@ +namespace RepoM.Api; + +using System; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using RepoM.Api.Resources; +using RepoM.Core.Plugin.Common; + +public class EnsureStartup +{ + private readonly IFileSystem _fileSystem; + private readonly IAppDataPathProvider _appDataProvider; + + public EnsureStartup(IFileSystem fileSystem, IAppDataPathProvider appDataProvider) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _appDataProvider = appDataProvider ?? throw new ArgumentNullException(nameof(appDataProvider)); + } + + public async Task EnsureFilesAsync() + { + await CheckOrCreateAsync("RepositoryActionsV2.yaml", EmbeddedResources.GetRepositoryActionsV2Yaml).ConfigureAwait(false); + await CheckOrCreateAsync("RepoM.Filtering.yaml", EmbeddedResources.GetFilteringYaml).ConfigureAwait(false); + await CheckOrCreateAsync("RepoM.Ordering.yaml", EmbeddedResources.GetSortingYaml).ConfigureAwait(false); + await CheckOrCreateAsync("appsettings.serilog.json", EmbeddedResources.GetSerilogAppSettings).ConfigureAwait(false); + } + + private async Task CheckOrCreateAsync(string filename, Func func) + { + var fullFilename = Path.Combine(_appDataProvider.AppDataPath, filename); + + if (_fileSystem.File.Exists(fullFilename)) + { + return; + } + + await using Stream stream = func.Invoke(); + await TryCreateAsync(fullFilename, stream).ConfigureAwait(false); + + if (!_fileSystem.File.Exists(fullFilename)) + { + throw new FileNotFoundException(fullFilename); + } + } + + private async Task TryCreateAsync(string filename, Stream content) + { + using var ms = new MemoryStream(); + await content.CopyToAsync(ms).ConfigureAwait(false); + await _fileSystem.File.WriteAllBytesAsync(filename, ms.ToArray()).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs b/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs index 9468bab1..ca9795b4 100644 --- a/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs +++ b/src/RepoM.Api/IO/DefaultAppDataPathProvider.cs @@ -7,7 +7,6 @@ namespace RepoM.Api.IO; public class DefaultAppDataPathProvider : IAppDataPathProvider { private static readonly string _applicationDataRepoM = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RepoM"); - private static readonly string _appResourcesPath = GetAppResourcePath(); private DefaultAppDataPathProvider() { @@ -16,25 +15,4 @@ private DefaultAppDataPathProvider() public static DefaultAppDataPathProvider Instance { get; } = new(); public string AppDataPath => _applicationDataRepoM; - - - public string AppResourcesPath => _appResourcesPath; - - private static string GetAppResourcePath() - { - var entryAssembly = System.Reflection.Assembly.GetEntryAssembly(); - if (entryAssembly == null) - { - throw new NotSupportedException("Could not get entry point of assembly."); - } - - var result = Path.GetDirectoryName(entryAssembly.Location); - - if (result == null) - { - throw new FileNotFoundException("Could not find location of entry assembly"); - } - - return result; - } } \ No newline at end of file diff --git a/src/RepoM.Api/RepoM.Api.csproj b/src/RepoM.Api/RepoM.Api.csproj index 2575862a..69512c40 100644 --- a/src/RepoM.Api/RepoM.Api.csproj +++ b/src/RepoM.Api/RepoM.Api.csproj @@ -20,4 +20,11 @@ + + + + + + + diff --git a/src/RepoM.Api/Resources/EmbeddedResources.cs b/src/RepoM.Api/Resources/EmbeddedResources.cs new file mode 100644 index 00000000..1802625a --- /dev/null +++ b/src/RepoM.Api/Resources/EmbeddedResources.cs @@ -0,0 +1,37 @@ +namespace RepoM.Api.Resources; + +using System.IO; +using System.Reflection; +using LibGit2Sharp; + +internal static class EmbeddedResources +{ + private static readonly Assembly _assembly = typeof(EmbeddedResources).Assembly; + private static readonly string _namespace = typeof(EmbeddedResources).Namespace!; + + public static Stream GetRepositoryActionsV2Yaml() + { + return ResolveFromAssembly("RepositoryActionsV2.yaml"); + } + + public static Stream GetSortingYaml() + { + return ResolveFromAssembly("RepoM.Sorting.yaml"); + } + + public static Stream GetFilteringYaml() + { + return ResolveFromAssembly("RepoM.Filtering.yaml"); + } + + public static Stream GetSerilogAppSettings() + { + return ResolveFromAssembly("appsettings.serilog.json"); + } + + private static Stream ResolveFromAssembly(string relativeFilename) + { + var embeddedFilename = $"{_namespace}.{relativeFilename}"; + return _assembly.GetManifestResourceStream(embeddedFilename) ?? throw new NotFoundException($"{relativeFilename} not found."); + } +} \ No newline at end of file diff --git a/_ref/RepoM.Filtering.yaml b/src/RepoM.Api/Resources/RepoM.Filtering.yaml similarity index 89% rename from _ref/RepoM.Filtering.yaml rename to src/RepoM.Api/Resources/RepoM.Filtering.yaml index 62335d1e..52ff09c4 100644 --- a/_ref/RepoM.Filtering.yaml +++ b/src/RepoM.Api/Resources/RepoM.Filtering.yaml @@ -15,4 +15,4 @@ Private: query: RepoM OR is:pinned filter: kind: query@1 - query: (-tag:work OR tag:prive) \ No newline at end of file + query: (-tag:work OR tag:private) \ No newline at end of file diff --git a/_ref/RepoM.Sorting.yaml b/src/RepoM.Api/Resources/RepoM.Sorting.yaml similarity index 100% rename from _ref/RepoM.Sorting.yaml rename to src/RepoM.Api/Resources/RepoM.Sorting.yaml diff --git a/src/RepoM.Api/Resources/RepositoryActionsV2.yaml b/src/RepoM.Api/Resources/RepositoryActionsV2.yaml new file mode 100644 index 00000000..9a713b37 --- /dev/null +++ b/src/RepoM.Api/Resources/RepositoryActionsV2.yaml @@ -0,0 +1,190 @@ +context: +- type: evaluate-script@1 + content: |- + # at this moment, you must leave this function intact + func translate(input) + ret input + end + + func is_null(input) + ret input == null + end + + func get_filename(path) + ret path | string.split("\\") | array.last + end + + func remotes_contain_inner(remotes, url_part) + urls = remotes | array.map "url" + filtered = array.filter(urls, do + ret string.contains($0, url_part) + end) + ret array.size(filtered) > 0; + end + + func remotes_contain(url_part) + ret remotes_contain_inner(repository.remotes, url_part) + end + + func get_remote_origin() + remotes = repository.remotes; + filtered = array.filter(remotes, do + remote = $0; + ret remote.key == "origin" + end) + ret array.first(filtered); + end + + func get_remote_origin_name() + remote = get_remote_origin(); + ret remote?.name; + end + + func repository_path_contains(path) + ret repository.linux_path | string.contains path + end + + func is_feature_branch() + ret repository.branch | string.starts_with "feature/" + end + + func sanitize_feature_branch_name() + ret repository.branch | string.replace "feature/" "" | string.strip + end + + remote_name_origin = get_remote_origin_name(); + is_work_repository = remotes_contain("My-Work"); + is_github_repository = remotes_contain("github.com"); + + solution_files = file.find_files(repository.linux_path, "*.sln"); + solution_file = array.first(solution_files); + + exe_vs_code = env.LocalAppData + "/Programs/Microsoft VS Code/code.exe"; + +- root_path_repom: C:\\Users\\Munckhof CJJ\\OneDrive - BDO\\BDO\\RepoM\\ + +# Specific var files +- type: render-variable@1 + name: repo_docs_directory + value: 'G:\\My Drive\\RepoDocs\\github.com\\{{ remote_name_origin }}' + enabled: is_github_repository + +# Env files +- type: render-variable@1 + name: repo_environment_file_directory + value: '{{ env.REPOZ_CONFIG_PATH }}\\{{ remote_name_origin }}' + +- type: render-variable@1 + name: repo_environment_file + value: '{{ env.REPOZ_CONFIG_PATH }}\\{{ remote_name_origin }}\\RepoM.env' + +- type: render-variable@1 + name: repo_yaml_file + value: 'C:\\WorkCofigs\\{{ remote_name_origin }}\\RepoMV2.yaml' + +# Runsettings +- type: load-file@1 + filename: '{{ repo_environment_file }}' + enabled: is_work_repository + +- type: load-file@1 + filename: '{{ env.REPOZ_CONFIG_PATH }}\\work.env' + enabled: is_work_repository + +- type: load-file@1 + filename: '{{ repo_yaml_file }}' + enabled: is_work_repository + +tags: + +- tag: work + when: is_work_repository + +- tag: private + when: '!is_work_repository && repository_path_contains("Projects/Private")' + +action-menu: + +- type: command@1 + name: Open in Windows File Explorer + command: '"{{ repository.path }}"' + +- type: command@1 + name: Open in Windows Terminal + command: wt + arguments: -d "{{ repository.linux_path }}" + +- type: executable@1 + name: 'Open in Windows PowerShell' + executable: '{{ env.WINDIR }}/System32/WindowsPowerShell/v1.0/powershell.exe' + arguments: -executionpolicy bypass -noexit -command "Set-Location '{{ repository.linux_path }}'" + +# Open in visual studio when exactly one '.sln' file was found: +- type: command@1 + name: Open in Visual Studio + command: '{{ solution_file }}' + active: array.size(solution_files) == 1 + +# Otherwise, Visual studio folder with all '.sln' files when multiple sln files were found: +- type: folder@1 + name: Open in Visual Studio + active: array.size(solution_files) > 1 + actions: + - type: foreach@1 + enumerable: solution_files + variable: sln + actions: + - type: command@1 + name: '{{ get_filename(sln) }}' + command: '{{ sln }}' + +- type: executable@1 + name: Open in Visual Studio Code + executable: '{{ exe_vs_code }}' + arguments: '"{{ repository.linux_path }}"' + +- type: executable@1 + name: Open in Sourcetree + executable: '{{ env.LocalAppData }}/SourceTree/SourceTree.exe' + arguments: -f "{{ repository.windows_path }}" + +- type: executable@1 + name: Open in Everything + executable: '{{ env.ProgramW6432 }}/Everything/Everything.exe' + arguments: -s """"{{ repository.path }}""" " + +- type: executable@1 + name: Open in TotalCommander + executable: '{{ env.ProgramW6432 }}/totalcmd/TOTALCMD64.EXE' + arguments: /O /T /L="{{ repository.linux_path }}" + +- type: separator@1 + +- type: folder@1 + name: Git + actions: + - type: browse-repository@1 + - type: git-fetch@1 + - type: git-pull@1 + - type: git-push@1 + - type: git-checkout@1 + +- type: separator@1 + +- type: ignore-repository@1 + +- type: separator@1 + active: is_work_repository + +- type: folder@1 + name: '-- Examples --' + actions: + + - type: just-text@1 + name: 'Current branch is {{ repository.branch }}' + + - type: command@1 + name: 'Create Directory {{ repo_docs_directory }}' + command: cmd + arguments: /k mkdir "{{ repo_docs_directory }}" + active: '!file.dir_exists(repo_docs_directory)' diff --git a/src/RepoM.Api/Resources/appsettings.serilog.json b/src/RepoM.Api/Resources/appsettings.serilog.json new file mode 100644 index 00000000..6ed7718c --- /dev/null +++ b/src/RepoM.Api/Resources/appsettings.serilog.json @@ -0,0 +1,18 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "MinimumLevel": "Verbose", + "WriteTo": [ + { + "Name": "File", + "Args": + { + "path": "%APPDATA%/RepoM/Logs/repom.txt", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} [{Level:u3}] [{ThreadId}:{ThreadName}] {Message}{NewLine}{Exception}" + } + } + ], + "Enrich": [ "WithThreadId" ] + } +} \ No newline at end of file diff --git a/src/RepoM.App/App.xaml.cs b/src/RepoM.App/App.xaml.cs index def68214..68a51096 100644 --- a/src/RepoM.App/App.xaml.cs +++ b/src/RepoM.App/App.xaml.cs @@ -22,6 +22,7 @@ namespace RepoM.App; using Container = SimpleInjector.Container; using RepoM.App.Services.HotKey; using Serilog.Enrichers; +using RepoM.Api; /// /// Interaction logic for App.xaml @@ -63,7 +64,7 @@ protected override async void OnStartup(StartupEventArgs e) IHmacService hmacService = new HmacSha256Service(); IPluginFinder pluginFinder = new PluginFinder(fileSystem, hmacService); - IConfiguration config = SetupConfiguration(fileSystem); + IConfiguration config = SetupConfiguration(); ILoggerFactory loggerFactory = CreateLoggerFactory(config); ILogger logger = loggerFactory.CreateLogger(nameof(App)); logger.LogInformation("Started"); @@ -73,12 +74,15 @@ protected override async void OnStartup(StartupEventArgs e) #if DEBUG Bootstrapper.Container.Verify(SimpleInjector.VerificationOption.VerifyAndDiagnose); +#else + Bootstrapper.Container.Options.EnableAutoVerification = false; #endif + EnsureStartup ensureStartup = Bootstrapper.Container.GetInstance(); + await ensureStartup.EnsureFilesAsync().ConfigureAwait(true); + UseRepositoryMonitor(Bootstrapper.Container); - _ = Bootstrapper.Container.GetInstance(); // not sure if this is required. - _moduleService = Bootstrapper.Container.GetInstance(); _hotKeyService = Bootstrapper.Container.GetInstance(); _windowSizeService = Bootstrapper.Container.GetInstance(); @@ -111,30 +115,10 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); } - private static IConfiguration SetupConfiguration(IFileSystem fileSystem) + private static IConfiguration SetupConfiguration() { const string FILENAME = "appsettings.serilog.json"; var fullFilename = Path.Combine(DefaultAppDataPathProvider.Instance.AppDataPath, FILENAME); - if (!fileSystem.File.Exists(fullFilename)) - { - try - { - var fullFilenameTemplate = Path.Combine(DefaultAppDataPathProvider.Instance.AppResourcesPath, FILENAME); - if (fileSystem.File.Exists(fullFilenameTemplate)) - { - fileSystem.File.Copy(fullFilenameTemplate, fullFilename); - } - } - catch (Exception) - { - // swallow - } - } - - if (!fileSystem.File.Exists(fullFilename)) - { - fullFilename = FILENAME; - } IConfigurationBuilder builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) diff --git a/src/RepoM.App/Bootstrapper.cs b/src/RepoM.App/Bootstrapper.cs index a5b72a37..49b91d09 100644 --- a/src/RepoM.App/Bootstrapper.cs +++ b/src/RepoM.App/Bootstrapper.cs @@ -109,6 +109,8 @@ public static void RegisterServices(IFileSystem fileSystem) Container.RegisterSingleton(); Container.RegisterSingleton(); + + Container.RegisterSingleton(); } public static async Task RegisterPlugins( diff --git a/src/RepoM.Core.Plugin/Common/IAppDataPathProvider.cs b/src/RepoM.Core.Plugin/Common/IAppDataPathProvider.cs index 137b61d7..eba01b99 100644 --- a/src/RepoM.Core.Plugin/Common/IAppDataPathProvider.cs +++ b/src/RepoM.Core.Plugin/Common/IAppDataPathProvider.cs @@ -3,6 +3,4 @@ namespace RepoM.Core.Plugin.Common; public interface IAppDataPathProvider { string AppDataPath { get; } - - string AppResourcesPath { get; } } \ No newline at end of file diff --git a/tests/RepoM.Api.Tests/EnsureStartupTests.cs b/tests/RepoM.Api.Tests/EnsureStartupTests.cs new file mode 100644 index 00000000..00ec3872 --- /dev/null +++ b/tests/RepoM.Api.Tests/EnsureStartupTests.cs @@ -0,0 +1,92 @@ +namespace RepoM.Api.Tests; + +using System; +using System.IO; +using System.IO.Abstractions; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using RepoM.Core.Plugin.Common; +using Xunit; + +public class EnsureStartupTests +{ + private readonly IFileSystem _fileSystem = A.Fake(); + private readonly IAppDataPathProvider _appDataProvider = A.Fake(); + private readonly EnsureStartup _sut; + + public EnsureStartupTests() + { + _sut = new EnsureStartup(_fileSystem, _appDataProvider); + A.CallTo(() => _appDataProvider.AppDataPath).Returns(Path.Combine("C:", "my-dummy", "path")); + } + + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new EnsureStartup(_fileSystem, null!); + Func act2 = () => new EnsureStartup(null!, _appDataProvider); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + } + + [Theory] + [InlineData("RepositoryActionsV2.yaml")] + [InlineData("RepoM.Filtering.yaml")] + [InlineData("RepoM.Ordering.yaml")] + [InlineData("appsettings.serilog.json")] + public async Task EnsureFilesAsync_ShouldCheckIfFileExists(string filename) + { + // arrange + var expectedFilename = Path.Combine("C:", "my-dummy", "path", filename); + A.CallTo(() => _fileSystem.File.Exists(A._)).Returns(true); + + // act + await _sut.EnsureFilesAsync(); + + // assert + A.CallTo(() => _fileSystem.File.Exists(expectedFilename)).MustHaveHappenedOnceExactly(); + } + + [Theory] + [InlineData("RepositoryActionsV2.yaml")] + [InlineData("RepoM.Filtering.yaml")] + [InlineData("RepoM.Ordering.yaml")] + [InlineData("appsettings.serilog.json")] + public async Task EnsureFilesAsync_ShouldThrowFileNotFoundException_WhenFileDoesNotExists(string filename) + { + // arrange + A.CallTo(() => _fileSystem.File.Exists(A._)).Returns(true); + A.CallTo(() => _fileSystem.File.Exists(A.That.EndsWith(filename))).Returns(false); + + // act + Func act = _sut.EnsureFilesAsync; + + // assert + await act.Should().ThrowAsync().WithMessage("*" + filename); + } + + [Theory] + [InlineData("RepositoryActionsV2.yaml")] + [InlineData("RepoM.Filtering.yaml")] + [InlineData("RepoM.Ordering.yaml")] + [InlineData("appsettings.serilog.json")] + public async Task EnsureFilesAsync_ShouldCreate_WhenFileDoesNotExists(string filename) + { + // arrange + A.CallTo(() => _fileSystem.File.Exists(A._)).Returns(true); + A.CallTo(() => _fileSystem.File.Exists(A.That.EndsWith(filename))).ReturnsNextFromSequence(false, true); + + // act + await _sut.EnsureFilesAsync(); + + // assert + A.CallTo(() => _fileSystem.File.WriteAllBytesAsync(A.That.EndsWith(filename), A._, A._)).MustHaveHappenedOnceExactly(); + } +} \ No newline at end of file