diff --git a/tests/Trax.Effect.Tests.Integration/UnitTests/Models/ModelToStringTests.cs b/tests/Trax.Effect.Tests.Integration/UnitTests/Models/ModelToStringTests.cs new file mode 100644 index 0000000..2fc0113 --- /dev/null +++ b/tests/Trax.Effect.Tests.Integration/UnitTests/Models/ModelToStringTests.cs @@ -0,0 +1,167 @@ +using FluentAssertions; +using NUnit.Framework; +using Trax.Effect.Enums; +using Trax.Effect.Models.BackgroundJob; +using Trax.Effect.Models.BackgroundJob.DTOs; +using Trax.Effect.Models.DeadLetter; +using Trax.Effect.Models.DeadLetter.DTOs; +using Trax.Effect.Models.Manifest; +using Trax.Effect.Models.Manifest.DTOs; +using Trax.Effect.Models.ManifestGroup; +using Trax.Effect.Models.WorkQueue; +using Trax.Effect.Models.WorkQueue.DTOs; + +namespace Trax.Effect.Tests.Integration.UnitTests.Models; + +[TestFixture] +public class ModelToStringTests +{ + [Test] + public void WorkQueue_Create_AndPropertiesAndToString_AllExercised() + { + var entry = WorkQueue.Create( + new CreateWorkQueue + { + TrainName = "T", + Input = "{}", + InputTypeName = "Trax.Tests.In", + ManifestId = 5, + Priority = 3, + ScheduledAt = DateTime.UtcNow, + DeadLetterId = null, + } + ); + + entry.TrainName.Should().Be("T"); + entry.Status.Should().Be(WorkQueueStatus.Queued); + entry.ManifestId.Should().Be(5); + entry.Priority.Should().Be(3); + entry.ScheduledAt.Should().NotBeNull(); + entry.DispatchedAt.Should().BeNull(); + entry.DispatchAttempts.Should().Be(0); + entry.MetadataId.Should().BeNull(); + entry.Manifest.Should().BeNull(); + entry.Metadata.Should().BeNull(); + entry.DeadLetter.Should().BeNull(); + entry.ToString().Should().NotBeNullOrEmpty().And.Contain("\"T\""); + } + + [Test] + public void WorkQueue_Create_PriorityClamped() + { + var low = WorkQueue.Create( + new CreateWorkQueue + { + TrainName = "T", + Input = "{}", + InputTypeName = "X", + Priority = -50, + } + ); + var high = WorkQueue.Create( + new CreateWorkQueue + { + TrainName = "T", + Input = "{}", + InputTypeName = "X", + Priority = 9999, + } + ); + + low.Priority.Should().BeGreaterThanOrEqualTo(0); + high.Priority.Should().BeLessThanOrEqualTo(31); + } + + [Test] + public void Manifest_Create_AndToString() + { + var manifest = Manifest.Create( + new CreateManifest + { + Name = typeof(ModelToStringTests), + IsEnabled = true, + ScheduleType = ScheduleType.Once, + IntervalSeconds = 60, + Properties = new Sample { Value = "hello" }, + } + ); + + manifest.IsEnabled.Should().BeTrue(); + manifest.ScheduleType.Should().Be(ScheduleType.Once); + manifest.IntervalSeconds.Should().Be(60); + manifest.PropertyTypeName.Should().NotBeNullOrEmpty(); + manifest.MaxRetries.Should().BeGreaterThanOrEqualTo(0); + manifest.ToString().Should().NotBeNullOrEmpty(); + } + + [Test] + public void ManifestGroup_PropertiesAndToString() + { + var group = new ManifestGroup + { + Name = "g", + MaxActiveJobs = 4, + Priority = 1, + IsEnabled = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + }; + + group.MaxActiveJobs.Should().Be(4); + group.Priority.Should().Be(1); + group.IsEnabled.Should().BeTrue(); + group.Manifests.Should().BeEmpty(); + group.ToString().Should().NotBeNullOrEmpty().And.Contain("\"g\""); + } + + [Test] + public void DeadLetter_Create_AndToString() + { + var manifest = Manifest.Create( + new CreateManifest + { + Name = typeof(ModelToStringTests), + IsEnabled = true, + ScheduleType = ScheduleType.Once, + Properties = new Sample { Value = "v" }, + } + ); + var dl = DeadLetter.Create( + new CreateDeadLetter + { + Manifest = manifest, + Reason = "boom", + RetryCount = 4, + } + ); + + dl.Reason.Should().Be("boom"); + dl.Status.Should().Be(DeadLetterStatus.AwaitingIntervention); + dl.ResolvedAt.Should().BeNull(); + dl.ResolutionNote.Should().BeNull(); + dl.ToString().Should().NotBeNullOrEmpty().And.Contain("\"boom\""); + } + + [Test] + public void BackgroundJob_Create_AndProperties() + { + var job = BackgroundJob.Create( + new CreateBackgroundJob + { + MetadataId = 99, + Input = "{\"value\":\"x\"}", + InputType = "Sample", + Priority = 1, + } + ); + + job.MetadataId.Should().Be(99); + job.InputType.Should().Be("Sample"); + job.ToString().Should().NotBeNullOrEmpty().And.Contain("99"); + } + + private sealed record Sample : IManifestProperties + { + public string Value { get; init; } = ""; + } +} diff --git a/tests/Trax.Effect.Tests.Integration/UnitTests/Services/CancellationCheckProviderTests.cs b/tests/Trax.Effect.Tests.Integration/UnitTests/Services/CancellationCheckProviderTests.cs new file mode 100644 index 0000000..e7ad648 --- /dev/null +++ b/tests/Trax.Effect.Tests.Integration/UnitTests/Services/CancellationCheckProviderTests.cs @@ -0,0 +1,106 @@ +using System.Reflection; +using FluentAssertions; +using LanguageExt; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Trax.Effect.Data.InMemory.Extensions; +using Trax.Effect.Data.Services.IDataContextFactory; +using Trax.Effect.Extensions; +using Trax.Effect.JunctionProvider.Progress.Services.CancellationCheckProvider; +using Trax.Effect.Models.Metadata; +using Trax.Effect.Models.Metadata.DTOs; +using Trax.Effect.Services.EffectJunction; +using Trax.Effect.Services.ServiceTrain; + +namespace Trax.Effect.Tests.Integration.UnitTests.Services; + +[TestFixture] +public class CancellationCheckProviderTests +{ + private static IDataContextProviderFactory BuildInMemoryFactory() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddTrax(trax => trax.AddEffects(effects => effects.UseInMemory())); + var sp = services.BuildServiceProvider(); + return sp.GetRequiredService(); + } + + [Test] + public async Task BeforeJunctionExecution_NullMetadata_ReturnsWithoutThrow() + { + var factory = BuildInMemoryFactory(); + var provider = new CancellationCheckProvider(factory); + var train = new TestTrain(); + var junction = new TestEffectJunction(); + + // train.Metadata is null — should be a no-op. + Func act = async () => + await provider.BeforeJunctionExecution(junction, train, CancellationToken.None); + + await act.Should().NotThrowAsync(); + } + + [Test] + public async Task BeforeJunctionExecution_MetadataNotInDb_ReturnsWithoutThrow() + { + var factory = BuildInMemoryFactory(); + var provider = new CancellationCheckProvider(factory); + var train = new TestTrain(); + var meta = Metadata.Create( + new CreateMetadata + { + Name = "T", + ExternalId = Guid.NewGuid().ToString("N"), + Input = null, + } + ); + SetTrainMetadata(train, meta); + var junction = new TestEffectJunction(); + + // Metadata not persisted to DB — query returns default(false) → no throw. + Func act = async () => + await provider.BeforeJunctionExecution(junction, train, CancellationToken.None); + + await act.Should().NotThrowAsync(); + } + + [Test] + public async Task AfterJunctionExecution_AlwaysCompletedSuccessfully() + { + var factory = BuildInMemoryFactory(); + var provider = new CancellationCheckProvider(factory); + + await provider.AfterJunctionExecution( + new TestEffectJunction(), + new TestTrain(), + CancellationToken.None + ); + } + + [Test] + public void Dispose_DoesNotThrow() + { + var factory = BuildInMemoryFactory(); + var provider = new CancellationCheckProvider(factory); + + Action act = () => provider.Dispose(); + act.Should().NotThrow(); + } + + private static void SetTrainMetadata(TestTrain train, Metadata metadata) => + typeof(ServiceTrain) + .GetProperty("Metadata", BindingFlags.Public | BindingFlags.Instance)! + .SetValue(train, metadata); + + private class TestTrain : ServiceTrain + { + protected override Task> RunInternal(string input) => + Task.FromResult>(input); + } + + private class TestEffectJunction : EffectJunction + { + public override Task Run(string input) => Task.FromResult(input); + } +} diff --git a/tests/Trax.Effect.Tests.Integration/UnitTests/Services/JunctionLoggerProviderTests.cs b/tests/Trax.Effect.Tests.Integration/UnitTests/Services/JunctionLoggerProviderTests.cs new file mode 100644 index 0000000..92b2dc6 --- /dev/null +++ b/tests/Trax.Effect.Tests.Integration/UnitTests/Services/JunctionLoggerProviderTests.cs @@ -0,0 +1,165 @@ +using System.Reflection; +using FluentAssertions; +using LanguageExt; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; +using Trax.Core.Exceptions; +using Trax.Effect.Configuration.TraxEffectConfiguration; +using Trax.Effect.JunctionProvider.Logging.Services.JunctionLoggerProvider; +using Trax.Effect.Models.JunctionMetadata; +using Trax.Effect.Models.JunctionMetadata.DTOs; +using Trax.Effect.Models.Metadata; +using Trax.Effect.Models.Metadata.DTOs; +using Trax.Effect.Services.EffectJunction; +using Trax.Effect.Services.ServiceTrain; + +namespace Trax.Effect.Tests.Integration.UnitTests.Services; + +[TestFixture] +public class JunctionLoggerProviderTests +{ + private static ITraxEffectConfiguration TestConfig(bool serializeJunctionData = true) => + new TraxEffectConfiguration + { + LogLevel = LogLevel.Information, + SerializeJunctionData = serializeJunctionData, + }; + + private static (TestTrain train, TestEffectJunction junction) BuildPair(string name) + { + var train = new TestTrain(); + var trainMeta = Metadata.Create( + new CreateMetadata + { + Name = "TestTrain", + ExternalId = Guid.NewGuid().ToString("N"), + Input = null, + } + ); + typeof(ServiceTrain) + .GetProperty("Metadata", BindingFlags.Public | BindingFlags.Instance)! + .SetValue(train, trainMeta); + + var junction = new TestEffectJunction(); + var junctionMeta = JunctionMetadata.Create( + new CreateJunctionMetadata + { + Name = name, + ExternalId = Guid.NewGuid().ToString("N"), + InputType = typeof(string), + OutputType = typeof(string), + State = EitherStatus.IsRight, + }, + trainMeta + ); + typeof(EffectJunction) + .GetProperty("Metadata", BindingFlags.Public | BindingFlags.Instance)! + .SetValue(junction, junctionMeta); + + return (train, junction); + } + + [Test] + public async Task BeforeJunctionExecution_LogsAtConfiguredLevel() + { + var (train, junction) = BuildPair("BeforeTest"); + var provider = new JunctionLoggerProvider( + TestConfig(), + NullLogger.Instance + ); + + await provider.BeforeJunctionExecution(junction, train, CancellationToken.None); + } + + [Test] + public async Task BeforeJunctionExecution_NullMetadata_Throws() + { + var train = new TestTrain(); + var junction = new TestEffectJunction(); // no metadata set + var provider = new JunctionLoggerProvider( + TestConfig(), + NullLogger.Instance + ); + + Func act = async () => + await provider.BeforeJunctionExecution(junction, train, CancellationToken.None); + + await act.Should().ThrowAsync().WithMessage("*Metadata*"); + } + + [Test] + public async Task AfterJunctionExecution_NullMetadata_Throws() + { + var train = new TestTrain(); + var junction = new TestEffectJunction(); + var provider = new JunctionLoggerProvider( + TestConfig(), + NullLogger.Instance + ); + + Func act = async () => + await provider.AfterJunctionExecution(junction, train, CancellationToken.None); + + await act.Should().ThrowAsync().WithMessage("*Metadata*"); + } + + [Test] + public async Task AfterJunctionExecution_AfterRailwayRun_SerializesRightOutput() + { + var (train, junction) = BuildPair("AfterRight"); + + // Drive a real RailwayJunction execution so Result is populated by the junction itself. + await junction.RailwayJunction(Either.Right("hello"), train); + + var provider = new JunctionLoggerProvider( + TestConfig(serializeJunctionData: true), + NullLogger.Instance + ); + + await provider.AfterJunctionExecution(junction, train, CancellationToken.None); + + junction.Metadata!.OutputJson.Should().NotBeNull(); + junction.Metadata.OutputJson!.Should().Contain("hello-out"); + } + + [Test] + public async Task AfterJunctionExecution_SerializeDisabled_LeavesOutputJsonNull() + { + var (train, junction) = BuildPair("AfterNoSerialize"); + + await junction.RailwayJunction(Either.Right("v"), train); + + var provider = new JunctionLoggerProvider( + TestConfig(serializeJunctionData: false), + NullLogger.Instance + ); + + await provider.AfterJunctionExecution(junction, train, CancellationToken.None); + + junction.Metadata!.OutputJson.Should().BeNull(); + } + + [Test] + public void Dispose_DoesNotThrow() + { + var provider = new JunctionLoggerProvider( + TestConfig(), + NullLogger.Instance + ); + + Action act = () => provider.Dispose(); + act.Should().NotThrow(); + } + + private class TestTrain : ServiceTrain + { + protected override Task> RunInternal(string input) => + Task.FromResult>(input); + } + + private class TestEffectJunction : EffectJunction + { + public override Task Run(string input) => Task.FromResult(input + "-out"); + } +}