(feature): saga tests and file logger improvements

This commit is contained in:
Maksym Sadovnychyy 2025-11-01 16:34:38 +01:00
parent cb83909ba2
commit 1fba73f690
6 changed files with 336 additions and 12 deletions

View File

@ -0,0 +1,106 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MaksIT.Core.Logging;
namespace MaksIT.Core.Tests.Logging;
public class FileLoggerTests {
private readonly string _testFolderPath;
public FileLoggerTests() {
_testFolderPath = Path.Combine(Path.GetTempPath(), "FileLoggerTests");
if (Directory.Exists(_testFolderPath)) {
Directory.Delete(_testFolderPath, true);
}
Directory.CreateDirectory(_testFolderPath);
}
[Fact]
public void ShouldWriteLogsToCorrectFile() {
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IHostEnvironment>(sp =>
new TestHostEnvironment {
EnvironmentName = Environments.Development,
ApplicationName = "TestApp",
ContentRootPath = Directory.GetCurrentDirectory()
});
serviceCollection.AddLogging(builder => builder.AddFile(_testFolderPath, TimeSpan.FromDays(7)));
var provider = serviceCollection.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<FileLoggerTests>>();
// Act
logger.LogInformation("Test log message");
// Assert
var logFile = Directory.GetFiles(_testFolderPath, "log_*.txt").FirstOrDefault();
Assert.NotNull(logFile);
var logContent = File.ReadAllText(logFile);
Assert.Contains("Test log message", logContent);
}
[Fact]
public void ShouldDeleteOldLogsBasedOnRetention() {
// Arrange
var retentionPeriod = TimeSpan.FromDays(1);
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IHostEnvironment>(sp =>
new TestHostEnvironment {
EnvironmentName = Environments.Development,
ApplicationName = "TestApp",
ContentRootPath = Directory.GetCurrentDirectory()
});
serviceCollection.AddLogging(builder => builder.AddFile(_testFolderPath, retentionPeriod));
var provider = serviceCollection.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<FileLoggerTests>>();
// Create an old log file
var oldLogFile = Path.Combine(_testFolderPath, $"log_{DateTime.Now.AddDays(-2):yyyy-MM-dd}.txt");
File.WriteAllText(oldLogFile, "Old log");
// Act
logger.LogInformation("New log message");
// Assert
Assert.False(File.Exists(oldLogFile), "Old log file should have been deleted.");
var logFile = Directory.GetFiles(_testFolderPath, "log_*.txt").FirstOrDefault();
Assert.NotNull(logFile);
var logContent = File.ReadAllText(logFile);
Assert.Contains("New log message", logContent);
}
[Fact]
public void ShouldHandleExceptionsGracefully() {
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IHostEnvironment>(sp =>
new TestHostEnvironment {
EnvironmentName = Environments.Development,
ApplicationName = "TestApp",
ContentRootPath = Directory.GetCurrentDirectory()
});
serviceCollection.AddLogging(builder => builder.AddFile(_testFolderPath));
var provider = serviceCollection.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<FileLoggerTests>>();
// Act & Assert
try {
logger.LogError(new InvalidOperationException("Test exception"), "An error occurred");
var logFile = Directory.GetFiles(_testFolderPath, "log_*.txt").FirstOrDefault();
Assert.NotNull(logFile);
var logContent = File.ReadAllText(logFile);
Assert.Contains("An error occurred", logContent);
Assert.Contains("Test exception", logContent);
} catch {
Assert.Fail("Logger should handle exceptions gracefully.");
}
}
}

View File

@ -187,4 +187,200 @@ public class LocalSagaTests
Assert.Contains("Step2 compensated", compensationLog); Assert.Contains("Step2 compensated", compensationLog);
Assert.Contains("Step1 compensated", compensationLog); Assert.Contains("Step1 compensated", compensationLog);
} }
[Fact]
public async Task LocalSagaBuilder_ShouldBuildSagaWithStepsAndReturnResult()
{
// Arrange
var logger = LoggerHelper.CreateConsoleLogger();
var builder = new LocalSagaBuilder(logger);
var stepResult = "";
builder.AddStep<string>(
"TestStep",
async (ctx, ct) =>
{
await Task.CompletedTask;
return "StepResult";
},
outputKey: "stepResult");
var saga = builder.Build();
var context = new LocalSagaContext();
// Act
await saga.ExecuteAsync(context);
stepResult = context.Get<string>("stepResult");
// Assert
Assert.Equal("StepResult", stepResult);
}
[Fact]
public async Task LocalSagaBuilder_ShouldSkipConditionalStep()
{
// Arrange
var logger = LoggerHelper.CreateConsoleLogger();
var builder = new LocalSagaBuilder(logger);
var stepExecuted = false;
builder.AddStepIf<string>(
ctx => false,
"SkippedStep",
async (ctx, ct) =>
{
stepExecuted = true;
return "Skipped";
},
outputKey: "skippedResult");
var saga = builder.Build();
// Act
await saga.ExecuteAsync();
// Assert
Assert.False(stepExecuted, "The step should have been skipped.");
}
[Fact]
public async Task LocalSagaBuilder_ShouldCompensateStepOnFailure()
{
// Arrange
var logger = LoggerHelper.CreateConsoleLogger();
var builder = new LocalSagaBuilder(logger);
var compensationCalled = false;
builder.AddStep<string>(
"FailingStep",
async (ctx, ct) =>
{
throw new InvalidOperationException("Step failed");
},
outputKey: "failingResult",
compensate: async (ctx, ct) =>
{
compensationCalled = true;
await Task.CompletedTask;
});
var saga = builder.Build();
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => saga.ExecuteAsync());
Assert.True(compensationCalled, "Compensation should have been called.");
}
[Fact]
public async Task LocalSagaBuilder_ShouldStoreStepOutputInContext()
{
// Arrange
var logger = LoggerHelper.CreateConsoleLogger();
var builder = new LocalSagaBuilder(logger);
var context = new LocalSagaContext();
builder.AddStep<int>(
"OutputStep",
async (ctx, ct) =>
{
await Task.CompletedTask;
return 42;
},
outputKey: "result");
var saga = builder.Build();
// Act
await saga.ExecuteAsync(context);
var result = context.Get<int>("result");
// Assert
Assert.Equal(42, result);
}
[Fact]
public async Task LocalSagaBuilder_ShouldSaveBackupValueAndRestoreOnError()
{
// Arrange
var logger = LoggerHelper.CreateConsoleLogger();
var builder = new LocalSagaBuilder(logger);
var context = new LocalSagaContext();
context.Set("state", "initial");
builder.AddStep<string>(
"ModifyStateStep",
async (ctx, ct) =>
{
// Save the original value to a backup key
var originalState = ctx.Get<string>("state");
ctx.Set("backup_state", originalState);
// Modify the state
ctx.Set("state", "modified");
await Task.CompletedTask;
return "modified";
},
outputKey: "modifyStateResult",
compensate: async (ctx, ct) =>
{
// Restore the original value from the backup key
var backupState = ctx.Get<string>("backup_state");
ctx.Set("state", backupState);
await Task.CompletedTask;
});
builder.AddStep<string>(
"FailingStep",
async (ctx, ct) =>
{
throw new InvalidOperationException("Step failed");
},
outputKey: "failingResult");
var saga = builder.Build();
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => saga.ExecuteAsync(context));
Assert.Equal("initial", context.Get<string>("state"));
}
[Fact]
public async Task LocalSagaBuilder_ShouldUseOutputKeyToStoreAndRetrieveValues()
{
// Arrange
var logger = LoggerHelper.CreateConsoleLogger();
var builder = new LocalSagaBuilder(logger);
var context = new LocalSagaContext();
builder.AddStep<int>(
"Step1",
async (ctx, ct) =>
{
await Task.CompletedTask;
return 100;
},
outputKey: "step1Result");
builder.AddStep<int>(
"Step2",
async (ctx, ct) =>
{
var step1Result = ctx.Get<int>("step1Result");
await Task.CompletedTask;
return step1Result + 50;
},
outputKey: "step2Result");
var saga = builder.Build();
// Act
await saga.ExecuteAsync(context);
// Assert
var step1Result = context.Get<int>("step1Result");
var step2Result = context.Get<int>("step2Result");
Assert.Equal(100, step1Result);
Assert.Equal(150, step2Result);
}
} }

View File

@ -1,14 +1,17 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.IO;
namespace MaksIT.Core.Logging; namespace MaksIT.Core.Logging;
public class FileLogger : ILogger { public class FileLogger : ILogger {
private readonly string _filePath; private readonly string _folderPath;
private readonly object _lock = new object(); private readonly object _lock = new object();
private readonly TimeSpan _retentionPeriod;
public FileLogger(string filePath) { public FileLogger(string folderPath, TimeSpan retentionPeriod) {
_filePath = filePath; _folderPath = folderPath;
_retentionPeriod = retentionPeriod;
Directory.CreateDirectory(_folderPath); // Ensure the folder exists
} }
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null; public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
@ -30,8 +33,25 @@ public class FileLogger : ILogger {
logRecord += Environment.NewLine + exception; logRecord += Environment.NewLine + exception;
} }
var logFileName = Path.Combine(_folderPath, $"log_{DateTime.Now:yyyy-MM-dd}.txt"); // Generate log file name by date
lock (_lock) { lock (_lock) {
File.AppendAllText(_filePath, logRecord + Environment.NewLine); File.AppendAllText(logFileName, logRecord + Environment.NewLine);
CleanUpOldLogs();
}
}
private void CleanUpOldLogs() {
var logFiles = Directory.GetFiles(_folderPath, "log_*.txt");
var expirationDate = DateTime.Now - _retentionPeriod;
foreach (var logFile in logFiles) {
var fileName = Path.GetFileNameWithoutExtension(logFile);
if (DateTime.TryParseExact(fileName.Substring(4), "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out var logDate)) {
if (logDate < expirationDate) {
File.Delete(logFile);
}
}
} }
} }
} }

View File

@ -9,14 +9,16 @@ namespace MaksIT.Core.Logging;
[ProviderAlias("FileLogger")] [ProviderAlias("FileLogger")]
public class FileLoggerProvider : ILoggerProvider { public class FileLoggerProvider : ILoggerProvider {
private readonly string _filePath; private readonly string _folderPath;
private readonly TimeSpan _retentionPeriod;
public FileLoggerProvider(string filePath) { public FileLoggerProvider(string folderPath, TimeSpan? retentionPeriod = null) {
_filePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); _folderPath = folderPath ?? throw new ArgumentNullException(nameof(folderPath));
_retentionPeriod = retentionPeriod ?? TimeSpan.FromDays(7); // Default retention period is 7 days
} }
public ILogger CreateLogger(string categoryName) { public ILogger CreateLogger(string categoryName) {
return new FileLogger(_filePath); return new FileLogger(_folderPath, _retentionPeriod);
} }
public void Dispose() { } public void Dispose() { }

View File

@ -5,8 +5,8 @@ using Microsoft.Extensions.Hosting;
namespace MaksIT.Core.Logging; namespace MaksIT.Core.Logging;
public static class LoggingBuilderExtensions { public static class LoggingBuilderExtensions {
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filePath) { public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string folderPath, TimeSpan? retentionPeriod = null) {
builder.Services.AddSingleton<ILoggerProvider>(new FileLoggerProvider(filePath)); builder.Services.AddSingleton<ILoggerProvider>(new FileLoggerProvider(folderPath, retentionPeriod));
return builder; return builder;
} }
public static ILoggingBuilder AddConsole(this ILoggingBuilder logging, IHostEnvironment env) { public static ILoggingBuilder AddConsole(this ILoggingBuilder logging, IHostEnvironment env) {

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId> <PackageId>MaksIT.Core</PackageId>
<Version>1.4.9</Version> <Version>1.5.0</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product> <Product>MaksIT.Core</Product>