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 @@
+