diff --git a/src/RepoM.Api/Common/AppSettings.cs b/src/RepoM.Api/Common/AppSettings.cs index bdfbfd04..65757ca5 100644 --- a/src/RepoM.Api/Common/AppSettings.cs +++ b/src/RepoM.Api/Common/AppSettings.cs @@ -33,9 +33,9 @@ public AppSettings() public List EnabledSearchProviders { get; set; } - public string SonarCloudPersonalAccessToken { get; set; } + public string? SonarCloudPersonalAccessToken { get; set; } - public AzureDevOpsOptions AzureDevOps { get; set; } + public AzureDevOpsOptions? AzureDevOps { get; set; } public List Plugins { get; set; } @@ -46,7 +46,7 @@ public AppSettings() MenuSize = Size.Default, ReposRootDirectories = new(), EnabledSearchProviders = new List(1), - SonarCloudPersonalAccessToken = string.Empty, + SonarCloudPersonalAccessToken = null, AzureDevOps = AzureDevOpsOptions.Default, Plugins = new List(), }; @@ -54,15 +54,11 @@ public AppSettings() public class AzureDevOpsOptions { - public string PersonalAccessToken { get; set; } = string.Empty; + public string? PersonalAccessToken { get; set; } = string.Empty; - public string BaseUrl { get; set; } = string.Empty; + public string? BaseUrl { get; set; } = string.Empty; - public static AzureDevOpsOptions Default => new() - { - PersonalAccessToken = string.Empty, - BaseUrl = string.Empty, - }; + public static AzureDevOpsOptions? Default => null; } public class Size diff --git a/src/RepoM.Api/Common/FileAppSettingsService.cs b/src/RepoM.Api/Common/FileAppSettingsService.cs index 3ee553c6..6f258b37 100644 --- a/src/RepoM.Api/Common/FileAppSettingsService.cs +++ b/src/RepoM.Api/Common/FileAppSettingsService.cs @@ -235,51 +235,120 @@ public List EnabledSearchProviders } } + [Obsolete("Will be removed in next big version")] public string SonarCloudPersonalAccessToken { - get => Settings.SonarCloudPersonalAccessToken; + get => Settings.SonarCloudPersonalAccessToken ?? string.Empty; set { - if (value.Equals(Settings.SonarCloudPersonalAccessToken, StringComparison.InvariantCulture)) + if (string.IsNullOrWhiteSpace(value)) { - return; + if (Settings.SonarCloudPersonalAccessToken == null) + { + return; + } + + Settings.SonarCloudPersonalAccessToken = null; } + else + { + if (value.Equals(Settings.SonarCloudPersonalAccessToken, StringComparison.InvariantCulture)) + { + return; + } - Settings.SonarCloudPersonalAccessToken = value; + Settings.SonarCloudPersonalAccessToken = value; + } NotifyChange(); Save(); } } + [Obsolete("Will be removed in next big version")] public string AzureDevOpsPersonalAccessToken { - get => Settings.AzureDevOps.PersonalAccessToken; + get => Settings.AzureDevOps?.PersonalAccessToken ?? string.Empty; set { - if (value.Equals(Settings.AzureDevOps.PersonalAccessToken, StringComparison.InvariantCulture)) + if (string.IsNullOrWhiteSpace(value)) { - return; + if (Settings.AzureDevOps == null) + { + return; + } + + if (string.IsNullOrWhiteSpace(Settings.AzureDevOps.PersonalAccessToken)) + { + return; + } + + Settings.AzureDevOps.PersonalAccessToken = null; + + if (Settings.AzureDevOps.BaseUrl == null) + { + Settings.AzureDevOps = null; + } + } + else + { + if (Settings.AzureDevOps == null) + { + Settings.AzureDevOps = new AzureDevOpsOptions { PersonalAccessToken = value, }; + } + else + { + if (value.Equals(Settings.AzureDevOps.PersonalAccessToken, StringComparison.InvariantCulture)) + { + return; + } + } } - - Settings.AzureDevOps.PersonalAccessToken = value; NotifyChange(); Save(); } } + [Obsolete("Will be removed in next big version")] public string AzureDevOpsBaseUrl { - get => Settings.AzureDevOps.BaseUrl; + get => Settings.AzureDevOps?.BaseUrl ?? string.Empty; set { - if (value.Equals(Settings.AzureDevOps.BaseUrl, StringComparison.InvariantCulture)) + if (string.IsNullOrWhiteSpace(value)) { - return; + if (Settings.AzureDevOps == null) + { + return; + } + + if (string.IsNullOrWhiteSpace(Settings.AzureDevOps.BaseUrl)) + { + return; + } + + Settings.AzureDevOps.BaseUrl = null; + + if (Settings.AzureDevOps.PersonalAccessToken == null) + { + Settings.AzureDevOps = null; + } + } + else + { + if (Settings.AzureDevOps == null) + { + Settings.AzureDevOps = new AzureDevOpsOptions { BaseUrl = value, }; + } + else + { + if (value.Equals(Settings.AzureDevOps.BaseUrl, StringComparison.InvariantCulture)) + { + return; + } + } } - - Settings.AzureDevOps.BaseUrl = value; NotifyChange(); Save(); diff --git a/src/RepoM.Api/Common/IAppSettingsService.cs b/src/RepoM.Api/Common/IAppSettingsService.cs index fe0e5b4d..82b31647 100644 --- a/src/RepoM.Api/Common/IAppSettingsService.cs +++ b/src/RepoM.Api/Common/IAppSettingsService.cs @@ -30,10 +30,13 @@ public interface IAppSettingsService List EnabledSearchProviders { get; set; } + [Obsolete("Will be removed in next big version")] string SonarCloudPersonalAccessToken { get; set; } + [Obsolete("Will be removed in next big version")] string AzureDevOpsPersonalAccessToken { get; set; } + [Obsolete("Will be removed in next big version")] string AzureDevOpsBaseUrl { get; set; } string SortKey { get; set; } diff --git a/src/RepoM.App/App.xaml.cs b/src/RepoM.App/App.xaml.cs index 90abc01d..83fba413 100644 --- a/src/RepoM.App/App.xaml.cs +++ b/src/RepoM.App/App.xaml.cs @@ -70,7 +70,7 @@ protected override void OnStartup(StartupEventArgs e) logger.LogInformation("Started"); Bootstrapper.RegisterLogging(loggerFactory); Bootstrapper.RegisterServices(fileSystem); - Bootstrapper.RegisterPlugins(pluginFinder, fileSystem); + Bootstrapper.RegisterPlugins(pluginFinder, fileSystem, loggerFactory).GetAwaiter().GetResult(); #if DEBUG Bootstrapper.Container.Verify(VerificationOption.VerifyAndDiagnose); diff --git a/src/RepoM.App/Bootstrapper.cs b/src/RepoM.App/Bootstrapper.cs index ddbaa8f1..e9745d40 100644 --- a/src/RepoM.App/Bootstrapper.cs +++ b/src/RepoM.App/Bootstrapper.cs @@ -41,6 +41,7 @@ namespace RepoM.App; using System.Reflection; using System; using System.Linq; +using System.Threading.Tasks; using SimpleInjector; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -172,7 +173,7 @@ public static void RegisterServices(IFileSystem fileSystem) Container.RegisterSingleton(); } - public static void RegisterPlugins(IPluginFinder pluginFinder, IFileSystem fileSystem) + public static async Task RegisterPlugins(IPluginFinder pluginFinder, IFileSystem fileSystem, ILoggerFactory loggerFactory) { Container.Register(Lifestyle.Singleton); Container.RegisterInstance(pluginFinder); @@ -211,7 +212,13 @@ static PluginSettings Convert(PluginInfo pluginInfo, string baseDir, bool enable if (assemblies.Any()) { - Container.RegisterPackages(assemblies); + await Container.RegisterPackagesAsync( + assemblies, + filename => new FileBasedPackageConfiguration( + DefaultAppDataPathProvider.Instance, + fileSystem, + loggerFactory.CreateLogger(), + filename)).ConfigureAwait(false); } } diff --git a/src/RepoM.App/Services/FileBasedPackageConfiguration.cs b/src/RepoM.App/Services/FileBasedPackageConfiguration.cs new file mode 100644 index 00000000..125ff380 --- /dev/null +++ b/src/RepoM.App/Services/FileBasedPackageConfiguration.cs @@ -0,0 +1,132 @@ +namespace RepoM.App.Services; + +using System; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RepoM.Core.Plugin; +using RepoM.Core.Plugin.Common; + +internal class FileBasedPackageConfiguration : IPackageConfiguration +{ + private readonly IAppDataPathProvider _appDataPathProvider; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly string _filename; + + public FileBasedPackageConfiguration(IAppDataPathProvider appDataPathProvider, IFileSystem fileSystem, ILogger logger, string filename) + { + _appDataPathProvider = appDataPathProvider ?? throw new ArgumentNullException(nameof(appDataPathProvider)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _filename = filename ?? throw new ArgumentNullException(nameof(filename)); + } + + public async Task GetConfigurationVersionAsync() + { + ConfigEnvelope? result = await LoadAsync().ConfigureAwait(false); + return result?.Version; + } + + public async Task LoadConfigurationAsync() where T : class, new() + { + ConfigEnvelope? result = await LoadAsync().ConfigureAwait(false); + return result?.Settings; + } + + public async Task PersistConfigurationAsync(T configuration, int version) + { + if (configuration == null) + { + return; + } + + var filename = GetFilename(); + + var exists = MakeSureDirectoryExists(filename); + if (!exists) + { + return; + } + + var json = JsonConvert.SerializeObject(new ConfigEnvelope { Version = version, Settings = configuration, }, Formatting.Indented); + + try + { + await _fileSystem.File.WriteAllTextAsync(filename, json).ConfigureAwait(false); + } + catch (Exception) + { + // swallow for now + } + } + + private bool MakeSureDirectoryExists(string filename) + { + try + { + IFileInfo fi = _fileSystem.FileInfo.New(filename); + var directoryName = fi.Directory?.FullName; + + if (string.IsNullOrWhiteSpace(directoryName)) + { + return false; + } + + if (_fileSystem.Directory.Exists(directoryName)) + { + return true; + } + + try + { + _fileSystem.Directory.CreateDirectory(directoryName); + } + catch (Exception e) + { + _logger.LogError(e, "Could not create directory '{directoryName}'. {message}", directoryName, e.Message); + } + + return _fileSystem.Directory.Exists(directoryName); + } + catch (Exception) + { + return false; + } + } + + private async Task?> LoadAsync() + { + var filename = GetFilename(); + if (!_fileSystem.File.Exists(filename)) + { + return null; + } + + try + { + var json = await _fileSystem.File.ReadAllTextAsync(filename).ConfigureAwait(false); + ConfigEnvelope? result = JsonConvert.DeserializeObject>(json); + return result; + } + catch (Exception e) + { + _logger.LogError(e, "Could not deserialize '{filename}'", filename); + return null; + } + } + + private string GetFilename() + { + return Path.Combine(_appDataPathProvider.GetAppDataPath(), "Module", _filename + ".json"); + } + + private sealed class ConfigEnvelope + { + public int? Version { get; init; } + + public T? Settings { get; init; } + } +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/IPackageConfiguration.cs b/src/RepoM.Core.Plugin/IPackageConfiguration.cs new file mode 100644 index 00000000..fbb01fda --- /dev/null +++ b/src/RepoM.Core.Plugin/IPackageConfiguration.cs @@ -0,0 +1,12 @@ +namespace RepoM.Core.Plugin; + +using System.Threading.Tasks; + +public interface IPackageConfiguration +{ + Task GetConfigurationVersionAsync(); + + Task LoadConfigurationAsync() where T : class, new(); + + Task PersistConfigurationAsync(T configuration, int version); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/IPackageWithConfiguration.cs b/src/RepoM.Core.Plugin/IPackageWithConfiguration.cs new file mode 100644 index 00000000..6e9606cf --- /dev/null +++ b/src/RepoM.Core.Plugin/IPackageWithConfiguration.cs @@ -0,0 +1,14 @@ +namespace RepoM.Core.Plugin; + +using System.Threading.Tasks; +using SimpleInjector; +using SimpleInjector.Packaging; + +public interface IPackageWithConfiguration : IPackage +{ + public string Name { get; } + + /// Registers the set of services in the specified . + /// The container the set of services is registered into. + Task RegisterServicesAsync(Container container, IPackageConfiguration packageConfiguration); +} \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj.DotSettings b/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj.DotSettings index 95499d42..76d90d5b 100644 --- a/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj.DotSettings +++ b/src/RepoM.Core.Plugin/RepoM.Core.Plugin.csproj.DotSettings @@ -1,2 +1,3 @@  + Library False \ No newline at end of file diff --git a/src/RepoM.Core.Plugin/SimpleInjector/IPackage.cs b/src/RepoM.Core.Plugin/SimpleInjector/IPackage.cs index d37e9a7d..a1ff15b6 100644 --- a/src/RepoM.Core.Plugin/SimpleInjector/IPackage.cs +++ b/src/RepoM.Core.Plugin/SimpleInjector/IPackage.cs @@ -5,6 +5,7 @@ namespace SimpleInjector.Packaging { using System.Collections.Generic; using System.Reflection; + using System.Threading.Tasks; /// /// Contract for types allow registering a set of services. diff --git a/src/RepoM.Core.Plugin/SimpleInjector/PackageExtensionsRepoM.cs b/src/RepoM.Core.Plugin/SimpleInjector/PackageExtensionsRepoM.cs new file mode 100644 index 00000000..7beb4703 --- /dev/null +++ b/src/RepoM.Core.Plugin/SimpleInjector/PackageExtensionsRepoM.cs @@ -0,0 +1,83 @@ +// Copyright (c) Simple Injector Contributors. All rights reserved. +// Licensed under the MIT License. See LICENSE file in the project root for license information. + +// This class is placed in the root namespace to allow users to start using these extension methods after +// adding the assembly reference, without find and add the correct namespace. +namespace SimpleInjector +{ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading.Tasks; + using RepoM.Core.Plugin; + using SimpleInjector.Packaging; + + /// + /// Extension methods for working with packages. + /// + public static class PackageExtensionsRepoM + { + /// + /// Loads all implementations from the given set of + /// and calls their Register method. + /// Note that only publicly exposed classes that contain a public default constructor will be loaded. + /// + /// The container to which the packages will be applied to. + /// The assemblies that will be searched for packages. + /// The factory method to create an instance based. + /// Thrown when the is a null + /// reference. + public static Task RegisterPackagesAsync(this Container container, IEnumerable assemblies, Func packageConfigurationFactoryMethod) + { + if (container is null) + { + throw new ArgumentNullException(nameof(container)); + } + + if (assemblies is null) + { + throw new ArgumentNullException(nameof(assemblies)); + } + + if (packageConfigurationFactoryMethod == null) + { + throw new ArgumentNullException(nameof(packageConfigurationFactoryMethod)); + } + + return RegisterPackagesInnerAsync(container, assemblies, packageConfigurationFactoryMethod); + } + + private static async Task RegisterPackagesInnerAsync(Container container, IEnumerable assemblies, Func packageConfigurationFactoryMethod) + { + foreach (Assembly assembly in assemblies) + { + var assemblyName = assembly.GetName().Name ?? string.Empty; + + foreach (IPackage package in container.GetPackagesToRegister(new[] { assembly, })) + { + if (package is IPackageWithConfiguration packageWithConfiguration) + { + var fileName = assemblyName; + if (fileName.StartsWith("RepoM.Plugin.")) + { + fileName = fileName["RepoM.Plugin.".Length..]; + } + + if (!string.IsNullOrWhiteSpace(packageWithConfiguration.Name)) + { + fileName += "." + packageWithConfiguration.Name; + } + + await packageWithConfiguration + .RegisterServicesAsync(container, packageConfigurationFactoryMethod.Invoke(fileName)) + .ConfigureAwait(false); + } + else + { + package.RegisterServices(container); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs b/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs index 921a9468..5c0f97f1 100644 --- a/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs +++ b/src/RepoM.Plugin.AzureDevOps/AzureDevOpsPackage.cs @@ -1,19 +1,78 @@ namespace RepoM.Plugin.AzureDevOps; +using System; +using System.Threading.Tasks; using JetBrains.Annotations; +using RepoM.Api.Common; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Core.Plugin; using RepoM.Core.Plugin.RepositoryFiltering; using RepoM.Plugin.AzureDevOps.ActionProvider; using RepoM.Plugin.AzureDevOps.Internal; +using RepoM.Plugin.AzureDevOps.PersistentConfiguration; using RepoM.Plugin.AzureDevOps.RepositoryFiltering; using SimpleInjector; using SimpleInjector.Packaging; [UsedImplicitly] -public class AzureDevOpsPackage : IPackage +public class AzureDevOpsPackage : IPackageWithConfiguration { - public void RegisterServices(Container container) + public string Name => "AzureDevOpsPackage"; // do not change this name, it is part of the persistant filename + + public async Task RegisterServicesAsync(Container container, IPackageConfiguration packageConfiguration) + { + await ExtractAndRegisterConfiguration(container, packageConfiguration).ConfigureAwait(false); + RegisterServices(container); + } + + private static async Task ExtractAndRegisterConfiguration(Container container, IPackageConfiguration packageConfiguration) + { + var version = await packageConfiguration.GetConfigurationVersionAsync().ConfigureAwait(false); + + AzureDevopsConfigV1 config; + if (version == CurrentConfigVersion.VERSION) + { + AzureDevopsConfigV1? result = await packageConfiguration.LoadConfigurationAsync().ConfigureAwait(false); + config = result ?? new AzureDevopsConfigV1(); + } + else + { + config = new AzureDevopsConfigV1(); + await packageConfiguration.PersistConfigurationAsync(config, CurrentConfigVersion.VERSION).ConfigureAwait(false); + } + + // this is temporarly to support the old way of storing the configuration + if (string.IsNullOrWhiteSpace(config.BaseUrl) && string.IsNullOrWhiteSpace(config.PersonalAccessToken)) + { + container.RegisterSingleton(() => + { + IAppSettingsService appSettingsService = container.GetInstance(); + + var c = new AzureDevopsConfigV1 + { + PersonalAccessToken = appSettingsService.AzureDevOpsPersonalAccessToken, + BaseUrl = appSettingsService.AzureDevOpsBaseUrl, + }; + _ = packageConfiguration.PersistConfigurationAsync(c, CurrentConfigVersion.VERSION); // do not await + + return new AzureDevopsConfiguration(c.BaseUrl, c.PersonalAccessToken); + }); + } + else + { + container.RegisterSingleton(() => + { + IAppSettingsService appSettingsService = container.GetInstance(); + + appSettingsService.AzureDevOpsBaseUrl = "This value has been copied to the new configuration file for this module."; + appSettingsService.AzureDevOpsPersonalAccessToken = "This value has been copied to the new configuration file for this module."; + + return new AzureDevopsConfiguration(config.BaseUrl, config.PersonalAccessToken); + }); + } + } + + private static void RegisterServices(Container container) { container.Collection.Append(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); @@ -25,4 +84,9 @@ public void RegisterServices(Container container) container.Collection.Append(Lifestyle.Singleton); container.Collection.Append(() => new HasPullRequestsMatcher(container.GetInstance(), true), Lifestyle.Singleton); } + + void IPackage.RegisterServices(Container container) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs index 4edd51cf..a768869b 100644 --- a/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs +++ b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevOpsPullRequestService.cs @@ -15,7 +15,6 @@ namespace RepoM.Plugin.AzureDevOps.Internal; using Microsoft.VisualStudio.Services.Common; using Microsoft.VisualStudio.Services.WebApi; using Newtonsoft.Json; -using RepoM.Api.Common; using RepoM.Api.IO; using RepoM.Core.Plugin.Repository; @@ -23,7 +22,7 @@ internal sealed partial class AzureDevOpsPullRequestService : IAzureDevOpsPullRe { private static readonly Regex _workItemRegex = DevOpsTaskMatchingRegex(); private readonly HttpClient _httpClient; - private readonly IAppSettingsService _appSettingsService; + private readonly IAzureDevopsConfiguration _configuration; private readonly ILogger _logger; private readonly VssConnection? _connection; private GitHttpClient? _azureDevopsGitClient; @@ -36,23 +35,20 @@ internal sealed partial class AzureDevOpsPullRequestService : IAzureDevOpsPullRe private readonly ConcurrentDictionary _gitRepositoriesPerProject = new(); private readonly ConcurrentDictionary _devOpsGitRepositories = new(); // Guid is the repository guid. - public AzureDevOpsPullRequestService(IAppSettingsService appSettingsService, ILogger logger) + public AzureDevOpsPullRequestService(IAzureDevopsConfiguration configuration, ILogger logger) { _httpClient = new(); - _appSettingsService = appSettingsService ?? throw new ArgumentNullException(nameof(appSettingsService)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - var token = _appSettingsService.AzureDevOpsPersonalAccessToken; - try { - Uri baseUrl = new(_appSettingsService.AzureDevOpsBaseUrl); _connection = new VssConnection( - baseUrl, - new VssBasicCredential(string.Empty, token)); - _httpClient.BaseAddress = baseUrl; + _configuration.AzureDevOpsBaseUrl, + new VssBasicCredential(string.Empty, _configuration.AzureDevOpsPersonalAccessToken)); + _httpClient.BaseAddress = _configuration.AzureDevOpsBaseUrl; _httpClient.DefaultRequestHeaders.Accept.Add(new("application/json")); - _httpClient.DefaultRequestHeaders.Authorization = new("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($":{_appSettingsService.AzureDevOpsPersonalAccessToken}"))); + _httpClient.DefaultRequestHeaders.Authorization = new("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($":{_configuration.AzureDevOpsPersonalAccessToken}"))); } catch (Exception e) { @@ -68,10 +64,10 @@ public Task InitializeAsync() return Task.CompletedTask; } - var key = _appSettingsService.AzureDevOpsPersonalAccessToken; + var key = _configuration.AzureDevOpsPersonalAccessToken; if (string.IsNullOrWhiteSpace(key)) { - _logger.LogInformation($"'{nameof(_appSettingsService.AzureDevOpsPersonalAccessToken)}' was null or empty. Module will not be enabled."); + _logger.LogInformation($"'{nameof(_configuration.AzureDevOpsPersonalAccessToken)}' was null or empty. Module will not be enabled."); return Task.CompletedTask; } diff --git a/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevopsConfiguration.cs b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevopsConfiguration.cs new file mode 100644 index 00000000..445ac513 --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/Internal/AzureDevopsConfiguration.cs @@ -0,0 +1,24 @@ +namespace RepoM.Plugin.AzureDevOps.Internal; + +using System; + +internal class AzureDevopsConfiguration : IAzureDevopsConfiguration +{ + public AzureDevopsConfiguration(string? url, string? pat) + { + AzureDevOpsPersonalAccessToken = pat; + + try + { + AzureDevOpsBaseUrl = url != null ? new Uri(url) : null!; + } + catch (Exception) + { + AzureDevOpsBaseUrl = null; + } + } + + public string? AzureDevOpsPersonalAccessToken { get; } + + public Uri? AzureDevOpsBaseUrl { get; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/Internal/IAzureDevopsConfiguration.cs b/src/RepoM.Plugin.AzureDevOps/Internal/IAzureDevopsConfiguration.cs new file mode 100644 index 00000000..6e0e3001 --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/Internal/IAzureDevopsConfiguration.cs @@ -0,0 +1,10 @@ +namespace RepoM.Plugin.AzureDevOps.Internal; + +using System; + +internal interface IAzureDevopsConfiguration +{ + string? AzureDevOpsPersonalAccessToken { get; } + + Uri? AzureDevOpsBaseUrl { get; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV1.cs b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV1.cs new file mode 100644 index 00000000..dad9221b --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/AzureDevopsConfigV1.cs @@ -0,0 +1,9 @@ +namespace RepoM.Plugin.AzureDevOps.PersistentConfiguration; + +/// DO NOT CHANGE PROPERTYNAMES, TYPES, or VISIBILITIES +public class AzureDevopsConfigV1 +{ + public string? PersonalAccessToken { get; init; } + + public string? BaseUrl { get; init; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/CurrentVersion.cs b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/CurrentVersion.cs new file mode 100644 index 00000000..b2cbd5f7 --- /dev/null +++ b/src/RepoM.Plugin.AzureDevOps/PersistentConfiguration/CurrentVersion.cs @@ -0,0 +1,6 @@ +namespace RepoM.Plugin.AzureDevOps.PersistentConfiguration; + +internal static class CurrentConfigVersion +{ + public const int VERSION = 1; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Heidi/HeidiPackage.cs b/src/RepoM.Plugin.Heidi/HeidiPackage.cs index b14156d0..ad8d5c73 100644 --- a/src/RepoM.Plugin.Heidi/HeidiPackage.cs +++ b/src/RepoM.Plugin.Heidi/HeidiPackage.cs @@ -1,19 +1,70 @@ namespace RepoM.Plugin.Heidi; +using System; +using System.Threading.Tasks; using JetBrains.Annotations; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Core.Plugin; using RepoM.Core.Plugin.VariableProviders; using RepoM.Plugin.Heidi.Internal; using SimpleInjector; -using SimpleInjector.Packaging; using RepoM.Plugin.Heidi.VariableProviders; using RepoM.Plugin.Heidi.ActionProvider; +using SimpleInjector.Packaging; +using RepoM.Plugin.Heidi.PersistentConfiguration; [UsedImplicitly] -public class HeidiPackage : IPackage +public class HeidiPackage : IPackageWithConfiguration { - public void RegisterServices(Container container) + public string Name => "HeidiPackage"; // do not change this name, it is part of the persistant filename + + public async Task RegisterServicesAsync(Container container, IPackageConfiguration packageConfiguration) + { + await ExtractAndRegisterConfiguration(container, packageConfiguration).ConfigureAwait(false); + RegisterServices(container); + } + + private static async Task ExtractAndRegisterConfiguration(Container container, IPackageConfiguration packageConfiguration) + { + var version = await packageConfiguration.GetConfigurationVersionAsync().ConfigureAwait(false); + + HeidiConfigV1 config; + if (version == CurrentConfigVersion.VERSION) + { + HeidiConfigV1? result = await packageConfiguration.LoadConfigurationAsync().ConfigureAwait(false); + config = result ?? new HeidiConfigV1(); + } + else + { + config = new HeidiConfigV1(); + await packageConfiguration.PersistConfigurationAsync(config, CurrentConfigVersion.VERSION).ConfigureAwait(false); + } + + // this is temporarly to support the old way of storing the configuration + if (string.IsNullOrWhiteSpace(config.ConfigPath) && string.IsNullOrWhiteSpace(config.ConfigFilename) && string.IsNullOrWhiteSpace(config.ExecutableFilename)) + { + container.RegisterSingleton(() => + { + var oldSettingsProvider = new EnvironmentVariablesHeidiSettings(); + + var c = new HeidiConfigV1 + { + ConfigPath = oldSettingsProvider.ConfigPath, + ConfigFilename = oldSettingsProvider.ConfigFilename, + ExecutableFilename = oldSettingsProvider.DefaultExe, + }; + _ = packageConfiguration.PersistConfigurationAsync(c, CurrentConfigVersion.VERSION); // do not await + + return new HeidiModuleConfiguration(c.ConfigPath, c.ConfigFilename, c.ExecutableFilename); + }); + } + else + { + container.RegisterInstance(new HeidiModuleConfiguration(config.ConfigPath, config.ConfigFilename, config.ExecutableFilename)); + } + } + + private static void RegisterServices(Container container) { RegisterPluginHooks(container); RegisterInternals(container); @@ -40,10 +91,14 @@ private static void RegisterPluginHooks(Container container) private static void RegisterInternals(Container container) { - container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.RegisterInstance(ExtractRepositoryFromHeidi.Instance); container.RegisterInstance(HeidiPasswordDecoder.Instance); } + + void IPackage.RegisterServices(Container container) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/RepoM.Plugin.Heidi/Internal/HeidiSettings.cs b/src/RepoM.Plugin.Heidi/Internal/EnvironmentVariablesHeidiSettings.cs similarity index 89% rename from src/RepoM.Plugin.Heidi/Internal/HeidiSettings.cs rename to src/RepoM.Plugin.Heidi/Internal/EnvironmentVariablesHeidiSettings.cs index f3bbb379..2116d276 100644 --- a/src/RepoM.Plugin.Heidi/Internal/HeidiSettings.cs +++ b/src/RepoM.Plugin.Heidi/Internal/EnvironmentVariablesHeidiSettings.cs @@ -2,7 +2,8 @@ namespace RepoM.Plugin.Heidi.Internal; using System; -internal class HeidiSettings : IHeidiSettings +[Obsolete("Will be removed in next major version.")] +internal class EnvironmentVariablesHeidiSettings : IHeidiSettings { private const string ENV_VAR_PREFIX = "REPOM_HEIDI_"; diff --git a/src/RepoM.Plugin.Heidi/Internal/ExtractRepositoryFromHeidi.cs b/src/RepoM.Plugin.Heidi/Internal/ExtractRepositoryFromHeidi.cs index b5f73eb8..17d7e22b 100644 --- a/src/RepoM.Plugin.Heidi/Internal/ExtractRepositoryFromHeidi.cs +++ b/src/RepoM.Plugin.Heidi/Internal/ExtractRepositoryFromHeidi.cs @@ -56,15 +56,7 @@ public bool TryExtract(HeidiSingleDatabaseConfiguration config, [NotNullWhen(tru if (!string.IsNullOrWhiteSpace(repo)) { repos.Add(repo.Trim()); - } - - // foreach (var c in repo) - // { - // //a-z, A-Z, 0-9, \s ._- - // - // - // } } } else @@ -94,7 +86,6 @@ public bool TryExtract(HeidiSingleDatabaseConfiguration config, [NotNullWhen(tru { k++; continue; - } stop = true; @@ -156,14 +147,7 @@ public bool TryExtract(HeidiSingleDatabaseConfiguration config, [NotNullWhen(tru if (!string.IsNullOrWhiteSpace(name)) { names.Add(name.Trim()); - } - // foreach (var c in repo) - // { - // //a-z, A-Z, 0-9, \s ._- - // - // - // } } } else diff --git a/src/RepoM.Plugin.Heidi/Internal/HeidiModuleConfiguration.cs b/src/RepoM.Plugin.Heidi/Internal/HeidiModuleConfiguration.cs new file mode 100644 index 00000000..e7357590 --- /dev/null +++ b/src/RepoM.Plugin.Heidi/Internal/HeidiModuleConfiguration.cs @@ -0,0 +1,17 @@ +namespace RepoM.Plugin.Heidi.Internal; + +internal class HeidiModuleConfiguration : IHeidiSettings +{ + public HeidiModuleConfiguration(string? configPath, string? configFilename, string? defaultExe) + { + ConfigPath = configPath ?? string.Empty; + ConfigFilename = configFilename ?? string.Empty; + DefaultExe = defaultExe ?? string.Empty; + } + + public string ConfigPath { get; } + + public string ConfigFilename { get; } + + public string DefaultExe { get; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Heidi/PersistentConfiguration/CurrentVersion.cs b/src/RepoM.Plugin.Heidi/PersistentConfiguration/CurrentVersion.cs new file mode 100644 index 00000000..dd7979d2 --- /dev/null +++ b/src/RepoM.Plugin.Heidi/PersistentConfiguration/CurrentVersion.cs @@ -0,0 +1,6 @@ +namespace RepoM.Plugin.Heidi.PersistentConfiguration; + +internal static class CurrentConfigVersion +{ + public const int VERSION = 1; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Heidi/PersistentConfiguration/HeidiConfigV1.cs b/src/RepoM.Plugin.Heidi/PersistentConfiguration/HeidiConfigV1.cs new file mode 100644 index 00000000..dd39d11b --- /dev/null +++ b/src/RepoM.Plugin.Heidi/PersistentConfiguration/HeidiConfigV1.cs @@ -0,0 +1,11 @@ +namespace RepoM.Plugin.Heidi.PersistentConfiguration; + +/// DO NOT CHANGE PROPERTYNAMES, TYPES, or VISIBILITIES +public class HeidiConfigV1 +{ + public string? ConfigPath { get; init; } + + public string? ConfigFilename { get; init; } + + public string? ExecutableFilename { get; init;} +} \ No newline at end of file diff --git a/src/RepoM.Plugin.SonarCloud/ISonarCloudConfiguration.cs b/src/RepoM.Plugin.SonarCloud/ISonarCloudConfiguration.cs new file mode 100644 index 00000000..23ed7c2b --- /dev/null +++ b/src/RepoM.Plugin.SonarCloud/ISonarCloudConfiguration.cs @@ -0,0 +1,9 @@ +namespace RepoM.Plugin.SonarCloud; + +internal interface ISonarCloudConfiguration +{ + string? PersonalAccessToken { get; } + + string? BaseUrl { get; } + +} \ No newline at end of file diff --git a/src/RepoM.Plugin.SonarCloud/PersistentConfiguration/CurrentVersion.cs b/src/RepoM.Plugin.SonarCloud/PersistentConfiguration/CurrentVersion.cs new file mode 100644 index 00000000..18d15838 --- /dev/null +++ b/src/RepoM.Plugin.SonarCloud/PersistentConfiguration/CurrentVersion.cs @@ -0,0 +1,6 @@ +namespace RepoM.Plugin.SonarCloud.PersistentConfiguration; + +internal static class CurrentConfigVersion +{ + public const int VERSION = 1; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.SonarCloud/PersistentConfiguration/SonarCloudConfigV1.cs b/src/RepoM.Plugin.SonarCloud/PersistentConfiguration/SonarCloudConfigV1.cs new file mode 100644 index 00000000..bec5bbc1 --- /dev/null +++ b/src/RepoM.Plugin.SonarCloud/PersistentConfiguration/SonarCloudConfigV1.cs @@ -0,0 +1,9 @@ +namespace RepoM.Plugin.SonarCloud.PersistentConfiguration; + +/// DO NOT CHANGE PROPERTYNAMES, TYPES, or VISIBILITIES +public class SonarCloudConfigV1 +{ + public string? PersonalAccessToken { get; init; } + + public string? BaseUrl { get; init; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.SonarCloud/SonarCloudConfiguration.cs b/src/RepoM.Plugin.SonarCloud/SonarCloudConfiguration.cs new file mode 100644 index 00000000..a476450d --- /dev/null +++ b/src/RepoM.Plugin.SonarCloud/SonarCloudConfiguration.cs @@ -0,0 +1,14 @@ +namespace RepoM.Plugin.SonarCloud; + +internal class SonarCloudConfiguration : ISonarCloudConfiguration +{ + public SonarCloudConfiguration(string? url, string? pat) + { + PersonalAccessToken = pat; + BaseUrl = url ?? "https://sonarcloud.io"; + } + + public string? PersonalAccessToken { get; } + + public string? BaseUrl { get; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs b/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs index 0acfacfc..10169803 100644 --- a/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs +++ b/src/RepoM.Plugin.SonarCloud/SonarCloudFavoriteService.cs @@ -4,34 +4,31 @@ namespace RepoM.Plugin.SonarCloud; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using RepoM.Api.Common; using SonarQube.Net; using SonarQube.Net.Common.Authentication; using SonarQube.Net.Models; internal class SonarCloudFavoriteService : ISonarCloudFavoriteService { - const string SONAR_CLOUD_URL = "https://sonarcloud.io"; - - private readonly IAppSettingsService _appSettingsService; + private readonly ISonarCloudConfiguration _appSettingsService; private SonarQubeClient? _client; private Task _task = Task.CompletedTask; private List _favorites = new(0); - public SonarCloudFavoriteService(IAppSettingsService appSettingsService) + public SonarCloudFavoriteService(ISonarCloudConfiguration appSettingsService) { _appSettingsService = appSettingsService ?? throw new ArgumentNullException(nameof(appSettingsService)); } public Task InitializeAsync() { - var key = _appSettingsService.SonarCloudPersonalAccessToken; + var key = _appSettingsService.PersonalAccessToken; if (string.IsNullOrWhiteSpace(key)) { return Task.CompletedTask; } - _client = new SonarQubeClient(SONAR_CLOUD_URL, new BasicAuthentication(key, string.Empty)); + _client = new SonarQubeClient(_appSettingsService.BaseUrl, new BasicAuthentication(key, string.Empty)); _task = Task.Run(async () => { diff --git a/src/RepoM.Plugin.SonarCloud/SonarCloudPackage.cs b/src/RepoM.Plugin.SonarCloud/SonarCloudPackage.cs index 66dbb21c..b373e493 100644 --- a/src/RepoM.Plugin.SonarCloud/SonarCloudPackage.cs +++ b/src/RepoM.Plugin.SonarCloud/SonarCloudPackage.cs @@ -1,16 +1,72 @@ namespace RepoM.Plugin.SonarCloud; +using System; +using System.Threading.Tasks; using ExpressionStringEvaluator.Methods; using JetBrains.Annotations; +using RepoM.Api.Common; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; using RepoM.Core.Plugin; +using RepoM.Plugin.SonarCloud.PersistentConfiguration; using SimpleInjector; using SimpleInjector.Packaging; [UsedImplicitly] -public class SonarCloudPackage : IPackage +public class SonarCloudPackage : IPackageWithConfiguration { - public void RegisterServices(Container container) + public string Name => "SonarCloudPackage"; // do not change this name, it is part of the persistant filename + + public async Task RegisterServicesAsync(Container container, IPackageConfiguration packageConfiguration) + { + await ExtractAndRegisterConfiguration(container, packageConfiguration).ConfigureAwait(false); + RegisterServices(container); + } + + private static async Task ExtractAndRegisterConfiguration(Container container, IPackageConfiguration packageConfiguration) + { + var version = await packageConfiguration.GetConfigurationVersionAsync().ConfigureAwait(false); + + SonarCloudConfigV1 config; + if (version == CurrentConfigVersion.VERSION) + { + SonarCloudConfigV1? result = await packageConfiguration.LoadConfigurationAsync().ConfigureAwait(false); + config = result ?? new SonarCloudConfigV1(); + } + else + { + config = new SonarCloudConfigV1(); + await packageConfiguration.PersistConfigurationAsync(config, CurrentConfigVersion.VERSION).ConfigureAwait(false); + } + + // this is temporarly to support the old way of storing the configuration + if (string.IsNullOrWhiteSpace(config.BaseUrl) && string.IsNullOrWhiteSpace(config.PersonalAccessToken)) + { + container.RegisterSingleton(() => + { + IAppSettingsService appSettingsService = container.GetInstance(); + + var c = new SonarCloudConfigV1 + { + PersonalAccessToken = appSettingsService.SonarCloudPersonalAccessToken, + BaseUrl = "https://sonarcloud.io", + }; + _ = packageConfiguration.PersistConfigurationAsync(c, CurrentConfigVersion.VERSION); // fire and forget ;-) + + return new SonarCloudConfiguration(c.BaseUrl, c.PersonalAccessToken); + }); + } + else + { + container.RegisterSingleton(() => + { + IAppSettingsService appSettingsService = container.GetInstance(); + appSettingsService.SonarCloudPersonalAccessToken = "This value has been copied to the new configuration file for this module."; + return new SonarCloudConfiguration(config.BaseUrl, config.PersonalAccessToken); + }); + } + } + + private static void RegisterServices(Container container) { container.Collection.Append(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); @@ -18,4 +74,9 @@ public void RegisterServices(Container container) container.Register(Lifestyle.Singleton); container.Collection.Append(Lifestyle.Singleton); } + + void IPackage.RegisterServices(Container container) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/IReadOnlyRepositoryStatistics.cs b/src/RepoM.Plugin.Statistics/IReadOnlyRepositoryStatistics.cs index 720df6e6..b64f49cd 100644 --- a/src/RepoM.Plugin.Statistics/IReadOnlyRepositoryStatistics.cs +++ b/src/RepoM.Plugin.Statistics/IReadOnlyRepositoryStatistics.cs @@ -5,6 +5,8 @@ namespace RepoM.Plugin.Statistics; public interface IReadOnlyRepositoryStatistics { int GetRecordingCount(DateTime from, DateTime to); + int GetRecordingCountFrom(DateTime from); + int GetRecordingCountBefore(DateTime to); } \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/IStatisticsConfiguration.cs b/src/RepoM.Plugin.Statistics/IStatisticsConfiguration.cs new file mode 100644 index 00000000..d0150f7b --- /dev/null +++ b/src/RepoM.Plugin.Statistics/IStatisticsConfiguration.cs @@ -0,0 +1,10 @@ +namespace RepoM.Plugin.Statistics; + +using System; + +internal interface IStatisticsConfiguration +{ + public TimeSpan PersistenceBuffer { get; } + + public int RetentionDays { get; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/PersistentConfiguration/CurrentVersion.cs b/src/RepoM.Plugin.Statistics/PersistentConfiguration/CurrentVersion.cs new file mode 100644 index 00000000..4055fd91 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/PersistentConfiguration/CurrentVersion.cs @@ -0,0 +1,6 @@ +namespace RepoM.Plugin.Statistics.PersistentConfiguration; + +internal static class CurrentConfigVersion +{ + public const int VERSION = 1; +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/PersistentConfiguration/StatisticsConfigV1.cs b/src/RepoM.Plugin.Statistics/PersistentConfiguration/StatisticsConfigV1.cs new file mode 100644 index 00000000..2275ffca --- /dev/null +++ b/src/RepoM.Plugin.Statistics/PersistentConfiguration/StatisticsConfigV1.cs @@ -0,0 +1,11 @@ +namespace RepoM.Plugin.Statistics.PersistentConfiguration; + +using System; + +/// DO NOT CHANGE PROPERTYNAMES, TYPES, or VISIBILITIES +public class StatisticsConfigV1 +{ + public TimeSpan? PersistenceBuffer { get; init; } + + public int? RetentionDays { get; init; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/RepositoryStatistics.cs b/src/RepoM.Plugin.Statistics/RepositoryStatistics.cs index f59a17d8..c935df8f 100644 --- a/src/RepoM.Plugin.Statistics/RepositoryStatistics.cs +++ b/src/RepoM.Plugin.Statistics/RepositoryStatistics.cs @@ -55,7 +55,7 @@ public void Apply(IEvent evt) return; } - throw new NotImplementedException(); + throw new InvalidOperationException($"Type '{evt.GetType().Name}' is unknown"); } private void Apply(RepositoryActionRecordedEvent evt) diff --git a/src/RepoM.Plugin.Statistics/StatisticsConfiguration.cs b/src/RepoM.Plugin.Statistics/StatisticsConfiguration.cs new file mode 100644 index 00000000..8bfd6321 --- /dev/null +++ b/src/RepoM.Plugin.Statistics/StatisticsConfiguration.cs @@ -0,0 +1,10 @@ +namespace RepoM.Plugin.Statistics; + +using System; + +internal class StatisticsConfiguration : IStatisticsConfiguration +{ + public TimeSpan PersistenceBuffer { get; init; } + + public int RetentionDays { get; init; } +} \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/StatisticsModule.cs b/src/RepoM.Plugin.Statistics/StatisticsModule.cs index a0015378..399e0f38 100644 --- a/src/RepoM.Plugin.Statistics/StatisticsModule.cs +++ b/src/RepoM.Plugin.Statistics/StatisticsModule.cs @@ -18,6 +18,7 @@ namespace RepoM.Plugin.Statistics; internal class StatisticsModule : IModule { private readonly IStatisticsService _service; + private readonly IStatisticsConfiguration _configuration; private readonly IClock _clock; private readonly IAppDataPathProvider _pathProvider; private readonly IFileSystem _fileSystem; @@ -28,12 +29,14 @@ internal class StatisticsModule : IModule public StatisticsModule( IStatisticsService service, + IStatisticsConfiguration configuration, IClock clock, IAppDataPathProvider pathProvider, IFileSystem fileSystem, ILogger logger) { _service = service ?? throw new ArgumentNullException(nameof(service)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _pathProvider = pathProvider ?? throw new ArgumentNullException(nameof(pathProvider)); _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); @@ -73,23 +76,23 @@ private async Task RemoveOldFilesAsync() IOrderedEnumerable orderedEnumerable = _fileSystem.Directory.GetFiles(_basePath, "statistics.v1.*.json").OrderBy(f => f); - DateTime threshold = _clock.Now.AddDays(-30); + DateTime threshold = _clock.Now.AddDays(-1 * _configuration.RetentionDays); foreach (var file in orderedEnumerable) { - IEvent[] list = Array.Empty(); + IEvent[] events = Array.Empty(); try { var json = await _fileSystem.File.ReadAllTextAsync(file, CancellationToken.None).ConfigureAwait(false); - list = JsonConvert.DeserializeObject(json, _settings) ?? Array.Empty(); + events = JsonConvert.DeserializeObject(json, _settings) ?? Array.Empty(); } catch (Exception e) { _logger.LogError(e, "Could not read or deserialize data from '{filename}'. {message}", file, e.Message); } - if (list.All(item => item.Timestamp <= threshold)) + if (Array.TrueForAll(events, item => item.Timestamp <= threshold)) { try { @@ -136,10 +139,16 @@ private async Task ProcessEventsFromFile() private IDisposable WriteEventsToFile() { + TimeSpan buffer = _configuration.PersistenceBuffer; + if (buffer < TimeSpan.FromSeconds(10)) + { + buffer = TimeSpan.FromSeconds(10); + } + return _service .Events .ObserveOn(Scheduler.Default) - .Buffer(TimeSpan.FromMinutes(5)) + .Buffer(buffer) .Subscribe(data => { IEvent[] events = data.ToArray(); diff --git a/src/RepoM.Plugin.Statistics/StatisticsPackage.cs b/src/RepoM.Plugin.Statistics/StatisticsPackage.cs index d881134a..7e4040e2 100644 --- a/src/RepoM.Plugin.Statistics/StatisticsPackage.cs +++ b/src/RepoM.Plugin.Statistics/StatisticsPackage.cs @@ -1,5 +1,7 @@ namespace RepoM.Plugin.Statistics; +using System; +using System.Threading.Tasks; using JetBrains.Annotations; using RepoM.Core.Plugin; using RepoM.Core.Plugin.RepositoryActions; @@ -7,20 +9,65 @@ namespace RepoM.Plugin.Statistics; using RepoM.Core.Plugin.RepositoryOrdering.Configuration; using RepoM.Core.Plugin.VariableProviders; using RepoM.Plugin.Statistics.Ordering; +using RepoM.Plugin.Statistics.PersistentConfiguration; using RepoM.Plugin.Statistics.RepositoryActions; using RepoM.Plugin.Statistics.VariableProviders; using SimpleInjector; using SimpleInjector.Packaging; [UsedImplicitly] -public class StatisticsPackage : IPackage +public class StatisticsPackage : IPackageWithConfiguration { - public void RegisterServices(Container container) + public string Name => "StatisticsPackage"; // do not change this name, it is part of the persistant filename + + public async Task RegisterServicesAsync(Container container, IPackageConfiguration packageConfiguration) { + await ExtractAndRegisterConfiguration(container, packageConfiguration).ConfigureAwait(false); RegisterPluginHooks(container); RegisterInternals(container); } + private static async Task ExtractAndRegisterConfiguration(Container container, IPackageConfiguration packageConfiguration) + { + var version = await packageConfiguration.GetConfigurationVersionAsync().ConfigureAwait(false); + + var config = new StatisticsConfigV1 + { + PersistenceBuffer = TimeSpan.FromMinutes(5), + RetentionDays = 30, + }; + + if (version == CurrentConfigVersion.VERSION) + { + StatisticsConfigV1? result = await packageConfiguration.LoadConfigurationAsync().ConfigureAwait(false); + if (result == null) + { + await packageConfiguration.PersistConfigurationAsync(config, CurrentConfigVersion.VERSION).ConfigureAwait(false); + } + else + { + config = result; + } + } + else + { + await packageConfiguration.PersistConfigurationAsync(config, CurrentConfigVersion.VERSION).ConfigureAwait(false); + } + + var retentionDays = config.RetentionDays ?? 30; + if (retentionDays < 0) + { + retentionDays *= -1; + } + + container.RegisterInstance( + new StatisticsConfiguration + { + PersistenceBuffer = config.PersistenceBuffer ?? TimeSpan.FromMinutes(5), + RetentionDays= retentionDays, + }); + } + private static void RegisterPluginHooks(Container container) { // ordering @@ -44,4 +91,9 @@ private static void RegisterInternals(Container container) { container.Register(Lifestyle.Singleton); } + + void IPackage.RegisterServices(Container container) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/RepoM.Plugin.Statistics/StatisticsService.cs b/src/RepoM.Plugin.Statistics/StatisticsService.cs index a93083d4..b20b31f5 100644 --- a/src/RepoM.Plugin.Statistics/StatisticsService.cs +++ b/src/RepoM.Plugin.Statistics/StatisticsService.cs @@ -11,7 +11,7 @@ namespace RepoM.Plugin.Statistics; using RepoM.Core.Plugin.Repository; using RepoM.Plugin.Statistics.Interface; -public class StatisticsService : IStatisticsService +internal class StatisticsService : IStatisticsService { private readonly IClock _clock; private readonly ReadOnlyCollection _empty = new List(0).AsReadOnly(); @@ -55,7 +55,7 @@ public IReadOnlyList GetRecordings(IRepository repository) public int GetTotalRecordingCount() { - return _recordings.Select(x => x.Value.Recordings.Count).Sum(); + return _recordings.Select(repo => repo.Value.Recordings.Count).Sum(); } public IReadOnlyRepositoryStatistics? GetRepositoryRecording(IRepository repository) diff --git a/tests/RepoM.App.Tests/Services/FileBasedPackageConfigurationTest.cs b/tests/RepoM.App.Tests/Services/FileBasedPackageConfigurationTest.cs new file mode 100644 index 00000000..9fb77c11 --- /dev/null +++ b/tests/RepoM.App.Tests/Services/FileBasedPackageConfigurationTest.cs @@ -0,0 +1,86 @@ +namespace RepoM.App.Tests.Services; + +using System; +using System.IO.Abstractions; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using RepoM.App.Services; +using RepoM.Core.Plugin.Common; +using Xunit; + +public class FileBasedPackageConfigurationTest +{ + private readonly IAppDataPathProvider _appDataPathProvider; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly string _filename; + + public FileBasedPackageConfigurationTest() + { + _appDataPathProvider = A.Fake(); + _fileSystem = A.Fake(); + _logger = A.Fake(); + _filename = "dummy"; + + A.CallTo(() => _appDataPathProvider.GetAppDataPath()).Returns("C:\\tmp-test\\"); + A.CallTo(() => _fileSystem.File.Exists("C:\\tmp-test\\Module\\dummy.json")).Returns(true); + } + + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new FileBasedPackageConfiguration(_appDataPathProvider, _fileSystem, _logger, null!); + Func act2 = () => new FileBasedPackageConfiguration(_appDataPathProvider, _fileSystem, null!, _filename); + Func act3 = () => new FileBasedPackageConfiguration(_appDataPathProvider, null!, _logger, _filename); + Func act4 = () => new FileBasedPackageConfiguration(null!, _fileSystem, _logger, _filename); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + act3.Should().Throw(); + act4.Should().Throw(); + } + + [Fact] + public async Task GetConfigurationVersionAsync_ShouldReturnNull_WhenFileNotFound() + { + // arrange + + A.CallTo(() => _fileSystem.File.Exists("C:\\tmp-test\\Module\\dummy.json")).Returns(false); + var sut = new FileBasedPackageConfiguration(_appDataPathProvider, _fileSystem, _logger, _filename); + + // act + var result = await sut.GetConfigurationVersionAsync(); + + // assert + result.Should().BeNull(); + A.CallTo(() => _fileSystem.File.Exists("C:\\tmp-test\\Module\\dummy.json")).MustHaveHappenedOnceExactly(); + } + + [Theory] + [InlineData("invalid json")] + [InlineData("{ }")] + [InlineData("{ VersioN = 34 }")] + public async Task GetConfigurationVersionAsync_ShouldReturnNull_WhenFileDoesNotContainJson(string fileContent) + { + // arrange + + A.CallTo(() => _fileSystem.File.Exists("C:\\tmp-test\\Module\\dummy.json")).Returns(true); + A.CallTo(() => _fileSystem.File.ReadAllTextAsync("C:\\tmp-test\\Module\\dummy.json", A._)).ReturnsLazily(() => fileContent); + var sut = new FileBasedPackageConfiguration(_appDataPathProvider, _fileSystem, _logger, _filename); + + // act + var result = await sut.GetConfigurationVersionAsync(); + + // assert + result.Should().BeNull(); + A.CallTo(() => _fileSystem.File.Exists("C:\\tmp-test\\Module\\dummy.json")).MustHaveHappenedOnceExactly(); + A.CallTo(() => _fileSystem.File.ReadAllTextAsync("C:\\tmp-test\\Module\\dummy.json", A._)).MustHaveHappenedOnceExactly(); + } +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig.verified.txt b/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig.verified.txt new file mode 100644 index 00000000..b35d4656 --- /dev/null +++ b/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig.verified.txt @@ -0,0 +1,4 @@ +{ + PersonalAccessToken: MY_TEST_PAT, + BaseUrl: https://dev.azure.com/MyOrg123ABC +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs b/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs index f7aceefd..baf336f8 100644 --- a/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs +++ b/tests/RepoM.Plugin.AzureDevOps.Tests/AzureDevOpsPackageTests.cs @@ -1,52 +1,125 @@ namespace RepoM.Plugin.AzureDevOps.Tests; using System; +using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Logging; using RepoM.Api.Common; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; +using RepoM.Core.Plugin; using RepoM.Core.Plugin.Expressions; +using RepoM.Plugin.AzureDevOps.PersistentConfiguration; using SimpleInjector; +using VerifyXunit; using Xunit; +[UsesVerify] public class AzureDevOpsPackageTests { + private readonly Container _container; + private readonly IPackageConfiguration _packageConfiguration; + private readonly IAppSettingsService _appSettingsService; + + public AzureDevOpsPackageTests() + { + _packageConfiguration = A.Fake(); + _appSettingsService = A.Fake(); + _container = new Container(); + + var azureDevopsConfigV1 = new AzureDevopsConfigV1 + { + PersonalAccessToken = "PAT", + BaseUrl = "https://dev.azure.com/MyOrg", + }; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(1 as int?)); + A.CallTo(() => _packageConfiguration.LoadConfigurationAsync()).ReturnsLazily(() => azureDevopsConfigV1); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).Returns(Task.CompletedTask); + + A.CallTo(() => _appSettingsService.AzureDevOpsBaseUrl).Returns("https://dev.azure.com/MyOrg123ABC"); + A.CallTo(() => _appSettingsService.AzureDevOpsPersonalAccessToken).Returns("MY_TEST_PAT"); + } + [Fact] - public void RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() + public async Task RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() { // arrange - var container = new Container(); - RegisterExternals(container); + RegisterExternals(_container); var sut = new AzureDevOpsPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert // implicit, Verify throws when container is not valid. - container.Verify(VerificationOption.VerifyAndDiagnose); + _container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(10)] + public async Task RegisterServices_ShouldPersistNewConfig_WhenVersionIsNotCorrect(int? version) + { + // arrange + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); + RegisterExternals(_container); + var sut = new AzureDevOpsPackage(); + + // act + await sut.RegisterServicesAsync(_container, _packageConfiguration); + + // assert + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + + // implicit, Verify throws when container is not valid. + _container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(10)] + public async Task RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig(int? version) + { + // arrange + AzureDevopsConfigV1? persistedConfig = null; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); + RegisterExternals(_container); + var sut = new AzureDevOpsPackage(); + await sut.RegisterServicesAsync(_container, _packageConfiguration); + + Fake.ClearRecordedCalls(_packageConfiguration); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)) + .Invokes(call => persistedConfig = call.Arguments[0] as AzureDevopsConfigV1); + + // act + // make sure everyting is resolved. This will trigger the copy of the config. + _container.Verify(VerificationOption.VerifyAndDiagnose); + + // assert + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + await Verifier.Verify(persistedConfig).IgnoreParametersForVerified(nameof(version)); } [Fact] - public void RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() + public async Task RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() { // arrange - var container = new Container(); var sut = new AzureDevOpsPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert - Assert.Throws(() => container.Verify(VerificationOption.VerifyAndDiagnose)); + Assert.Throws(() => _container.Verify(VerificationOption.VerifyAndDiagnose)); } - private static void RegisterExternals(Container container) + private void RegisterExternals(Container container) { container.RegisterSingleton(A.Dummy); container.RegisterSingleton(A.Dummy); container.RegisterSingleton(A.Dummy); - container.RegisterSingleton(A.Dummy); + container.RegisterInstance(_appSettingsService); container.RegisterSingleton(A.Dummy); } } \ No newline at end of file diff --git a/tests/RepoM.Plugin.AzureDevOps.Tests/Internal/AzureDevOpsPullRequestServiceTests.cs b/tests/RepoM.Plugin.AzureDevOps.Tests/Internal/AzureDevOpsPullRequestServiceTests.cs index aaeb862e..6e6b2b37 100644 --- a/tests/RepoM.Plugin.AzureDevOps.Tests/Internal/AzureDevOpsPullRequestServiceTests.cs +++ b/tests/RepoM.Plugin.AzureDevOps.Tests/Internal/AzureDevOpsPullRequestServiceTests.cs @@ -4,7 +4,6 @@ namespace RepoM.Plugin.AzureDevOps.Tests.Internal; using FakeItEasy; using FluentAssertions; using Microsoft.Extensions.Logging; -using RepoM.Api.Common; using RepoM.Plugin.AzureDevOps.Internal; using Xunit; @@ -16,7 +15,7 @@ public void Ctor_ShouldThrow_WhenArgumentNull() // arrange // act - Func act1 = () => new AzureDevOpsPullRequestService(A.Dummy(), null!); + Func act1 = () => new AzureDevOpsPullRequestService(A.Dummy(), null!); Func act2 = () => new AzureDevOpsPullRequestService(null!, A.Dummy()); // assert diff --git a/tests/RepoM.Plugin.Heidi.Tests/EnvironmentVariableManager.cs b/tests/RepoM.Plugin.Heidi.Tests/EnvironmentVariableManager.cs index 38622fb1..4fa6cc0e 100644 --- a/tests/RepoM.Plugin.Heidi.Tests/EnvironmentVariableManager.cs +++ b/tests/RepoM.Plugin.Heidi.Tests/EnvironmentVariableManager.cs @@ -4,6 +4,7 @@ namespace RepoM.Plugin.Heidi.Tests; using System.Collections.Concurrent; using System.Threading; + internal static class EnvironmentVariableManager { private static readonly ConcurrentDictionary _envVarLocks = new(); diff --git a/tests/RepoM.Plugin.Heidi.Tests/HeidiPackageTest.cs b/tests/RepoM.Plugin.Heidi.Tests/HeidiPackageTest.cs index 1323ecd7..9348e09b 100644 --- a/tests/RepoM.Plugin.Heidi.Tests/HeidiPackageTest.cs +++ b/tests/RepoM.Plugin.Heidi.Tests/HeidiPackageTest.cs @@ -2,44 +2,117 @@ namespace RepoM.Plugin.Heidi.Tests; using System; using System.IO.Abstractions; +using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Logging; using RepoM.Api.Common; using RepoM.Api.IO.ModuleBasedRepositoryActionProvider; +using RepoM.Core.Plugin; using RepoM.Core.Plugin.Expressions; +using RepoM.Plugin.Heidi.PersistentConfiguration; using SimpleInjector; using Xunit; public class HeidiPackageTest { + private readonly Container _container; + private readonly IPackageConfiguration _packageConfiguration; + + public HeidiPackageTest() + { + _packageConfiguration = A.Fake(); + _container = new Container(); + + var heidiConfigV1 = new HeidiConfigV1 + { + ConfigPath = "C:\\Config\\Path\\Test\\", + ConfigFilename = "portable.heidi.test.txt", + ExecutableFilename = "TestHeidiSQL.exe", + }; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(1 as int?)); + A.CallTo(() => _packageConfiguration.LoadConfigurationAsync()).ReturnsLazily(() => heidiConfigV1); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).Returns(Task.CompletedTask); + } + [Fact] - public void RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() + public async Task RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() + { + // arrange + RegisterExternals(_container); + var sut = new HeidiPackage(); + + // act + await sut.RegisterServicesAsync(_container, _packageConfiguration); + + // assert + // implicit, Verify throws when container is not valid. + _container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(10)] + public async Task RegisterServices_ShouldPersistNewConfig_WhenVersionIsNotCorrect(int? version) { // arrange - var container = new Container(); - RegisterExternals(container); + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); + RegisterExternals(_container); var sut = new HeidiPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + // implicit, Verify throws when container is not valid. - container.Verify(VerificationOption.VerifyAndDiagnose); + _container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(10)] + public async Task RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig(int? version) + { + // This test is not complete but has some issues running in AzureDevops. Propbably due to the environment variables. + // Because this functionality will be stripped in a few months (ie, october 2023) this test is not a priority. + + // arrange + // using IDisposable d1 = EnvironmentVariableManager.SetEnvironmentVariable("REPOM_HEIDI_CONFIG_PATH", "heidi-configpath-envvar"); + // using IDisposable d2 = EnvironmentVariableManager.SetEnvironmentVariable("REPOM_HEIDI_CONFIG_FILENAME", "heidi-filename-envvar"); + // using IDisposable d3 = EnvironmentVariableManager.SetEnvironmentVariable("REPOM_HEIDI_EXE", "heidi-exe-envvar"); + + HeidiConfigV1? persistedConfig = null; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); + RegisterExternals(_container); + var sut = new HeidiPackage(); + await sut.RegisterServicesAsync(_container, _packageConfiguration); + + Fake.ClearRecordedCalls(_packageConfiguration); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)) + .Invokes(call => persistedConfig = call.Arguments[0] as HeidiConfigV1); + + // act + // make sure everyting is resolved. This will trigger the copy of the config. + _container.Verify(VerificationOption.VerifyAndDiagnose); + + // assert + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); } [Fact] - public void RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() + public async Task RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() { // arrange - var container = new Container(); var sut = new HeidiPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert - Assert.Throws(() => container.Verify(VerificationOption.VerifyAndDiagnose)); + Assert.Throws(() => _container.Verify(VerificationOption.VerifyAndDiagnose)); } private static void RegisterExternals(Container container) diff --git a/tests/RepoM.Plugin.Heidi.Tests/Internal/HeidiSettingsTests.cs b/tests/RepoM.Plugin.Heidi.Tests/Internal/EnvironmentVariablesHeidiSettingsTests.cs similarity index 83% rename from tests/RepoM.Plugin.Heidi.Tests/Internal/HeidiSettingsTests.cs rename to tests/RepoM.Plugin.Heidi.Tests/Internal/EnvironmentVariablesHeidiSettingsTests.cs index 63e470f9..6b0c68d0 100644 --- a/tests/RepoM.Plugin.Heidi.Tests/Internal/HeidiSettingsTests.cs +++ b/tests/RepoM.Plugin.Heidi.Tests/Internal/EnvironmentVariablesHeidiSettingsTests.cs @@ -5,14 +5,14 @@ namespace RepoM.Plugin.Heidi.Tests.Internal; using RepoM.Plugin.Heidi.Internal; using Xunit; -public class HeidiSettingsTests +public class EnvironmentVariablesHeidiSettingsTests { [Fact] public void ConfigFilename_ShouldReturnDefault_WhenEnvironmentVariableIsUnset() { // arrange using IDisposable _ = EnvironmentVariableManager.SetEnvironmentVariable("REPOM_HEIDI_CONFIG_FILENAME", string.Empty); - var sut = new HeidiSettings(); + var sut = new EnvironmentVariablesHeidiSettings(); // act var result = sut.ConfigFilename; @@ -26,7 +26,7 @@ public void ConfigFilename_ShouldReturnEnvironmentVariable_WhenEnvironmentVariab { // arrange using IDisposable _ = EnvironmentVariableManager.SetEnvironmentVariable("REPOM_HEIDI_CONFIG_FILENAME", "Dummy@#"); - var sut = new HeidiSettings(); + var sut = new EnvironmentVariablesHeidiSettings(); // act var result = sut.ConfigFilename; diff --git a/tests/RepoM.Plugin.Heidi.Tests/Internal/ExtractRepositoryFromHeidiTests.cs b/tests/RepoM.Plugin.Heidi.Tests/Internal/ExtractRepositoryFromHeidiTests.cs index a5949b8e..9381b2f5 100644 --- a/tests/RepoM.Plugin.Heidi.Tests/Internal/ExtractRepositoryFromHeidiTests.cs +++ b/tests/RepoM.Plugin.Heidi.Tests/Internal/ExtractRepositoryFromHeidiTests.cs @@ -246,4 +246,24 @@ await Verifier.Verify(new }, _verifySettings); } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void TryExtract_ShouldReturnFalse_WhenCommentIsNullOrEmpty(string? comment) + { + // arrange + var dbSettings = new HeidiSingleDatabaseConfiguration("hk") + { + Comment = comment!, + }; + + // act + var result = _sut.TryExtract(dbSettings, out RepoHeidi? repo); + + // assert + result.Should().BeFalse(); + repo.Should().BeNull(); + } } \ No newline at end of file diff --git a/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudFavoriteServiceTests.cs b/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudFavoriteServiceTests.cs index 9d68f78f..33516d96 100644 --- a/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudFavoriteServiceTests.cs +++ b/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudFavoriteServiceTests.cs @@ -4,7 +4,6 @@ namespace RepoM.Plugin.SonarCloud.Tests; using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; -using RepoM.Api.Common; using Xunit; public class SonarCloudFavoriteServiceTest @@ -28,8 +27,8 @@ public void Ctor_ShouldThrow_WhenArgumentIsNull() public async Task InitializeAsync_ShouldNotInitialize_WhenPatInvalid(string? pat) { // arrange - IAppSettingsService appSettingsService = A.Fake(); - appSettingsService.SonarCloudPersonalAccessToken = pat!; + ISonarCloudConfiguration appSettingsService = A.Fake(); + A.CallTo(() => appSettingsService.PersonalAccessToken).Returns(pat!); var sut = new SonarCloudFavoriteService(appSettingsService); // act @@ -43,7 +42,7 @@ public async Task InitializeAsync_ShouldNotInitialize_WhenPatInvalid(string? pat public void SetFavorite_ShouldReturn_WhenNotInitialized() { // arrange - IAppSettingsService appSettingsService = A.Fake(); + ISonarCloudConfiguration appSettingsService = A.Fake(); var sut = new SonarCloudFavoriteService(appSettingsService); // assume @@ -60,7 +59,7 @@ public void SetFavorite_ShouldReturn_WhenNotInitialized() public void IsFavorite_ShouldReturnFalse_WhenNotInitialized() { // arrange - IAppSettingsService appSettingsService = A.Fake(); + ISonarCloudConfiguration appSettingsService = A.Fake(); var sut = new SonarCloudFavoriteService(appSettingsService); // assume diff --git a/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudPackageTest.RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig.verified.txt b/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudPackageTest.RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig.verified.txt new file mode 100644 index 00000000..46db2cd7 --- /dev/null +++ b/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudPackageTest.RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig.verified.txt @@ -0,0 +1,4 @@ +{ + PersonalAccessToken: MY_TEST_PAT_SONAR, + BaseUrl: https://sonarcloud.io +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudPackageTest.cs b/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudPackageTest.cs index 9dd652ab..fa8da3af 100644 --- a/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudPackageTest.cs +++ b/tests/RepoM.Plugin.SonarCloud.Tests/SonarCloudPackageTest.cs @@ -4,45 +4,116 @@ namespace RepoM.Plugin.SonarCloud.Tests; using FakeItEasy; using RepoM.Api.Common; using RepoM.Core.Plugin.Expressions; -using RepoM.Plugin.SonarCloud; using SimpleInjector; using Xunit; +using System.Threading.Tasks; +using RepoM.Core.Plugin; +using VerifyXunit; +using RepoM.Plugin.SonarCloud.PersistentConfiguration; +[UsesVerify] public class SonarCloudPackageTest { + private readonly Container _container; + private readonly IPackageConfiguration _packageConfiguration; + private readonly IAppSettingsService _appSettingsService; + + public SonarCloudPackageTest() + { + _packageConfiguration = A.Fake(); + _appSettingsService = A.Fake(); + _container = new Container(); + + var sonarCloudConfigV1 = new SonarCloudConfigV1 + { + PersonalAccessToken = "PATx", + BaseUrl = "https://sonarcloud.io", + }; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(1 as int?)); + A.CallTo(() => _packageConfiguration.LoadConfigurationAsync()).ReturnsLazily(() => sonarCloudConfigV1); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).Returns(Task.CompletedTask); + + A.CallTo(() => _appSettingsService.SonarCloudPersonalAccessToken).Returns("MY_TEST_PAT_SONAR"); + } + [Fact] - public void RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() + public async Task RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() { // arrange - var container = new Container(); - RegisterExternals(container); + RegisterExternals(_container); var sut = new SonarCloudPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert // implicit, Verify throws when container is not valid. - container.Verify(VerificationOption.VerifyAndDiagnose); + _container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(10)] + public async Task RegisterServices_ShouldPersistNewConfig_WhenVersionIsNotCorrect(int? version) + { + // arrange + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); + RegisterExternals(_container); + var sut = new SonarCloudPackage(); + + // act + await sut.RegisterServicesAsync(_container, _packageConfiguration); + + // assert + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + + // implicit, Verify throws when container is not valid. + _container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(10)] + public async Task RegisterServices_ShouldCopyExistingAppSettingsConfig_WhenNoCurrentCorrectConfig(int? version) + { + // arrange + SonarCloudConfigV1? persistedConfig = null; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); + RegisterExternals(_container); + var sut = new SonarCloudPackage(); + await sut.RegisterServicesAsync(_container, _packageConfiguration); + + Fake.ClearRecordedCalls(_packageConfiguration); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)) + .Invokes(call => persistedConfig = call.Arguments[0] as SonarCloudConfigV1); + + // act + // make sure everyting is resolved. This will trigger the copy of the config. + _container.Verify(VerificationOption.VerifyAndDiagnose); + + // assert + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + await Verifier.Verify(persistedConfig).IgnoreParametersForVerified(nameof(version)); } [Fact] - public void RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() + public async Task RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() { // arrange - var container = new Container(); var sut = new SonarCloudPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert - Assert.Throws(() => container.Verify(VerificationOption.VerifyAndDiagnose)); + Assert.Throws(() => _container.Verify(VerificationOption.VerifyAndDiagnose)); } - private static void RegisterExternals(Container container) + private void RegisterExternals(Container container) { - container.RegisterSingleton(A.Dummy); + container.RegisterInstance(_appSettingsService); container.RegisterSingleton(A.Dummy); container.RegisterSingleton(A.Dummy); } diff --git a/tests/RepoM.Plugin.Statistics.Tests/Ordering/IntegrationTest.cs b/tests/RepoM.Plugin.Statistics.Tests/Ordering/IntegrationTest.cs index 2d75aa25..8f2b9d81 100644 --- a/tests/RepoM.Plugin.Statistics.Tests/Ordering/IntegrationTest.cs +++ b/tests/RepoM.Plugin.Statistics.Tests/Ordering/IntegrationTest.cs @@ -1,5 +1,6 @@ namespace RepoM.Plugin.Statistics.Tests.Ordering; +using System; using System.Collections.Generic; using FakeItEasy; using System.IO.Abstractions; @@ -15,6 +16,8 @@ namespace RepoM.Plugin.Statistics.Tests.Ordering; using EasyTestFile; using VerifyTests; using RepoM.Core.Plugin.RepositoryOrdering.Configuration; +using RepoM.Core.Plugin; +using RepoM.Plugin.Statistics.PersistentConfiguration; [UsesEasyTestFile] [UsesVerify] @@ -28,8 +31,13 @@ public class IntegrationTest public IntegrationTest() { + var packageConfiguration = A.Fake(); + A.CallTo(() => packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(1 as int?)); + A.CallTo(() => packageConfiguration.LoadConfigurationAsync()) + .ReturnsLazily(() => new StatisticsConfigV1 { PersistenceBuffer = TimeSpan.FromMinutes(5), RetentionDays = 30, }); var container = new Container(); - new StatisticsPackage().RegisterServices(container); + var package = new StatisticsPackage(); + package.RegisterServicesAsync(container, packageConfiguration).GetAwaiter().GetResult(); _appDataPathProvider = A.Fake(); _fileSystem = new MockFileSystem(); @@ -65,6 +73,7 @@ public async Task LastOpenedConfiguration() // assert await Verifier.Verify(result, _verifySettings); } + [Fact] public async Task LastOpenedConfiguration1() { diff --git a/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs index 23df53b1..4a058140 100644 --- a/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs +++ b/tests/RepoM.Plugin.Statistics.Tests/RepositoryStatisticsTest.cs @@ -80,7 +80,7 @@ public void Apply_ShouldThrow_WhenEventTypeIsWrong() Action act = () => sut.Apply(new DummyEvent()); // assert - act.Should().Throw(); + act.Should().Throw(); } [Fact] diff --git a/tests/RepoM.Plugin.Statistics.Tests/StatisticsModuleTest.cs b/tests/RepoM.Plugin.Statistics.Tests/StatisticsModuleTest.cs index 4202e2bd..f0bb5d6b 100644 --- a/tests/RepoM.Plugin.Statistics.Tests/StatisticsModuleTest.cs +++ b/tests/RepoM.Plugin.Statistics.Tests/StatisticsModuleTest.cs @@ -1,5 +1,6 @@ namespace RepoM.Plugin.Statistics.Tests; +using System; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Threading.Tasks; @@ -7,14 +8,16 @@ namespace RepoM.Plugin.Statistics.Tests; using FluentAssertions; using Microsoft.Extensions.Logging; using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.Repository; using Xunit; -using IClock = RepoM.Core.Plugin.Common.IClock; +using IClock = Core.Plugin.Common.IClock; public class StatisticsModuleTest { private readonly IClock _clock; private readonly IAppDataPathProvider _pathProvider; private readonly ILogger _logger; + private readonly IStatisticsConfiguration _configuration; public StatisticsModuleTest() { @@ -22,6 +25,35 @@ public StatisticsModuleTest() _pathProvider = A.Fake(); A.CallTo(() => _pathProvider.GetAppDataPath()).Returns("C:\\data"); _logger = A.Fake(); + _configuration = A.Fake(); + } + + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + IStatisticsService statisticsService = A.Dummy(); + IStatisticsConfiguration configuration = A.Dummy(); + IClock clock = A.Dummy(); + IAppDataPathProvider pathProvider = A.Dummy(); + IFileSystem fileSystem = A.Dummy(); + ILogger logger = A.Dummy(); + + // act + Func act1 = () => new StatisticsModule(statisticsService, configuration, clock, pathProvider, fileSystem, null!); + Func act2 = () => new StatisticsModule(statisticsService, configuration, clock, pathProvider, null!, logger); + Func act3 = () => new StatisticsModule(statisticsService, configuration, clock, null!, fileSystem, logger); + Func act4 = () => new StatisticsModule(statisticsService, configuration, null!, pathProvider, fileSystem, logger); + Func act5 = () => new StatisticsModule(statisticsService, null!, clock, pathProvider, fileSystem, logger); + Func act6 = () => new StatisticsModule(null!, configuration, clock, pathProvider, fileSystem, logger); + + // assert + act1.Should().Throw(); + act2.Should().Throw(); + act3.Should().Throw(); + act4.Should().Throw(); + act5.Should().Throw(); + act6.Should().Throw(); } [Fact] @@ -30,7 +62,7 @@ public async Task StartAsync_ShouldInitialize() // arrange IFileSystem fileSystem = new MockFileSystem(); var statisticsService = new StatisticsService(_clock); - var sut = new StatisticsModule(statisticsService, _clock, _pathProvider, fileSystem, _logger); + var sut = new StatisticsModule(statisticsService, _configuration, _clock, _pathProvider, fileSystem, _logger); // act await sut.StartAsync(); @@ -38,4 +70,29 @@ public async Task StartAsync_ShouldInitialize() // assert statisticsService.GetRepositories().Should().BeEmpty(); } + + // long running + [Fact] + public async Task StopAsync_ShouldStopWritingEvents() + { + // arrange + IRepository repository = A.Fake(); + A.CallTo(() => repository.SafePath).Returns("C:\\tmp-test"); + A.CallTo(() => _configuration.PersistenceBuffer).Returns(TimeSpan.FromSeconds(10)); // minimal + IFileSystem fileSystem = A.Fake(); + var statisticsService = new StatisticsService(_clock); + var sut = new StatisticsModule(statisticsService, _configuration, _clock, _pathProvider, fileSystem, _logger); + await sut.StartAsync(); + + // act + await sut.StopAsync(); + await Task.Delay(TimeSpan.FromSeconds(2)); // not needed but to be more robus against race condition. + Fake.ClearRecordedCalls(fileSystem); + statisticsService.Record(repository); + await Task.Delay(TimeSpan.FromSeconds(10)); + await Task.Delay(TimeSpan.FromSeconds(5)); // extra + + // assert + A.CallTo(fileSystem).MustNotHaveHappened(); + } } \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/StatisticsPackageTest.cs b/tests/RepoM.Plugin.Statistics.Tests/StatisticsPackageTest.cs index 0e762bf5..bf3849f2 100644 --- a/tests/RepoM.Plugin.Statistics.Tests/StatisticsPackageTest.cs +++ b/tests/RepoM.Plugin.Statistics.Tests/StatisticsPackageTest.cs @@ -2,42 +2,82 @@ namespace RepoM.Plugin.Statistics.Tests; using System; using System.IO.Abstractions; +using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Logging; +using RepoM.Core.Plugin; using RepoM.Core.Plugin.Common; +using RepoM.Plugin.Statistics.PersistentConfiguration; using SimpleInjector; using Xunit; public class StatisticsPackageTest { + private readonly Container _container; + private readonly IPackageConfiguration _packageConfiguration; + + public StatisticsPackageTest() + { + _packageConfiguration = A.Fake(); + _container = new Container(); + + var statisticsConfigV1 = new StatisticsConfigV1 + { + PersistenceBuffer = TimeSpan.FromMinutes(15), + RetentionDays = 50, + }; + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(1 as int?)); + A.CallTo(() => _packageConfiguration.LoadConfigurationAsync()).ReturnsLazily(() => statisticsConfigV1); + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).Returns(Task.CompletedTask); + } + [Fact] - public void RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() + public async Task RegisterServices_ShouldBeSuccessful_WhenExternalDependenciesAreRegistered() + { + // arrange + RegisterExternals(_container); + var sut = new StatisticsPackage(); + + // act + await sut.RegisterServicesAsync(_container, _packageConfiguration); + + // assert + // implicit, Verify throws when container is not valid. + _container.Verify(VerificationOption.VerifyAndDiagnose); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(10)] + public async Task RegisterServices_ShouldPersistNewConfig_WhenVersionIsNotCorrect(int? version) { // arrange - var container = new Container(); - RegisterExternals(container); + A.CallTo(() => _packageConfiguration.GetConfigurationVersionAsync()).Returns(Task.FromResult(version)); + RegisterExternals(_container); var sut = new StatisticsPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert + A.CallTo(() => _packageConfiguration.PersistConfigurationAsync(A._, 1)).MustHaveHappenedOnceExactly(); + // implicit, Verify throws when container is not valid. - container.Verify(VerificationOption.VerifyAndDiagnose); + _container.Verify(VerificationOption.VerifyAndDiagnose); } [Fact] - public void RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() + public async Task RegisterServices_ShouldFail_WhenExternalDependenciesAreNotRegistered() { // arrange - var container = new Container(); var sut = new StatisticsPackage(); // act - sut.RegisterServices(container); + await sut.RegisterServicesAsync(_container, _packageConfiguration); // assert - Assert.Throws(() => container.Verify(VerificationOption.VerifyAndDiagnose)); + Assert.Throws(() => _container.Verify(VerificationOption.VerifyAndDiagnose)); } private static void RegisterExternals(Container container) diff --git a/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.Apply_ShouldAddEventInRepositoryRecording.verified.txt b/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.Apply_ShouldAddEventInRepositoryRecording.verified.txt new file mode 100644 index 00000000..9f296ff5 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.Apply_ShouldAddEventInRepositoryRecording.verified.txt @@ -0,0 +1,5 @@ +{ + Recordings: [ + DateTime_1 + ] +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.Apply_ShouldAddSecondEvent_WhenRepositoryIsSame.verified.txt b/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.Apply_ShouldAddSecondEvent_WhenRepositoryIsSame.verified.txt new file mode 100644 index 00000000..81da2232 --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.Apply_ShouldAddSecondEvent_WhenRepositoryIsSame.verified.txt @@ -0,0 +1,6 @@ +{ + Recordings: [ + DateTime_1, + DateTime_2 + ] +} \ No newline at end of file diff --git a/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.cs b/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.cs new file mode 100644 index 00000000..94e5387b --- /dev/null +++ b/tests/RepoM.Plugin.Statistics.Tests/StatisticsServiceTest.cs @@ -0,0 +1,139 @@ +namespace RepoM.Plugin.Statistics.Tests; + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using RepoM.Core.Plugin.Common; +using RepoM.Core.Plugin.Repository; +using RepoM.Plugin.Statistics.Interface; +using VerifyXunit; +using Xunit; + +[UsesVerify] +public class StatisticsServiceTest +{ + private readonly StatisticsService _sut; + private readonly IRepository _repository1; + private readonly IRepository _repository2; + + public StatisticsServiceTest() + { + IClock clock = A.Fake(); + _sut = new StatisticsService(clock); + + _repository1 = A.Fake(); + A.CallTo(() => _repository1.SafePath).Returns("C:\\repo1"); + + _repository2 = A.Fake(); + A.CallTo(() => _repository2.SafePath).Returns("C:\\repo2"); + } + + [Fact] + public void Ctor_ShouldThrow_WhenArgumentNull() + { + // arrange + + // act + Func act1 = () => new StatisticsService(null!); + + // assert + act1.Should().Throw(); + } + + [Fact] + public void GetTotalRecordingCount_ShouldReturnZero_WhenJustInitialized() + { + // arrange + + // act + var result = _sut.GetTotalRecordingCount(); + + // assert + result.Should().Be(0); + } + + [Fact] + public void GetTotalRecordingCount_ShouldReturnOne_WhenSingleRepoIsRecorded() + { + // arrange + _sut.Record(_repository1); + + // act + var result = _sut.GetTotalRecordingCount(); + + // assert + result.Should().Be(1); + } + + [Fact] + public void GetTotalRecordingCount_ShouldReturnTwo_WhenTwoRepoIsRecorded() + { + // arrange + _sut.Record(_repository1); + _sut.Record(_repository2); + + // act + var result = _sut.GetTotalRecordingCount(); + + // assert + result.Should().Be(2); + } + + [Fact] + public void Apply_ShouldThrow_WhenEventTypeIsUnknown() + { + // arrange + IEvent evt = A.Fake(); + A.CallTo(() => evt.Repository).Returns("C:\\repo1"); + A.CallTo(() => evt.Timestamp).Returns(new DateTime(2020, 6, 14, 20, 30, 40)); + + // act + Action act = () => _sut.Apply(evt); + + // assert + act.Should().Throw().WithMessage("Type 'ObjectProxy_*' is unknown"); + } + + [Fact] + public async Task Apply_ShouldAddEventInRepositoryRecording() + { + // arrange + IEvent evt = new RepositoryActionRecordedEvent + { + Repository = "C:\\repo1", + Timestamp = new DateTime(2020, 6, 14, 20, 30, 40), + }; + + // act + _sut.Apply(evt); + + // assert + IReadOnlyRepositoryStatistics? recordings = _sut.GetRepositoryRecording(_repository1); + await Verifier.Verify(recordings); + } + + [Fact] + public async Task Apply_ShouldAddSecondEvent_WhenRepositoryIsSame() + { + // arrange + IEvent evt1 = new RepositoryActionRecordedEvent + { + Repository = "C:\\repo1", + Timestamp = new DateTime(2020, 6, 14, 20, 30, 40), + }; + IEvent evt2 = new RepositoryActionRecordedEvent + { + Repository = "C:\\repo1", + Timestamp = new DateTime(2020, 7, 7, 7, 7, 7), + }; + + // act + _sut.Apply(evt1); + _sut.Apply(evt2); + + // assert + IReadOnlyRepositoryStatistics? recordings = _sut.GetRepositoryRecording(_repository1); + await Verifier.Verify(recordings); + } +} \ No newline at end of file