From 384cc3c6fdd8c39ba326c634a351574a84291490 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sun, 26 Jan 2025 22:57:27 +0100 Subject: [PATCH] Adds Configuration Manager + tests --- .kdtooling/crm_connections.kdco | 2 + ...anager.cs => AzureConfigurationManager.cs} | 6 +- Core/Configurations/Class1.cs | 57 ------ .../ConfigurationManager/AppConfiguration.cs | 14 ++ .../ConfigurationManager/Class1.cs | 28 +++ .../ConfigurationRepository.cs | 24 +++ .../IConfigurationRepository.cs | 5 + .../ConfigurationManager/JsonConfiguration.cs | 31 ++++ .../JsonConfigurationSection.cs | 56 ++++++ .../KeyValueConfigurationBuilder.cs | 56 ++++++ .../ConfigurationDatabaseSetup.cs | 154 ++++++++++++++++ PlanTempus.sln | 16 +- .../CodeSnippets/TestPostgresLISTENNOTIFY.cs | 43 +++++ Tests/TestConfigurationManagement.cs | 171 ++++++++++++++++++ Tests/TestFixture.cs | 130 ++++++------- Tests/Tests.csproj | 1 + 16 files changed, 657 insertions(+), 137 deletions(-) create mode 100644 .kdtooling/crm_connections.kdco rename Core/Configurations/{ConfigurationManager.cs => AzureConfigurationManager.cs} (94%) delete mode 100644 Core/Configurations/Class1.cs create mode 100644 Core/Configurations/ConfigurationManager/AppConfiguration.cs create mode 100644 Core/Configurations/ConfigurationManager/Class1.cs create mode 100644 Core/Configurations/ConfigurationManager/ConfigurationRepository.cs create mode 100644 Core/Configurations/ConfigurationManager/IConfigurationRepository.cs create mode 100644 Core/Configurations/ConfigurationManager/JsonConfiguration.cs create mode 100644 Core/Configurations/ConfigurationManager/JsonConfigurationSection.cs create mode 100644 Core/Configurations/ConfigurationManager/KeyValueConfigurationBuilder.cs create mode 100644 Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs create mode 100644 Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs create mode 100644 Tests/TestConfigurationManagement.cs diff --git a/.kdtooling/crm_connections.kdco b/.kdtooling/crm_connections.kdco new file mode 100644 index 0000000..bebc5df --- /dev/null +++ b/.kdtooling/crm_connections.kdco @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager.cs b/Core/Configurations/AzureConfigurationManager.cs similarity index 94% rename from Core/Configurations/ConfigurationManager.cs rename to Core/Configurations/AzureConfigurationManager.cs index 2931025..bf6c712 100644 --- a/Core/Configurations/ConfigurationManager.cs +++ b/Core/Configurations/AzureConfigurationManager.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; + using Microsoft.Extensions.Configuration; namespace Core.Configurations { - public class ConfigurationManager + public class AzureConfigurationManager { private static IConfigurationBuilder _configurationBuilder; diff --git a/Core/Configurations/Class1.cs b/Core/Configurations/Class1.cs deleted file mode 100644 index 4094f9d..0000000 --- a/Core/Configurations/Class1.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Core.Configurations -{ - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - - public class KeyValueNester - { - private JObject _rootObject = new JObject(); - - public void AddKeyValue(string key, string jsonValue) - { - try - { - // Parse input JSON value - var valueObject = JsonConvert.DeserializeObject(jsonValue); - var parts = key.Split(':'); - - // Start med root object eller nuværende struktur - JObject current = _rootObject; - - // Gennemgå hver del af key'en - for (int i = 0; i < parts.Length - 1; i++) - { - string part = parts[i]; - if (!current.ContainsKey(part)) - { - current[part] = new JObject(); - } - current = (JObject)current[part]; - } - - // Tilføj den sidste værdi - current[parts[^1]] = valueObject; - } - catch (JsonReaderException ex) - { - throw new ArgumentException("Invalid JSON value", nameof(jsonValue), ex); - } - } - - public JObject GetResult() - { - return _rootObject; - } - - public void Clear() - { - _rootObject = new JObject(); - } - } -} diff --git a/Core/Configurations/ConfigurationManager/AppConfiguration.cs b/Core/Configurations/ConfigurationManager/AppConfiguration.cs new file mode 100644 index 0000000..25adc81 --- /dev/null +++ b/Core/Configurations/ConfigurationManager/AppConfiguration.cs @@ -0,0 +1,14 @@ +namespace Configuration.Core; +public class AppConfiguration +{ + public long Id { get; set; } + public string Key { get; set; } + public string Value { get; set; } + public string Label { get; set; } + public string ContentType { get; set; } + public DateTime? ValidFrom { get; set; } + public DateTime? ExpiresAt { get; set; } + public DateTime? CreatedAt { get; set; } + public DateTime? ModifiedAt { get; set; } + public Guid? Etag { get; set; } +} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/Class1.cs b/Core/Configurations/ConfigurationManager/Class1.cs new file mode 100644 index 0000000..62ad83b --- /dev/null +++ b/Core/Configurations/ConfigurationManager/Class1.cs @@ -0,0 +1,28 @@ +using Configuration.Core; +using Microsoft.Extensions.Configuration; + +namespace Core.Configurations.ConfigurationManager +{ + public static class ConfigurationExtensions + { + public static T Get(this IConfigurationSection section) where T : class + { + if (section is JsonConfigurationSection jsonSection) + { + var token = jsonSection.GetToken(); + return token?.ToObject(); + } + throw new InvalidOperationException("Section is not a JsonConfigurationSection"); + } + + public static T GetValue(this IConfigurationSection section, string key) + { + if (section is JsonConfigurationSection jsonSection) + { + var token = jsonSection.GetToken().SelectToken(key.Replace(":", ".")); + return token.ToObject(); + } + throw new InvalidOperationException("Section is not a JsonConfigurationSection"); + } + } +} diff --git a/Core/Configurations/ConfigurationManager/ConfigurationRepository.cs b/Core/Configurations/ConfigurationManager/ConfigurationRepository.cs new file mode 100644 index 0000000..8d4286f --- /dev/null +++ b/Core/Configurations/ConfigurationManager/ConfigurationRepository.cs @@ -0,0 +1,24 @@ +using System.Data; +using Insight.Database; + +namespace Configuration.Core; + public class ConfigurationRepository : IConfigurationRepository +{ + private readonly IDbConnection _connection; + + public ConfigurationRepository(IDbConnection connection) + { + _connection = connection; + } + + public async Task> GetActiveConfigurations() + { + const string sql = @" + SELECT id, key, value, label, content_type, valid_from, expires_at, created_at, modified_at, etag + FROM prod.app_configuration + WHERE (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP) + AND (valid_from IS NULL OR valid_from <= CURRENT_TIMESTAMP)"; + + return await _connection.QueryAsync(sql); + } +} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/IConfigurationRepository.cs b/Core/Configurations/ConfigurationManager/IConfigurationRepository.cs new file mode 100644 index 0000000..5e1fc74 --- /dev/null +++ b/Core/Configurations/ConfigurationManager/IConfigurationRepository.cs @@ -0,0 +1,5 @@ +namespace Configuration.Core; + public interface IConfigurationRepository +{ + Task> GetActiveConfigurations(); +} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/JsonConfiguration.cs b/Core/Configurations/ConfigurationManager/JsonConfiguration.cs new file mode 100644 index 0000000..ce0122f --- /dev/null +++ b/Core/Configurations/ConfigurationManager/JsonConfiguration.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using System.Data; + +namespace Configuration.Core; + public class JsonConfiguration : IConfiguration +{ + private readonly JObject _data; + private readonly IChangeToken _changeToken; + + public JsonConfiguration(JObject data, IChangeToken changeToken) + { + _data = data; + _changeToken = changeToken; + } + + public string this[string key] + { + get => _data.SelectToken(key.Replace(":", "."))?.ToString(); + set => throw new NotImplementedException(); + } + + public IConfigurationSection GetSection(string key) => + new JsonConfigurationSection(_data, key); + + public IEnumerable GetChildren() => + _data.Properties().Select(p => new JsonConfigurationSection(_data, p.Name)); + + public IChangeToken GetReloadToken() => _changeToken; +} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/JsonConfigurationSection.cs b/Core/Configurations/ConfigurationManager/JsonConfigurationSection.cs new file mode 100644 index 0000000..6abaf42 --- /dev/null +++ b/Core/Configurations/ConfigurationManager/JsonConfigurationSection.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using System.Data; + +namespace Configuration.Core; +public class JsonConfigurationSection : IConfigurationSection +{ + private readonly JObject _data; + private readonly string _path; + + public JsonConfigurationSection(JObject data, string path) + { + _data = data; + _path = path; + } + + public string this[string key] + { + get => _data.SelectToken($"{_path.Replace(":", ".")}.{key.Replace(":", ".")}")?.ToString(); + set => throw new NotImplementedException(); + } + + public string Key => _path.Split(':').Last(); + public string Path => _path; + public string Value + { + get => _data.SelectToken(_path.Replace(":", "."))?.ToString(); + set => throw new NotImplementedException(); + } + + + public IConfigurationSection GetSection(string key) => + new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? key : $"{_path}:{key}"); + + public JToken GetToken() => _data.SelectToken(_path.Replace(":", ".")); + + public IEnumerable GetChildren() + { + var token = _data.SelectToken(_path.Replace(":", ".")); + if (token is JObject obj) + { + return obj.Properties() + .Select(p => new JsonConfigurationSection(_data, + string.IsNullOrEmpty(_path) ? p.Name : $"{_path}:{p.Name}")); + } + return Enumerable.Empty(); + } + public T Get() where T : class + { + var token = _data.SelectToken(_path.Replace(":", ".")); + return token?.ToObject(); + } + + public IChangeToken GetReloadToken() => new ConfigurationReloadToken(); +} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/KeyValueConfigurationBuilder.cs b/Core/Configurations/ConfigurationManager/KeyValueConfigurationBuilder.cs new file mode 100644 index 0000000..77e8214 --- /dev/null +++ b/Core/Configurations/ConfigurationManager/KeyValueConfigurationBuilder.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Configuration; + +namespace Configuration.Core; + public class KeyValueConfigurationBuilder +{ + private readonly IConfigurationRepository _repository; + private readonly JObject _rootObject = new(); + private ConfigurationReloadToken _reloadToken = new(); + private IConfiguration _configuration; + + public KeyValueConfigurationBuilder(IConfigurationRepository repository) + { + _repository = repository; + } + + public async Task LoadConfiguration() + { + var configurations = await _repository.GetActiveConfigurations(); + foreach (var config in configurations) + { + AddKeyValue(config.Key, config.Value); + } + OnReload(); + } + + public void AddKeyValue(string key, string jsonValue) + { + var valueObject = JsonConvert.DeserializeObject(jsonValue); + var parts = key.Split(':'); + + JObject current = _rootObject; + for (int i = 0; i < parts.Length - 1; i++) + { + var part = parts[i]; + if (!current.ContainsKey(part)) + { + current[part] = new JObject(); + } + current = (JObject)current[part]; + } + + current[parts[^1]] = valueObject; + } + + private void OnReload() + { + var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); + previousToken.OnReload(); + _configuration = null; + } + + public IConfiguration Build() => + _configuration ??= new JsonConfiguration(_rootObject, _reloadToken); +} \ No newline at end of file diff --git a/Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs b/Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs new file mode 100644 index 0000000..5cb31bb --- /dev/null +++ b/Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs @@ -0,0 +1,154 @@ +using Insight.Database; +using System.Data; + +namespace Database.AppConfigurationSystem; + +public class ConfigurationDatabaseSetup +{ + private readonly IDbConnection _connection; + + public ConfigurationDatabaseSetup(IDbConnection connection) + { + _connection = connection; + } + + public async Task CreateConfigurationTable() + { + const string sql = @" + CREATE TABLE prod.app_configuration ( + id bigserial NOT NULL, + ""key"" varchar(255) NOT NULL, + value text NULL, + ""label"" varchar(255) NULL, + content_type varchar(255) DEFAULT 'text/plain'::character varying NULL, + valid_from timestamptz NULL, + expires_at timestamptz NULL, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL, + modified_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL, + etag uuid DEFAULT gen_random_uuid() NULL, + CONSTRAINT app_configuration_pkey PRIMARY KEY (id) + );"; + await _connection.ExecuteAsync(sql); + } + + public async Task CreateHistoryTable() + { + const string sql = @" + CREATE TABLE prod.app_configuration_history ( + history_id bigserial NOT NULL, + action_type char(1) NOT NULL, + action_timestamp timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + action_by text NOT NULL DEFAULT CURRENT_USER, + id bigint NOT NULL, + ""key"" varchar(255) NOT NULL, + value text NULL, + ""label"" varchar(255) NULL, + content_type varchar(255) NULL, + valid_from timestamptz NULL, + expires_at timestamptz NULL, + created_at timestamptz NULL, + modified_at timestamptz NULL, + etag uuid NULL, + CONSTRAINT app_configuration_history_pkey PRIMARY KEY (history_id) + );"; + await _connection.ExecuteAsync(sql); + } + + public async Task CreateConfigurationIndexes() + { + const string sql = @" + CREATE INDEX idx_app_configuration_key ON prod.app_configuration(""key""); + CREATE INDEX idx_app_configuration_validity ON prod.app_configuration(valid_from, expires_at);"; + await _connection.ExecuteAsync(sql); + } + + public async Task CreateModifiedAtTrigger() + { + const string sql = @" + CREATE OR REPLACE FUNCTION prod.update_app_configuration_modified_at() + RETURNS TRIGGER AS $$ + BEGIN + NEW.modified_at = CURRENT_TIMESTAMP; + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER trg_app_configuration_modified_at + BEFORE UPDATE ON prod.app_configuration + FOR EACH ROW + EXECUTE FUNCTION prod.update_app_configuration_modified_at();"; + await _connection.ExecuteAsync(sql); + } + + public async Task CreateNotifyTrigger() + { + const string sql = @" + CREATE OR REPLACE FUNCTION prod.notify_app_configuration_change() + RETURNS TRIGGER AS $$ + BEGIN + PERFORM pg_notify('config_changes', NEW.key); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER trg_app_configuration_notify + AFTER INSERT OR UPDATE ON prod.app_configuration + FOR EACH ROW + EXECUTE FUNCTION prod.notify_app_configuration_change();"; + await _connection.ExecuteAsync(sql); + } + + public async Task CreateHistoryTrigger() + { + const string sql = @" + CREATE OR REPLACE FUNCTION prod.log_app_configuration_changes() + RETURNS TRIGGER AS $$ + BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO prod.app_configuration_history ( + action_type, id, ""key"", value, label, content_type, + valid_from, expires_at, created_at, modified_at, etag + ) + VALUES ( + 'I', NEW.id, NEW.key, NEW.value, NEW.label, NEW.content_type, + NEW.valid_from, NEW.expires_at, NEW.created_at, NEW.modified_at, NEW.etag + ); + ELSIF (TG_OP = 'UPDATE') THEN + INSERT INTO prod.app_configuration_history ( + action_type, id, ""key"", value, label, content_type, + valid_from, expires_at, created_at, modified_at, etag + ) + VALUES ( + 'U', OLD.id, OLD.key, OLD.value, OLD.label, OLD.content_type, + OLD.valid_from, OLD.expires_at, OLD.created_at, OLD.modified_at, OLD.etag + ); + ELSIF (TG_OP = 'DELETE') THEN + INSERT INTO prod.app_configuration_history ( + action_type, id, ""key"", value, label, content_type, + valid_from, expires_at, created_at, modified_at, etag + ) + VALUES ( + 'D', OLD.id, OLD.key, OLD.value, OLD.label, OLD.content_type, + OLD.valid_from, OLD.expires_at, OLD.created_at, OLD.modified_at, OLD.etag + ); + END IF; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER trg_app_configuration_history + AFTER INSERT OR UPDATE OR DELETE ON prod.app_configuration + FOR EACH ROW EXECUTE FUNCTION prod.log_app_configuration_changes();"; + await _connection.ExecuteAsync(sql); + } + + public async Task CreateDatabaseStructure(IDbConnection connection) + { + await CreateConfigurationTable(); + await CreateHistoryTable(); + await CreateConfigurationIndexes(); + await CreateModifiedAtTrigger(); + await CreateNotifyTrigger(); + await CreateHistoryTrigger(); + } +} diff --git a/PlanTempus.sln b/PlanTempus.sln index d2e53b1..6d9ec53 100644 --- a/PlanTempus.sln +++ b/PlanTempus.sln @@ -10,9 +10,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Database", "Database\Database.csproj", "{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPostgresql", "TestPostgresLISTEN\TestPostgresql.csproj", "{743EF625-6C74-419C-A492-AA069956F471}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetupInfrastructure", "SetupInfrastructure\SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SetupInfrastructure", "SetupInfrastructure\SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,14 +18,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.Build.0 = Release|Any CPU - {743EF625-6C74-419C-A492-AA069956F471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {743EF625-6C74-419C-A492-AA069956F471}.Debug|Any CPU.Build.0 = Debug|Any CPU - {743EF625-6C74-419C-A492-AA069956F471}.Release|Any CPU.ActiveCfg = Release|Any CPU - {743EF625-6C74-419C-A492-AA069956F471}.Release|Any CPU.Build.0 = Release|Any CPU {7B554252-1CE4-44BD-B108-B0BDCCB24742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7B554252-1CE4-44BD-B108-B0BDCCB24742}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B554252-1CE4-44BD-B108-B0BDCCB24742}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -44,6 +34,10 @@ Global {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.Build.0 = Release|Any CPU + {48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs b/Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs new file mode 100644 index 0000000..ed52167 --- /dev/null +++ b/Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs @@ -0,0 +1,43 @@ +using Npgsql; + +class TestPostgresLISTENNOTIFY +{ + static async Task Main(string[] args) + { + var connectionString = "Host=192.168.1.57;Database=ptdb01;Username=postgres;Password=3911"; + + try + { + await using NpgsqlConnection conn = new NpgsqlConnection(connectionString); + await conn.OpenAsync(); + + Console.WriteLine("Forbundet til databasen. Lytter efter notifikationer..."); + + conn.Notification += (o, e) => + { + Console.WriteLine($"Notifikation modtaget:"); + Console.WriteLine($" PID: {e.PID}"); + Console.WriteLine($" Kanal: {e.Channel}"); + Console.WriteLine($" Payload: {e.Payload}"); + Console.WriteLine("------------------------"); + }; + + await using (var cmd = new NpgsqlCommand("LISTEN config_changes;", conn)) + { + await cmd.ExecuteNonQueryAsync(); + } + + Console.WriteLine("Tryk på en tast for at stoppe..."); + + while (!Console.KeyAvailable) + { + await conn.WaitAsync(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Der opstod en fejl: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + } +} \ No newline at end of file diff --git a/Tests/TestConfigurationManagement.cs b/Tests/TestConfigurationManagement.cs new file mode 100644 index 0000000..0d482a9 --- /dev/null +++ b/Tests/TestConfigurationManagement.cs @@ -0,0 +1,171 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tests; +using Core.Configurations.ConfigurationManager; + +namespace Configuration.Core.Tests; + +[TestClass] +public class ConfigurationTests : TestFixture +{ + private Mock _mockRepo; + private KeyValueConfigurationBuilder _builder; + + [TestInitialize] + public void Init() + { + _mockRepo = new Mock(); + _builder = new KeyValueConfigurationBuilder(_mockRepo.Object); + } + + [TestMethod] + public async Task LoadConfiguration_WithValidData_BuildsCorrectly() + { + // Arrange + var configurations = new List + { + new AppConfiguration + { + Key = "Email:Templates:Welcome", + Value = @"{""subject"":""Welcome"",""sender"":""test@company.com""}", + ValidFrom = DateTime.UtcNow.AddDays(-1), + ExpiresAt = DateTime.UtcNow.AddDays(1) + } + }; + + _mockRepo.Setup(r => r.GetActiveConfigurations()) + .ReturnsAsync(configurations); + + // Act + await _builder.LoadConfiguration(); + var config = _builder.Build(); + + // Assert + Assert.IsNotNull(config); + Assert.AreEqual("Welcome", config["Email:Templates:Welcome:subject"]); + } + + [TestMethod] + public async Task LoadConfiguration_WithExpiredData_NotIncluded() + { + // Arrange + var configurations = new List + { + new AppConfiguration + { + Key = "Test:Key", + Value = @"{""value"":""test""}", + ValidFrom = DateTime.UtcNow.AddDays(1), + ExpiresAt = DateTime.UtcNow.AddDays(2) + } + }; + + _mockRepo.Setup(r => r.GetActiveConfigurations()) + .ReturnsAsync(new List()); + + // Act + await _builder.LoadConfiguration(); + var config = _builder.Build(); + + // Assert + Assert.IsNull(config["Test:Key:value"]); + } + + [TestMethod] + public void AddKeyValue_WithNestedKeys_BuildsCorrectHierarchy() + { + // Arrange + var key = "Level1:Level2:Level3"; + var value = @"{""setting"":""value""}"; + + // Act + _builder.AddKeyValue(key, value); + var config = _builder.Build(); + + // Assert + Assert.AreEqual("value", config["Level1:Level2:Level3:setting"]); + } + + [TestMethod] + public async Task GetSection_ReturnsCorrectSubSection() + { + // Arrange + var configurations = new List + { + new AppConfiguration + { + Key = "Parent:Child", + Value = @"{""setting"":""value""}", + ValidFrom = DateTime.UtcNow.AddDays(-1) + } + }; + + _mockRepo.Setup(r => r.GetActiveConfigurations()) + .ReturnsAsync(configurations); + + // Act + await _builder.LoadConfiguration(); + var config = _builder.Build(); + var section = config.GetSection("Parent"); + + // Assert + Assert.AreEqual("value", section["Child:setting"]); + } + + [TestMethod] + public void Configuration_ShouldMapToTypedClass() + { + // Arrange + var configData = new JObject + { + ["Email"] = new JObject + { + ["Templates"] = new JObject + { + ["Welcome"] = new JObject + { + ["subject"] = "Welcome to our service", + ["template"] = "welcome-template.html", + ["sender"] = "noreply@test.com", + ["settings"] = new JObject + { + ["isEnabled"] = true, + ["retryCount"] = 3 + } + } + } + } + }; + + var configuration = new JsonConfiguration(configData, new Microsoft.Extensions.Configuration.ConfigurationReloadToken()); + + // Act + var welcomeConfig = configuration.GetSection("Email:Templates:Welcome").Get(); + + // Assert + Assert.IsNotNull(welcomeConfig); + Assert.AreEqual("Welcome to our service", welcomeConfig.Subject); + Assert.AreEqual("welcome-template.html", welcomeConfig.Template); + Assert.AreEqual("noreply@test.com", welcomeConfig.Sender); + Assert.IsTrue(welcomeConfig.Settings.IsEnabled); + Assert.AreEqual(3, welcomeConfig.Settings.RetryCount); + } + + public class WelcomeEmailConfig + { + public string Subject { get; set; } + public string Template { get; set; } + public string Sender { get; set; } + public EmailSettings Settings { get; set; } + } + + public class EmailSettings + { + public bool IsEnabled { get; set; } + public int RetryCount { get; set; } + } +} \ No newline at end of file diff --git a/Tests/TestFixture.cs b/Tests/TestFixture.cs index 368faff..c1d48a5 100644 --- a/Tests/TestFixture.cs +++ b/Tests/TestFixture.cs @@ -9,89 +9,89 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Tests { - /// - /// Act as base class for tests. Avoids duplication of test setup code - /// - [TestClass] - public abstract partial class TestFixture - { - protected IContainer Container { get; private set; } - protected ContainerBuilder ContainerBuilder { get; private set; } + /// + /// Act as base class for tests. Avoids duplication of test setup code + /// + [TestClass] + public abstract partial class TestFixture + { + protected IContainer Container { get; private set; } + protected ContainerBuilder ContainerBuilder { get; private set; } - [AssemblyInitialize] - public static void AssemblySetup(TestContext tc) - { - Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + [AssemblyInitialize] + public static void AssemblySetup(TestContext tc) + { + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); - var envConfiguration = new ConfigurationBuilder() - .AddEnvironmentVariables() - .Build(); + var envConfiguration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); - } + } - public virtual IConfigurationRoot Configuration() - { + public virtual IConfigurationRoot Configuration() + { - IConfigurationBuilder configBuilder = Core.Configurations.ConfigurationManager.AppConfigBuilder("appsettings.dev.json"); - IConfigurationRoot configuration = configBuilder.Build(); + IConfigurationBuilder configBuilder = Core.Configurations.AzureConfigurationManager.AppConfigBuilder("appsettings.dev.json"); + IConfigurationRoot configuration = configBuilder.Build(); - return configuration; - } + return configuration; + } - /// - /// Should not be overriden. Rather override PreArrangeAll to setup data needed for a test class. - /// Override PrebuildContainer with a method that does nothing to prevent early build of IOC container - /// - [TestInitialize] - public void Setup() - { - CreateContainerBuilder(); - Container = ContainerBuilder.Build(); - Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider(); - } + /// + /// Should not be overriden. Rather override PreArrangeAll to setup data needed for a test class. + /// Override PrebuildContainer with a method that does nothing to prevent early build of IOC container + /// + [TestInitialize] + public void Setup() + { + CreateContainerBuilder(); + Container = ContainerBuilder.Build(); + Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider(); + } - protected virtual void CreateContainerBuilder() - { - IConfigurationRoot configuration = Configuration(); + protected virtual void CreateContainerBuilder() + { + IConfigurationRoot configuration = Configuration(); - var builder = new ContainerBuilder(); - builder.RegisterInstance(new LoggerFactory()) - .As(); + var builder = new ContainerBuilder(); + builder.RegisterInstance(new LoggerFactory()) + .As(); - builder.RegisterGeneric(typeof(Logger<>)) - .As(typeof(ILogger<>)) - .SingleInstance(); + builder.RegisterGeneric(typeof(Logger<>)) + .As(typeof(ILogger<>)) + .SingleInstance(); - builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule - { - ConnectionString = configuration.GetConnectionString("ptdb") - }); + builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule + { + ConnectionString = configuration.GetConnectionString("ptdb") + }); - builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule - { - TelemetryConfig = configuration.GetSection("ApplicationInsights").Get() - }); + builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule + { + TelemetryConfig = configuration.GetSection("ApplicationInsights").Get() + }); - ContainerBuilder = builder; - } + ContainerBuilder = builder; + } - [TestCleanup] - public void CleanUp() - { - Trace.Flush(); - var telemetryClient = Container.Resolve(); - telemetryClient.Flush(); + [TestCleanup] + public void CleanUp() + { + Trace.Flush(); + var telemetryClient = Container.Resolve(); + telemetryClient.Flush(); - if (Container != null) - { - Container.Dispose(); - Container = null; - } - } + if (Container != null) + { + Container.Dispose(); + Container = null; + } + } - } + } } \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 6e294c1..d9a077c 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -11,6 +11,7 @@ +