Initial commit: SWP.Core enterprise framework with multi-tenant architecture, configuration management, security, telemetry and comprehensive test suite

This commit is contained in:
Janus C. H. Knudsen 2025-08-02 22:16:39 +02:00
commit 5275a75502
87 changed files with 6140 additions and 0 deletions

42
Tests/.runsettings Normal file
View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<RunConfiguration>
<MaxCpuCount>0</MaxCpuCount>
<ResultsDirectory>.\TestResults</ResultsDirectory>
<TargetFrameworkVersion>net9.0</TargetFrameworkVersion>
</RunConfiguration>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="Code Coverage" uri="datacollector://Microsoft/CodeCoverage/2.0">
<Configuration>
<CodeCoverage>
<ModulePaths>
<Include>
<ModulePath>.*SWP\.Core\.dll$</ModulePath>
</Include>
<Exclude>
<ModulePath>.*Tests.*</ModulePath>
</Exclude>
</ModulePaths>
<UseVerifiableInstrumentation>True</UseVerifiableInstrumentation>
<AllowLowIntegrityProcesses>True</AllowLowIntegrityProcesses>
<CollectFromChildProcesses>True</CollectFromChildProcesses>
<CollectAspDotNet>False</CollectAspDotNet>
</CodeCoverage>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
<MSTest>
<MapInconclusiveToFailed>false</MapInconclusiveToFailed>
<CaptureTraceOutput>true</CaptureTraceOutput>
<DeleteDeploymentDirectoryAfterTestRunIsComplete>true</DeleteDeploymentDirectoryAfterTestRunIsComplete>
<DeploymentEnabled>true</DeploymentEnabled>
<Parallelize>
<Workers>0</Workers>
<Scope>MethodLevel</Scope>
</Parallelize>
</MSTest>
</RunSettings>

View file

@ -0,0 +1,40 @@
using Npgsql;
namespace SWP.Core.X.TDD.CodeSnippets;
internal class TestPostgresLISTENNOTIFY
{
private static async Task Main(string[] args)
{
var connectionString = "Host=192.168.1.57;Database=ptdb01;Username=postgres;Password=3911";
try
{
await using var 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,11 @@
INSERT INTO "system".app_configuration ("key",value,"label",content_type,valid_from,expires_at,created_at,modified_at,etag) VALUES
('Email:Templates:Welcome','{"subject":"Velkommen til vores platform","template":"welcome-dk.html","sender":"velkommen@firma.dk"}','test','application/json','2024-01-01 01:00:00+01',NULL,'2025-02-03 16:46:36.665888+01','2025-02-03 16:47:30.528326+01','c48949c4-c02f-4c77-b81c-e281a810def1'::uuid),
('Email:Templates:Password','{"subject":"Nulstil dit kodeord","template":"reset-password-dk.html","sender":"support@firma.dk"}','Email Templates','application/json','2024-01-01 01:00:00+01',NULL,'2025-02-03 16:47:56.537775+01','2025-02-03 16:47:56.537775+01','26500738-4f5b-4cc8-a0e4-2a6a5fd57675'::uuid),
('Debug','true',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','f1348731-9396-4f1d-b40a-7fbd23a897d2'::uuid),
('Database:ConnectionString','"Server=db.example.com;Port=5432"',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','2aa0bc3e-fa24-449a-8f25-a76d9b4d535e'::uuid),
('Database:Timeout','30',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','d25ebb14-49f6-4e33-9ac7-a3253705d0fb'::uuid),
('Database:UseSSL','true',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','f4d52ec4-b723-4561-9b18-0e7a68b89a17'::uuid),
('Logging:FileOptions','{"Path": "/var/logs/app.log", "MaxSizeMB": 100, "RetentionDays": 7}',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','06c0891d-a860-4acc-917a-d0877f511c1b'::uuid),
('Features:Experimental','{"Enabled": true, "RolloutPercentage": 25, "AllowedUserGroups": ["beta"]}',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','0136fdef-51d9-4909-82ef-f72053ce6d6d'::uuid),
('API:Endpoints','"/api/users"',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','fe362b69-a486-48ad-9165-2e623e2e6f70'::uuid),
('API:Endpoints','"/api/products"',NULL,'text/plain',NULL,NULL,'2025-02-02 14:25:22.200058+01','2025-02-02 14:25:22.200058+01','c087e2d4-1f38-4814-b4dd-f30c463dc6d1'::uuid);

View file

@ -0,0 +1,63 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SWP.Core.CommandQueries;
namespace SWP.Core.X.TDD.CommandQueries;
[TestClass]
public class CommandTests
{
[TestMethod]
public void Command_ShouldHaveCorrelationId()
{
// Arrange & Act
var correlationId = Guid.NewGuid();
var command = new TestCommand { CorrelationId = correlationId };
// Assert
command.CorrelationId.ShouldBe(correlationId);
}
[TestMethod]
public void Command_ShouldHaveTransactionId()
{
// Arrange & Act
var correlationId = Guid.NewGuid();
var transactionId = Guid.NewGuid();
var command = new TestCommand { CorrelationId = correlationId };
command.TransactionId = transactionId;
// Assert
command.TransactionId.ShouldBe(transactionId);
}
private class TestCommand : Command
{
public string TestProperty { get; set; }
}
}
[TestClass]
public class ProblemDetailsTests
{
[TestMethod]
public void ProblemDetails_ShouldHaveBasicProperties()
{
// Arrange & Act
var problem = new ProblemDetails
{
Type = "ValidationError",
Title = "Validation Failed",
Status = 400,
Detail = "Email is required",
Instance = "/api/users"
};
// Assert
problem.Type.ShouldBe("ValidationError");
problem.Title.ShouldBe("Validation Failed");
problem.Status.ShouldBe(400);
problem.Detail.ShouldBe("Email is required");
problem.Instance.ShouldBe("/api/users");
}
}

View file

@ -0,0 +1,53 @@
using Newtonsoft.Json;
using Shouldly;
using SWP.Core.CommandQueries;
namespace SWP.Core.X.TDD.CommandQueryHandlerTests;
[TestClass]
public class ProblemDetailsTests
{
[TestMethod]
public void TestFormatOfProblemDetails()
{
// Arrange
var problemDetails = new ProblemDetails
{
Type = "https://example.com/errors/invalid-input",
Title = "Invalid Input",
Status = 400,
Detail = "The request body is invalid.",
Instance = "/api/users"
};
problemDetails.AddExtension("invalidFields", new[]
{
new { Field = "name", Message = "The 'name' field is required." },
new { Field = "email", Message = "The 'email' field must be a valid email address." }
});
var json = JsonConvert.SerializeObject(problemDetails, Formatting.Indented);
var expectedJson = """
{
"Type": "https://example.com/errors/invalid-input",
"Title": "Invalid Input",
"Status": 400,
"Detail": "The request body is invalid.",
"Instance": "/api/users",
"invalidFields": [
{
"Field": "name",
"Message": "The 'name' field is required."
},
{
"Field": "email",
"Message": "The 'email' field must be a valid email address."
}
]
}
""";
json.ShouldBe(expectedJson);
}
}

View file

@ -0,0 +1,188 @@
using System.Data;
using Autofac;
using Insight.Database;
using Newtonsoft.Json;
using SWP.Core.Database.ConnectionFactory;
using Shouldly;
namespace SWP.Core.X.TDD.ConfigurationSystem;
[TestClass]
public class SetupConfigurationTests : TestFixture
{
private IDbConnection _connection;
[TestInitialize]
public void Setup()
{
var connectionFactory = Container.Resolve<IDbConnectionFactory>();
_connection = connectionFactory.Create();
}
[TestCleanup]
public void Cleanup()
{
_connection.ExecuteSql(@"
TRUNCATE TABLE app_configuration_history;
TRUNCATE TABLE app_configuration CASCADE;");
_connection.Dispose();
}
[TestMethod]
public void InsertConfiguration_ShouldCreateHistoryRecord()
{
// Arrange
var configData = new
{
key = "test.key",
value = "test value",
label = "Test Label"
};
// Act
var result = _connection.QuerySql<dynamic>(@"
INSERT INTO app_configuration (key, value, label)
VALUES (@key, @value, @label)
RETURNING *", configData).Single();
var history = _connection.QuerySql<dynamic>(@"
SELECT key, value, label, action_type
FROM app_configuration_history
WHERE id = @id AND action_type = 'I'",
new { id = (int)result.id })
.Single();
// Assert
var expected = JsonConvert.SerializeObject(new
{
configData.key,
configData.value,
configData.label,
action_type = "I"
});
var actual = JsonConvert.SerializeObject(history) as string;
actual.ShouldBe(expected);
}
[TestMethod]
public void UpdateConfiguration_ShouldUpdateModifiedAt()
{
// Arrange
var configData = new
{
key = "test.key",
value = "original value"
};
var original = _connection.QuerySql<dynamic>(@"
INSERT INTO app_configuration (key, value)
VALUES (@key, @value)
RETURNING modified_at", configData)
.Single();
Thread.Sleep(1000);
// Act
var updated = _connection.QuerySql<dynamic>(@"
UPDATE app_configuration
SET value = @value
WHERE key = @key
RETURNING modified_at",
new { configData.key, value = "updated value" })
.Single();
// Assert
((DateTime)updated.modified_at).ShouldBeGreaterThan((DateTime)original.modified_at);
}
[TestMethod]
public void DeleteConfiguration_ShouldCreateHistoryRecord()
{
// Arrange
var configData = new
{
key = "test.key",
value = "test value"
};
var original = _connection.QuerySql<dynamic>(@"
INSERT INTO app_configuration (key, value)
VALUES (@key, @value)
RETURNING id", configData)
.Single();
// Act
_connection.ExecuteSql(
"DELETE FROM app_configuration WHERE id = @id",
new { id = (int)original.id });
// Assert
var history = _connection.QuerySql<dynamic>(@"
SELECT key, value, action_type
FROM app_configuration_history
WHERE id = @id AND action_type = 'D'",
new { id = (int)original.id })
.Single();
var expected = JsonConvert.SerializeObject(new
{
configData.key,
configData.value,
action_type = "D"
});
var actual = JsonConvert.SerializeObject(history) as string;
actual.ShouldBe(expected);
}
[TestMethod]
public void InsertConfiguration_ShouldSetAllColumns()
{
// Arrange
var now = DateTime.UtcNow;
now = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0, DateTimeKind.Utc);
var configData = new
{
key = "test.columns",
value = "test value",
label = "Test Label",
content_type = "application/json",
valid_from = now,
expires_at = now.AddDays(30)
};
// Act
var result = _connection.QuerySql<dynamic>(@"
INSERT INTO app_configuration (
key,
value,
label,
content_type,
valid_from,
expires_at)
VALUES (
@key,
@value,
@label,
@content_type,
@valid_from,
@expires_at)
RETURNING key, value, label, content_type,
CAST(EXTRACT(EPOCH FROM date_trunc('minute', valid_from)) AS INTEGER) as valid_from,
CAST(EXTRACT(EPOCH FROM date_trunc('minute', expires_at)) AS INTEGER) as expires_at", configData)
.Single();
// Assert
var expected = JsonConvert.SerializeObject(new
{
configData.key,
configData.value,
configData.label,
configData.content_type,
valid_from = ((DateTimeOffset)configData.valid_from).ToUnixTimeSeconds(),
expires_at = ((DateTimeOffset)configData.expires_at).ToUnixTimeSeconds()
});
Assert.AreEqual(expected, JsonConvert.SerializeObject(result));
}
}

View file

@ -0,0 +1,145 @@
using Newtonsoft.Json.Linq;
using SWP.Core.Configurations;
using SWP.Core.Configurations.JsonConfigProvider;
using SWP.Core.Configurations.SmartConfigProvider;
using Shouldly;
using SWP.Core.X.TDD;
namespace SWP.Core.X.TDD.ConfigurationTests;
[TestClass]
public class JsonConfigurationProviderTests : TestFixture
{
private const string _testFolder = "ConfigurationTests/";
public JsonConfigurationProviderTests() : base(_testFolder)
{
}
[TestMethod]
public void GetSection_ShouldReturnCorrectFeatureSection()
{
// Arrange
var expectedJObject = JObject.Parse(@"{
'Enabled': true,
'RolloutPercentage': 25,
'AllowedUserGroups': ['beta']
}") as JToken;
var builder = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.Build();
// Act
var section = builder.GetSection("Feature");
// Assert
section.ShouldNotBeNull();
section.Value.ShouldBeEquivalentTo(expectedJObject);
}
[TestMethod]
public void Get_ShouldReturnCorrectFeatureObject()
{
// Arrange
var expectedFeature = new Feature
{
Enabled = true,
RolloutPercentage = 25,
AllowedUserGroups = new List<string> { "beta" }
};
var builder = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.Build();
// Act
var actualFeature = builder.GetSection("Feature").ToObject<Feature>();
#pragma warning disable CS0618 // Type or member is obsolete
var actualFeatureObsoleted = builder.GetSection("Feature").Get<Feature>();
#pragma warning restore CS0618 // Type or member is obsolete
// Assert
actualFeature.ShouldBeEquivalentTo(expectedFeature);
actualFeatureObsoleted.ShouldBeEquivalentTo(expectedFeature);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueAsString()
{
// Arrange
var expectedFeature = "123";
var builder = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.Build();
// Act
var actualFeature = builder.GetSection("AnotherSetting").Get<string>("Thresholds:High");
// Assert
actualFeature.ShouldBeEquivalentTo(expectedFeature);
}
/// <summary>
/// Testing a stupid indexer for compability with Microsoft ConfigurationBuilder
/// </summary>
[TestMethod]
public void Indexer_ShouldReturnValueAsString()
{
// Arrange
var expected = "SHA256";
var builder = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.Build();
// Act
var actual = builder["Authentication"];
// Assert
actual.ShouldBeEquivalentTo(expected);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueAsInt()
{
// Arrange
var expectedFeature = 22;
var builder = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.Build();
// Act
var actualFeature = builder.GetSection("AnotherSetting:Temperature").Get<int>("Indoor:Max:Limit");
// Assert
actualFeature.ShouldBe(expectedFeature);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueAsBool()
{
// Arrange
var expectedFeature = true;
var configRoot = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.AddSmartConfig()
.Build();
// Act
var actualFeature = configRoot.Get<bool>("Database:UseSSL");
// Assert
actualFeature.ShouldBe(expectedFeature);
}
}
internal class Feature
{
public bool Enabled { get; set; }
public int RolloutPercentage { get; set; }
public List<string> AllowedUserGroups { get; set; }
}

View file

@ -0,0 +1,75 @@
using Newtonsoft.Json.Linq;
using SWP.Core.Configurations.Common;
namespace SWP.Core.X.TDD.ConfigurationTests;
[TestClass]
public class ConfigurationTests : TestFixture
{
[TestInitialize]
public void Init()
{
}
[TestMethod]
public void ConfigurationSettingsTest()
{
var pairs = new List<KeyValuePair<string, JToken>>
{
new("Debug", true),
// Database konfiguration
new("Database:ConnectionString", "Server=db.example.com;Port=5432"),
new("Database:Timeout", 30),
new("Database:UseSSL", true),
// Logging konfiguration med JObject
new("Logging:FileOptions", JObject.Parse(@"{
'Path': '/var/logs/app.log',
'MaxSizeMB': 100,
'RetentionDays': 7
}")),
// Feature flags med kompleks konfiguration
new("Features:Experimental", JObject.Parse(@"{
'Enabled': true,
'RolloutPercentage': 25,
'AllowedUserGroups': ['beta']
}")),
// API endpoints med array
new("API:Endpoints", "/api/users"),
new("API:Endpoints", "/api/products")
};
var result = KeyValueToJson.Convert(pairs);
var expected = JObject.Parse(@"{
'Debug' : true,
'Database': {
'ConnectionString': 'Server=db.example.com;Port=5432',
'Timeout': 30,
'UseSSL': true
},
'Logging': {
'FileOptions': {
'Path': '/var/logs/app.log',
'MaxSizeMB': 100,
'RetentionDays': 7
}
},
'Features': {
'Experimental': {
'Enabled': true,
'RolloutPercentage': 25,
'AllowedUserGroups': ['beta']
}
},
'API': {
'Endpoints': ['/api/users', '/api/products']
}
}");
Assert.IsTrue(JToken.DeepEquals(expected, result));
}
}

View file

@ -0,0 +1,82 @@
using Autofac;
using Insight.Database;
using SWP.Core.Configurations;
using SWP.Core.Configurations.JsonConfigProvider;
using SWP.Core.Configurations.SmartConfigProvider;
using SWP.Core.Database.ConnectionFactory;
using Shouldly;
namespace SWP.Core.X.TDD.ConfigurationTests;
[TestClass]
public class SmartConfigProviderTests : TestFixture
{
private const string _testFolder = "ConfigurationTests/";
[TestMethod]
public void TrySmartConfigWithOptionsForPostgres()
{
var config = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.AddSmartConfig(options => options.UsePostgres("DefaultConnection"))
.Build();
var actualFeature = config.Get<bool>("Database:UseSSL");
}
[TestMethod]
public void Get_ShouldReturnCorrectValueAsBool()
{
// Arrange
var expectedFeature = true;
var config = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.AddSmartConfig(options => options.UsePostgres("DefaultConnection"))
.Build();
// Act
var actualFeature = config.Get<bool>("Database:UseSSL");
// Assert
actualFeature.ShouldBe(expectedFeature);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueWhenSelectingIntoValueRowInConfigTable()
{
// Arrange
var expectedFeature = 100;
var builder = new ConfigurationBuilder()
.AddJsonFile($"{_testFolder}appconfiguration.dev.json")
.AddSmartConfig(options => options.UsePostgres("DefaultConnection"))
.Build();
// Act
var actualFeature = builder.GetSection("Logging:FileOptions").Get<int>("MaxSizeMB");
var withoutSectionThisAlsoWorks = builder.Get<int>("Logging:FileOptions:MaxSizeMB");
// Assert
actualFeature.ShouldBe(expectedFeature);
actualFeature.ShouldBe(withoutSectionThisAlsoWorks);
}
[TestMethod]
public void TryGetActiveConfigurations()
{
var connFactory = Container.Resolve<IDbConnectionFactory>();
const string sql = @"
SELECT id, ""key"", value, label, content_type,
valid_from, expires_at, created_at, modified_at, etag
FROM app_configuration
WHERE CURRENT_TIMESTAMP BETWEEN valid_from AND expires_at
OR (valid_from IS NULL AND expires_at IS NULL)";
using (var conn = connFactory.Create())
{
var result = conn.QuerySql(sql);
}
}
}

View file

@ -0,0 +1,74 @@
{
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.1.57;Port=5432;Database=sandbox;User Id=sathumper;Password=3911;"
},
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=07d2a2b9-5e8e-4924-836e-264f8438f6c5;IngestionEndpoint=https://northeurope-2.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/;ApplicationId=56748c39-2fa3-4880-a1e2-24068e791548",
"UseSeqLoggingTelemetryChannel": true
},
"SeqConfiguration": {
"IngestionEndpoint": "http://localhost:5341",
"ApiKey": null,
"Environment": "MSTEST"
},
"Authentication": "SHA256",
"Feature": {
"Enabled": true,
"RolloutPercentage": 25,
"AllowedUserGroups": [
"beta"
]
},
"AnotherSetting": {
"Thresholds": {
"High": "123",
"Low": "-1"
},
"Temperature": {
"Indoor": {
"Max": {
"Limit": 22
},
"Min": {
"Limit": 18
}
},
"Outdoor": {
"Max": {
"Limit": 12
},
"Min": {
"Limit": 9
}
}
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341",
"apiKey": ""
}
}
],
"Enrich": [
"WithMachineName",
"WithThreadId",
"WithProcessId",
"WithEnvironmentName"
],
"Properties": {
"Application": "PlanTempus"
}
}
}

View file

@ -0,0 +1,83 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SWP.Core.Entities.Users;
using SWP.Core.X.TDD.TestHelpers;
namespace SWP.Core.X.TDD.Entities;
[TestClass]
public class UserTests
{
[TestMethod]
public void User_ShouldHaveBasicProperties()
{
// Arrange & Act
var user = new User
{
Id = 1,
Email = "test@example.com",
PasswordHash = "hashedPassword",
SecurityStamp = "securityStamp",
EmailConfirmed = true,
CreatedDate = DateTime.UtcNow
};
// Assert
user.Id.ShouldBe(1);
user.Email.ShouldBe("test@example.com");
user.PasswordHash.ShouldBe("hashedPassword");
user.SecurityStamp.ShouldBe("securityStamp");
user.EmailConfirmed.ShouldBeTrue();
user.CreatedDate.ShouldBeInRange(DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1));
}
[TestMethod]
public void TestDataBuilder_ShouldCreateValidUser()
{
// Act
var user = TestDataBuilder.Users.CreateTestUser();
// Assert
user.ShouldNotBeNull();
user.Email.ShouldNotBeNullOrEmpty();
user.Email.ShouldContain("@example.com");
user.CreatedDate.ShouldBeInRange(DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow.AddMinutes(1));
}
[TestMethod]
public void TestDataBuilder_ShouldCreateUserWithCustomEmail()
{
// Arrange
var customEmail = "custom@test.com";
// Act
var user = TestDataBuilder.Users.CreateTestUser(customEmail);
// Assert
user.Email.ShouldBe(customEmail);
}
}
[TestClass]
public class OrganizationTests
{
[TestMethod]
public void Organization_ShouldHaveBasicProperties()
{
// Arrange & Act
var org = new Organization
{
Id = 1,
ConnectionString = "test connection",
CreatedDate = DateTime.UtcNow,
CreatedBy = 1,
IsActive = true
};
// Assert
org.Id.ShouldBe(1);
org.ConnectionString.ShouldBe("test connection");
org.CreatedBy.ShouldBe(1);
org.IsActive.ShouldBeTrue();
}
}

View file

@ -0,0 +1,71 @@
using System.Net;
using Autofac;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using SWP.Core.SeqLogging;
using SWP.Core.Telemetry;
namespace SWP.Core.X.TDD.Logging;
[TestClass]
public class SeqBackgroundServiceTest : TestFixture
{
private CancellationTokenSource _cts;
private IMessageChannel<ITelemetry> _messageChannel;
private SeqBackgroundService _service;
[TestInitialize]
public void SetupThis()
{
_messageChannel = new MessageChannel();
var telemetryClient = Container.Resolve<TelemetryClient>();
var config = new SeqConfiguration("http://localhost:5341", null, "MSTEST");
var httpClient = new SeqHttpClient(config);
var logger = new SeqLogger<SeqBackgroundService>(httpClient, config);
_service = new SeqBackgroundService(telemetryClient, _messageChannel, logger);
_cts = new CancellationTokenSource();
}
[TestMethod]
public async Task Messages_ShouldBeProcessedFromQueue()
{
await _service.StartAsync(_cts.Token);
for (var i = 0; i < 5; i++)
{
var eventTelemetry = new EventTelemetry
{
Name = "Test Event",
Timestamp = DateTimeOffset.UtcNow
};
eventTelemetry.Properties.Add("TestId", Guid.NewGuid().ToString());
eventTelemetry.Metrics.Add("TestMetric", 42.0);
await _messageChannel.Writer.WriteAsync(eventTelemetry);
}
// wait for processing
await Task.Delay(5000);
_cts.Cancel(); //not sure about this, we need to analyse more before this is "the way"
await _service.StopAsync(CancellationToken.None);
var hasMoreMessages = await _messageChannel.Reader.WaitToReadAsync();
Assert.IsFalse(hasMoreMessages, "Queue should be empty after 5 seconds");
}
private class TestMessageHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
}

View file

@ -0,0 +1,145 @@
using Autofac;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using SWP.Core.SeqLogging;
namespace SWP.Core.X.TDD.Logging;
[TestClass]
public class SeqLoggerTests : TestFixture
{
private readonly string _testId;
private readonly SeqHttpClient _httpClient;
private readonly SeqLogger<SeqLoggerTests> _logger;
public SeqLoggerTests()
{
_testId = Guid.NewGuid().ToString();
var config = new SeqConfiguration("http://localhost:5341", null, "MSTEST");
_httpClient = new SeqHttpClient(config);
_logger = new SeqLogger<SeqLoggerTests>(_httpClient, config);
}
[TestMethod]
public async Task LogTraceTelemetry_SendsCorrectDataWithErrorLevel()
{
// Arrange
var traceTelemetry = new TraceTelemetry
{
Message = "Test trace error message",
SeverityLevel = SeverityLevel.Error,
Timestamp = DateTimeOffset.UtcNow
};
traceTelemetry.Properties.Add("TestId", _testId);
// Act
await _logger.LogAsync(traceTelemetry);
}
[TestMethod]
public async Task LogTraceTelemetry_SendsCorrectDataWithWarningLevel()
{
// Arrange
var traceTelemetry = new TraceTelemetry
{
Message = "Test trace warning message",
SeverityLevel = SeverityLevel.Warning,
Timestamp = DateTimeOffset.UtcNow
};
traceTelemetry.Properties.Add("TestId", _testId);
// Act
await _logger.LogAsync(traceTelemetry);
}
[TestMethod]
public async Task LogEventTelemetry_SendsCorrectData()
{
// Arrange
var eventTelemetry = new EventTelemetry
{
Name = "Test Event",
Timestamp = DateTimeOffset.UtcNow
};
eventTelemetry.Properties.Add("TestId", _testId);
eventTelemetry.Metrics.Add("TestMetric", 42.0);
// Act
await _logger.LogAsync(eventTelemetry);
}
[TestMethod]
public async Task LogExceptionTelemetry_SendsCorrectData()
{
try
{
var t = 0;
var result = 10 / t;
}
catch (Exception e)
{
// Arrange
var exceptionTelemetry = new ExceptionTelemetry(e)
{
Timestamp = DateTimeOffset.UtcNow
};
exceptionTelemetry.Properties.Add("TestId", _testId);
// Act
await _logger.LogAsync(exceptionTelemetry);
}
}
[TestMethod]
public async Task LogDependencyTelemetry_SendsCorrectData()
{
// Arrange
var dependencyTelemetry = new DependencyTelemetry
{
Name = "SQL Query",
Type = "SQL",
Target = "TestDB",
Success = true,
Duration = TimeSpan.FromMilliseconds(100),
Timestamp = DateTimeOffset.UtcNow
};
dependencyTelemetry.Properties.Add("TestId", _testId);
// Act
await _logger.LogAsync(dependencyTelemetry);
}
/// <summary>
/// This is for scope test in SeqLogger. It is not testing anything related to the TelemetryChannel which logs to Seq.
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task LogRequestTelemetryInOperationHolderWithParentChild_SendsCorrectData()
{
var telemetryClient = Container.Resolve<TelemetryClient>();
using (var parent = telemetryClient.StartOperation<RequestTelemetry>("Parent First"))
{
parent.Telemetry.Duration = TimeSpan.FromMilliseconds(250);
parent.Telemetry.Url = new Uri("http://parent.test.com/api/test");
using (var child = telemetryClient.StartOperation<RequestTelemetry>("Child 1"))
{
child.Telemetry.Success = true;
child.Telemetry.ResponseCode = "200";
child.Telemetry.Duration = TimeSpan.FromMilliseconds(50);
child.Telemetry.Url = new Uri("http://child.test.com/api/test");
child.Telemetry.Timestamp = DateTimeOffset.UtcNow;
child.Telemetry.Properties.Add("httpMethod", HttpMethod.Get.ToString());
child.Telemetry.Properties.Add("TestId", _testId);
await _logger.LogAsync(child);
}
;
await _logger.LogAsync(parent);
}
}
}

View file

@ -0,0 +1,60 @@
using Autofac;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using SWP.Core.SeqLogging;
using SWP.Core.Telemetry;
namespace SWP.Core.X.TDD.Logging;
[TestClass]
public class SeqTelemetryChannelTest : TestFixture
{
private CancellationTokenSource _cts;
private IMessageChannel<ITelemetry> _messageChannel;
private SeqBackgroundService _service;
private TelemetryClient _telemetryClient;
[TestInitialize]
public void SetupThis()
{
//it is important to use the same MessageChannel as the BackgroundService uses
//we know that IMessageChannel<ITelemetry> _messageChannel; is registered via Autofac and manually injected into SeqBackgroundService
//so we can get it by calling the Autofac Container in this test.
_messageChannel = Container.Resolve<IMessageChannel<ITelemetry>>();
_service = Container.Resolve<SeqBackgroundService>();
_telemetryClient = Container.Resolve<TelemetryClient>();
_cts = new CancellationTokenSource();
}
[TestMethod]
public async Task Messages_ShouldBeProcessedFromQueue()
{
await _service.StartAsync(_cts.Token);
for (var i = 0; i < 5; i++)
{
var eventTelemetry = new EventTelemetry
{
Name = "Test Event 3",
Timestamp = DateTimeOffset.UtcNow
};
eventTelemetry.Properties.Add("TestId", Guid.NewGuid().ToString());
eventTelemetry.Metrics.Add("TestMetric", 42.0);
//we don't write to the _messageChannel.Writer.WriteAsync(eventTelemetry);, but the TelemetryClient which is configured to use SeqTelemetryChannel
_telemetryClient.TrackEvent(eventTelemetry);
}
// wait for processing
await Task.Delay(5000);
await _service.StopAsync(CancellationToken.None);
var hasMoreMessages = await _messageChannel.Reader.WaitToReadAsync();
Assert.IsFalse(hasMoreMessages, "Queue should be empty after 5 seconds");
}
}

View file

@ -0,0 +1,86 @@
using System.Diagnostics;
using System.Text;
using Sodium;
namespace SWP.Core.X.TDD;
[TestClass]
public class PasswordHasherTests : TestFixture
{
[TestMethod]
public void MyTestMethod()
{
var stopwatch = Stopwatch.StartNew();
var salt = PasswordHash.ScryptGenerateSalt();
// 2. Konverter password til byte[]
var passwordBytes = Encoding.UTF8.GetBytes("password123");
// 3. Kald ScryptHashBinary korrekt
var hash = PasswordHash.ScryptHashBinary(
passwordBytes,
salt
);
stopwatch.Stop();
}
[TestMethod]
public void HashPassword_ShouldCreateValidHashFormat()
{
// Arrange
var password = "TestPassword123";
// Act
var hashedPassword = new SecureTokenizer().TokenizeText(password);
var parts = hashedPassword.Split('.');
// Assert
Assert.AreEqual(3, parts.Length);
Assert.AreEqual("100000", parts[0]);
}
[TestMethod]
public void VerifyPassword_WithCorrectPassword_ShouldReturnTrue()
{
// Arrange
var password = "TestPassword123";
var hashedPassword = new SecureTokenizer().TokenizeText(password);
// Act
var result = new SecureTokenizer().VerifyToken(hashedPassword, password);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public void VerifyPassword_WithWrongPassword_ShouldReturnFalse()
{
// Arrange
var correctPassword = "TestPassword123";
var wrongPassword = "WrongPassword123";
var hashedPassword = new SecureTokenizer().TokenizeText(correctPassword);
// Act
var result = new SecureTokenizer().VerifyToken(hashedPassword, wrongPassword);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void VerifyPassword_WithInvalidHashFormat_ShouldReturnFalse()
{
// Arrange
var password = "TestPassword123";
var invalidHash = "InvalidHash";
// Act
var result = new SecureTokenizer().VerifyToken(invalidHash, password);
// Assert
Assert.IsFalse(result);
}
}

80
Tests/PostgresTests.cs Normal file
View file

@ -0,0 +1,80 @@
using Autofac;
using Insight.Database;
using Shouldly;
using SWP.Core.Database;
using SWP.Core.Database.ConnectionFactory;
namespace SWP.Core.X.TDD;
[TestClass]
public class PostgresTests : TestFixture
{
private IDbConnectionFactory _connFactory;
private IDatabaseOperations _databaseOperations;
[TestInitialize]
public void MyTestMethod()
{
_connFactory = Container.Resolve<IDbConnectionFactory>();
_databaseOperations = Container.Resolve<IDatabaseOperations>();
}
[TestMethod]
public void TestDefaultConnection()
{
//https://stackoverflow.com/questions/69169247/how-to-create-idbconnection-factory-using-autofac-for-dapper
using (var conn = _connFactory.Create())
conn.ExecuteSql("SELECT 1 as p");
}
[TestMethod]
public async Task TestScopeConnectionWithLogging()
{
using var db = _databaseOperations.CreateScope(nameof(TestScopeConnectionWithLogging));
try
{
var user = await db.Connection.QuerySqlAsync<string>(
"SELECT tablename FROM pg_tables limit 5");
}
catch (Exception ex)
{
db.Error(ex);
throw;
}
}
[TestMethod]
public async Task TestScopeConnectionWithErrorLogging()
{
using var db = _databaseOperations.CreateScope(nameof(TestScopeConnectionWithLogging));
try
{
var user = await db.Connection.QuerySqlAsync<string>(
"SELECT tablename FROM pg_tables limit 5");
}
catch (Exception ex)
{
db.Error(ex);
}
}
[TestMethod]
public async Task TestSimpleDatabaseOperation()
{
try
{
await _databaseOperations.ExecuteAsync(async connection =>
{
return await connection.QuerySqlAsync<string>(
"SELECT tablename FROM pg_tables limit 5");
}, nameof(TestSimpleDatabaseOperation));
}
catch (Exception)
{
throw;
}
}
}

View file

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<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" />
<PackageReference Include="Shouldly" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\SWP.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
<ItemGroup>
<None Update="appconfiguration.dev.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="ConfigurationTests\appconfiguration.dev.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="ConfigurationTests\appconfiguration.dev.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1 @@
namespace SWP.Core.X.TDD;

View file

@ -0,0 +1,86 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SWP.Core;
namespace SWP.Core.X.TDD.Security;
[TestClass]
public class SecureTokenizerTests
{
private ISecureTokenizer _tokenizer;
[TestInitialize]
public void Setup()
{
_tokenizer = new SecureTokenizer();
}
[TestMethod]
public void TokenizeText_ShouldReturnNonEmptyString()
{
// Act
var token = _tokenizer.TokenizeText("testPassword");
// Assert
token.ShouldNotBeNullOrEmpty();
}
[TestMethod]
public void TokenizeText_ShouldReturnDifferentTokensForSamePassword()
{
// Arrange
var password = "testPassword";
// Act
var token1 = _tokenizer.TokenizeText(password);
var token2 = _tokenizer.TokenizeText(password);
// Assert
token1.ShouldNotBe(token2);
}
[TestMethod]
public void VerifyToken_ShouldReturnTrueForValidPassword()
{
// Arrange
var password = "testPassword";
var token = _tokenizer.TokenizeText(password);
// Act
var result = _tokenizer.VerifyToken(token, password);
// Assert
result.ShouldBeTrue();
}
[TestMethod]
public void VerifyToken_ShouldReturnFalseForInvalidPassword()
{
// Arrange
var password = "testPassword";
var token = _tokenizer.TokenizeText(password);
// Act
var result = _tokenizer.VerifyToken(token, "wrongPassword");
// Assert
result.ShouldBeFalse();
}
[TestMethod]
public void VerifyToken_ShouldReturnFalseForMalformedToken()
{
// Act & Assert
_tokenizer.VerifyToken("invalid.token", "password").ShouldBeFalse();
_tokenizer.VerifyToken("", "password").ShouldBeFalse();
}
[TestMethod]
public void VerifyToken_ShouldHandleNullInputs()
{
// Act & Assert
Should.Throw<NullReferenceException>(() => _tokenizer.VerifyToken(null, "password"));
// Note: Current implementation doesn't handle null inputs gracefully
// This should be fixed in production code
}
}

87
Tests/TestFixture.cs Normal file
View file

@ -0,0 +1,87 @@
using System.Diagnostics;
using Autofac;
using Microsoft.ApplicationInsights;
using Microsoft.Extensions.Logging;
using SWP.Core.Configurations;
using SWP.Core.Configurations.JsonConfigProvider;
using SWP.Core.Database.ModuleRegistry;
using SWP.Core.ModuleRegistry;
using SWP.Core.SeqLogging;
namespace SWP.Core.X.TDD;
/// <summary>
/// Act as base class for tests. Avoids duplication of test setup code
/// </summary>
[TestClass]
public abstract class TestFixture
{
private readonly string _configurationFilePath;
protected TestFixture() : this(null)
{
}
public TestFixture(string configurationFilePath)
{
if (configurationFilePath is not null)
_configurationFilePath = configurationFilePath?.TrimEnd('/') + "/";
CreateContainerBuilder();
Container = ContainerBuilder.Build();
}
protected IContainer Container { get; private set; }
protected ContainerBuilder ContainerBuilder { get; private set; }
public virtual IConfigurationRoot Configuration()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile($"{_configurationFilePath}appconfiguration.dev.json")
.Build();
return configuration;
}
protected virtual void CreateContainerBuilder()
{
var configuration = Configuration();
var builder = new ContainerBuilder();
builder.RegisterGeneric(typeof(Logger<>))
.As(typeof(ILogger<>))
.SingleInstance();
builder.RegisterModule(new DbPostgreSqlModule
{
ConnectionString = configuration.GetConnectionString("DefaultConnection")
});
builder.RegisterModule(new TelemetryModule
{
TelemetryConfig = configuration.GetSection("ApplicationInsights").ToObject<TelemetryConfig>()
});
builder.RegisterModule(new SeqLoggingModule
{
SeqConfiguration = configuration.GetSection("SeqConfiguration").ToObject<SeqConfiguration>()
});
builder.RegisterModule<SecurityModule>();
ContainerBuilder = builder;
}
[TestCleanup]
public void CleanUp()
{
Trace.Flush();
var telemetryClient = Container.Resolve<TelemetryClient>();
telemetryClient.Flush();
if (Container is null) return;
Container.Dispose();
Container = null;
}
}

View file

@ -0,0 +1,33 @@
using Microsoft.Extensions.Configuration;
using SWP.Core.Entities.Users;
namespace SWP.Core.X.TDD.TestHelpers;
public static class TestDataBuilder
{
public static class Users
{
public static User CreateTestUser(string email = null)
{
return new User
{
Id = new Random().Next(1, 1000),
Email = email ?? $"test{Guid.NewGuid()}@example.com",
EmailConfirmed = false,
CreatedDate = DateTime.UtcNow
};
}
}
public static class Configuration
{
public static Dictionary<string, string> CreateTestConfiguration()
{
return new Dictionary<string, string>
{
["Database:ConnectionString"] = "Host=localhost;Database=test",
["Logging:Level"] = "Debug"
};
}
}
}

View file

@ -0,0 +1,14 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.1.57;Port=5432;Database=ptmain;User Id=sathumper;Password=3911;"
},
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=07d2a2b9-5e8e-4924-836e-264f8438f6c5;IngestionEndpoint=https://northeurope-2.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/;ApplicationId=56748c39-2fa3-4880-a1e2-24068e791548",
"UseSeqLoggingTelemetryChannel": true
},
"SeqConfiguration": {
"IngestionEndpoint": "http://localhost:5341",
"ApiKey": null,
"Environment": "MSTEST"
}
}