Adds Configuration Manager + tests

This commit is contained in:
Janus C. H. Knudsen 2025-01-26 22:57:27 +01:00
parent 55e65a1b21
commit 384cc3c6fd
16 changed files with 657 additions and 137 deletions

View 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" />

View file

@ -1,11 +1,9 @@
using System; 
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Core.Configurations namespace Core.Configurations
{ {
public class ConfigurationManager public class AzureConfigurationManager
{ {
private static IConfigurationBuilder _configurationBuilder; private static IConfigurationBuilder _configurationBuilder;

View file

@ -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();
}
}
}

View 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; }
}

View 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");
}
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,5 @@
namespace Configuration.Core;
public interface IConfigurationRepository
{
Task<IEnumerable<AppConfiguration>> GetActiveConfigurations();
}

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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);
}

View 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();
}
}

View file

@ -10,9 +10,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Database", "Database\Database.csproj", "{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Database", "Database\Database.csproj", "{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPostgresql", "TestPostgresLISTEN\TestPostgresql.csproj", "{743EF625-6C74-419C-A492-AA069956F471}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SetupInfrastructure", "SetupInfrastructure\SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetupInfrastructure", "SetupInfrastructure\SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -20,14 +18,6 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution 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.ActiveCfg = Debug|Any CPU
{7B554252-1CE4-44BD-B108-B0BDCCB24742}.Debug|Any CPU.Build.0 = 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 {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}.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.ActiveCfg = Release|Any CPU
{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View 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}");
}
}
}

View 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; }
}
}

View file

@ -9,89 +9,89 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Tests namespace Tests
{ {
/// <summary> /// <summary>
/// Act as base class for tests. Avoids duplication of test setup code /// Act as base class for tests. Avoids duplication of test setup code
/// </summary> /// </summary>
[TestClass] [TestClass]
public abstract partial class TestFixture public abstract partial class TestFixture
{ {
protected IContainer Container { get; private set; } protected IContainer Container { get; private set; }
protected ContainerBuilder ContainerBuilder { get; private set; } protected ContainerBuilder ContainerBuilder { get; private set; }
[AssemblyInitialize] [AssemblyInitialize]
public static void AssemblySetup(TestContext tc) public static void AssemblySetup(TestContext tc)
{ {
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
var envConfiguration = new ConfigurationBuilder() var envConfiguration = new ConfigurationBuilder()
.AddEnvironmentVariables() .AddEnvironmentVariables()
.Build(); .Build();
} }
public virtual IConfigurationRoot Configuration() 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(); IConfigurationRoot configuration = configBuilder.Build();
return configuration; return configuration;
} }
/// <summary> /// <summary>
/// Should not be overriden. Rather override PreArrangeAll to setup data needed for a test class. /// 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 /// Override PrebuildContainer with a method that does nothing to prevent early build of IOC container
/// </summary> /// </summary>
[TestInitialize] [TestInitialize]
public void Setup() public void Setup()
{ {
CreateContainerBuilder(); CreateContainerBuilder();
Container = ContainerBuilder.Build(); Container = ContainerBuilder.Build();
Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider(); Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider();
} }
protected virtual void CreateContainerBuilder() protected virtual void CreateContainerBuilder()
{ {
IConfigurationRoot configuration = Configuration(); IConfigurationRoot configuration = Configuration();
var builder = new ContainerBuilder(); var builder = new ContainerBuilder();
builder.RegisterInstance(new LoggerFactory()) builder.RegisterInstance(new LoggerFactory())
.As<ILoggerFactory>(); .As<ILoggerFactory>();
builder.RegisterGeneric(typeof(Logger<>)) builder.RegisterGeneric(typeof(Logger<>))
.As(typeof(ILogger<>)) .As(typeof(ILogger<>))
.SingleInstance(); .SingleInstance();
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
{ {
ConnectionString = configuration.GetConnectionString("ptdb") ConnectionString = configuration.GetConnectionString("ptdb")
}); });
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
{ {
TelemetryConfig = configuration.GetSection("ApplicationInsights").Get<Core.ModuleRegistry.TelemetryConfig>() TelemetryConfig = configuration.GetSection("ApplicationInsights").Get<Core.ModuleRegistry.TelemetryConfig>()
}); });
ContainerBuilder = builder; ContainerBuilder = builder;
} }
[TestCleanup] [TestCleanup]
public void CleanUp() public void CleanUp()
{ {
Trace.Flush(); Trace.Flush();
var telemetryClient = Container.Resolve<TelemetryClient>(); var telemetryClient = Container.Resolve<TelemetryClient>();
telemetryClient.Flush(); telemetryClient.Flush();
if (Container != null) if (Container != null)
{ {
Container.Dispose(); Container.Dispose();
Container = null; Container = null;
} }
} }
} }
} }

View file

@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.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.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" /> <PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
</ItemGroup> </ItemGroup>