(feature): saga tests and file logger improvements
This commit is contained in:
parent
cb83909ba2
commit
1fba73f690
106
src/MaksIT.Core.Tests/Logging/FileLoggerTests.cs
Normal file
106
src/MaksIT.Core.Tests/Logging/FileLoggerTests.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -187,4 +187,200 @@ public class LocalSagaTests
|
||||
Assert.Contains("Step2 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace MaksIT.Core.Logging;
|
||||
|
||||
public class FileLogger : ILogger {
|
||||
private readonly string _filePath;
|
||||
private readonly string _folderPath;
|
||||
private readonly object _lock = new object();
|
||||
private readonly TimeSpan _retentionPeriod;
|
||||
|
||||
public FileLogger(string filePath) {
|
||||
_filePath = filePath;
|
||||
public FileLogger(string folderPath, TimeSpan retentionPeriod) {
|
||||
_folderPath = folderPath;
|
||||
_retentionPeriod = retentionPeriod;
|
||||
Directory.CreateDirectory(_folderPath); // Ensure the folder exists
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
@ -30,8 +33,25 @@ public class FileLogger : ILogger {
|
||||
logRecord += Environment.NewLine + exception;
|
||||
}
|
||||
|
||||
var logFileName = Path.Combine(_folderPath, $"log_{DateTime.Now:yyyy-MM-dd}.txt"); // Generate log file name by date
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,14 +9,16 @@ namespace MaksIT.Core.Logging;
|
||||
|
||||
[ProviderAlias("FileLogger")]
|
||||
public class FileLoggerProvider : ILoggerProvider {
|
||||
private readonly string _filePath;
|
||||
private readonly string _folderPath;
|
||||
private readonly TimeSpan _retentionPeriod;
|
||||
|
||||
public FileLoggerProvider(string filePath) {
|
||||
_filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
||||
public FileLoggerProvider(string folderPath, TimeSpan? retentionPeriod = null) {
|
||||
_folderPath = folderPath ?? throw new ArgumentNullException(nameof(folderPath));
|
||||
_retentionPeriod = retentionPeriod ?? TimeSpan.FromDays(7); // Default retention period is 7 days
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName) {
|
||||
return new FileLogger(_filePath);
|
||||
return new FileLogger(_folderPath, _retentionPeriod);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@ -5,8 +5,8 @@ using Microsoft.Extensions.Hosting;
|
||||
namespace MaksIT.Core.Logging;
|
||||
|
||||
public static class LoggingBuilderExtensions {
|
||||
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filePath) {
|
||||
builder.Services.AddSingleton<ILoggerProvider>(new FileLoggerProvider(filePath));
|
||||
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string folderPath, TimeSpan? retentionPeriod = null) {
|
||||
builder.Services.AddSingleton<ILoggerProvider>(new FileLoggerProvider(folderPath, retentionPeriod));
|
||||
return builder;
|
||||
}
|
||||
public static ILoggingBuilder AddConsole(this ILoggingBuilder logging, IHostEnvironment env) {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>MaksIT.Core</PackageId>
|
||||
<Version>1.4.9</Version>
|
||||
<Version>1.5.0</Version>
|
||||
<Authors>Maksym Sadovnychyy</Authors>
|
||||
<Company>MAKS-IT</Company>
|
||||
<Product>MaksIT.Core</Product>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user