Adds Configuration Manager + tests
This commit is contained in:
parent
55e65a1b21
commit
384cc3c6fd
16 changed files with 657 additions and 137 deletions
2
.kdtooling/crm_connections.kdco
Normal file
2
.kdtooling/crm_connections.kdco
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ArrayOfCrmConnectionInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://kdtooling.kupp.at/kdtooling/schema_1.0" />
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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<JObject>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Core/Configurations/ConfigurationManager/AppConfiguration.cs
Normal file
14
Core/Configurations/ConfigurationManager/AppConfiguration.cs
Normal file
|
|
@ -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; }
|
||||
}
|
||||
28
Core/Configurations/ConfigurationManager/Class1.cs
Normal file
28
Core/Configurations/ConfigurationManager/Class1.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Configuration.Core;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Core.Configurations.ConfigurationManager
|
||||
{
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
public static T Get<T>(this IConfigurationSection section) where T : class
|
||||
{
|
||||
if (section is JsonConfigurationSection jsonSection)
|
||||
{
|
||||
var token = jsonSection.GetToken();
|
||||
return token?.ToObject<T>();
|
||||
}
|
||||
throw new InvalidOperationException("Section is not a JsonConfigurationSection");
|
||||
}
|
||||
|
||||
public static T GetValue<T>(this IConfigurationSection section, string key)
|
||||
{
|
||||
if (section is JsonConfigurationSection jsonSection)
|
||||
{
|
||||
var token = jsonSection.GetToken().SelectToken(key.Replace(":", "."));
|
||||
return token.ToObject<T>();
|
||||
}
|
||||
throw new InvalidOperationException("Section is not a JsonConfigurationSection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IEnumerable<AppConfiguration>> 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<AppConfiguration>(sql);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
namespace Configuration.Core;
|
||||
public interface IConfigurationRepository
|
||||
{
|
||||
Task<IEnumerable<AppConfiguration>> GetActiveConfigurations();
|
||||
}
|
||||
|
|
@ -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<IConfigurationSection> GetChildren() =>
|
||||
_data.Properties().Select(p => new JsonConfigurationSection(_data, p.Name));
|
||||
|
||||
public IChangeToken GetReloadToken() => _changeToken;
|
||||
}
|
||||
|
|
@ -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<IConfigurationSection> 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<IConfigurationSection>();
|
||||
}
|
||||
public T Get<T>() where T : class
|
||||
{
|
||||
var token = _data.SelectToken(_path.Replace(":", "."));
|
||||
return token?.ToObject<T>();
|
||||
}
|
||||
|
||||
public IChangeToken GetReloadToken() => new ConfigurationReloadToken();
|
||||
}
|
||||
|
|
@ -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<JObject>(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);
|
||||
}
|
||||
154
Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs
Normal file
154
Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
43
Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs
Normal file
43
Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs
Normal file
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
171
Tests/TestConfigurationManagement.cs
Normal file
171
Tests/TestConfigurationManagement.cs
Normal file
|
|
@ -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<IConfigurationRepository> _mockRepo;
|
||||
private KeyValueConfigurationBuilder _builder;
|
||||
|
||||
[TestInitialize]
|
||||
public void Init()
|
||||
{
|
||||
_mockRepo = new Mock<IConfigurationRepository>();
|
||||
_builder = new KeyValueConfigurationBuilder(_mockRepo.Object);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadConfiguration_WithValidData_BuildsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var configurations = new List<AppConfiguration>
|
||||
{
|
||||
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<AppConfiguration>
|
||||
{
|
||||
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<AppConfiguration>());
|
||||
|
||||
// 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<AppConfiguration>
|
||||
{
|
||||
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<WelcomeEmailConfig>();
|
||||
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ namespace Tests
|
|||
public virtual IConfigurationRoot Configuration()
|
||||
{
|
||||
|
||||
IConfigurationBuilder configBuilder = Core.Configurations.ConfigurationManager.AppConfigBuilder("appsettings.dev.json");
|
||||
IConfigurationBuilder configBuilder = Core.Configurations.AzureConfigurationManager.AppConfigBuilder("appsettings.dev.json");
|
||||
IConfigurationRoot configuration = configBuilder.Build();
|
||||
|
||||
return configuration;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="moq" Version="4.20.72" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue