Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
d3d1efc
test: add validation builder contract tests
codefoxx May 21, 2026
cce8f35
test: add validation result contract tests
codefoxx May 21, 2026
e709b7c
fix: disambiguate validation type in tests
codefoxx May 21, 2026
179b1ba
fix: use unsupported selector shape in validation test
codefoxx May 21, 2026
d2ba2c2
test: add endpoint result mapping contract tests
codefoxx May 21, 2026
3613308
fix endpoint result mapping tests
codefoxx May 21, 2026
0533371
fix: keep arrange act assert comments in endpoint tests
codefoxx May 21, 2026
c72bbca
fix: configure request services for result mapping tests
codefoxx May 21, 2026
b444911
fix: reuse lightweight api test factory in result mapping tests
codefoxx May 21, 2026
0c9aca8
refactor: use lightweight api factory fixture
codefoxx May 21, 2026
f10109f
test: cover global exception handler behavior
codefoxx May 22, 2026
471c319
fix: keep response body stream open in exception handler tests
codefoxx May 22, 2026
9670e3c
fix: simulate started response in exception handler tests
codefoxx May 22, 2026
ca831b6
fix: configure request feature for started response tests
codefoxx May 22, 2026
49e6635
fix: configure response body feature for started response tests
codefoxx May 22, 2026
27f0c26
test: harden result contract tests
codefoxx May 22, 2026
bc372dc
test: harden option contract tests
codefoxx May 22, 2026
98405db
test: harden error contract tests
codefoxx May 22, 2026
1d5ccff
refactor: split result tests into partials
codefoxx May 22, 2026
a6469f4
refactor: add result success failure partial
codefoxx May 22, 2026
2d089a1
refactor: add result match partial
codefoxx May 22, 2026
e56c4ba
fix: use explicit body for result partial base
codefoxx May 22, 2026
b6a9e59
refactor: add result map partial
codefoxx May 22, 2026
3cb9c82
refactor: add result bind partial
codefoxx May 22, 2026
07773fa
refactor: add result bind async partial
codefoxx May 22, 2026
fc46f88
refactor: add non generic result partial
codefoxx May 22, 2026
96420aa
refactor: split option tests into partials
codefoxx May 22, 2026
e73fac1
refactor: add option construction partial
codefoxx May 22, 2026
7ab9690
refactor: add option match partial
codefoxx May 22, 2026
e43297c
refactor: add option map partial
codefoxx May 22, 2026
3c0158c
refactor: add option bind partial
codefoxx May 22, 2026
974cddc
refactor: add option where partial
codefoxx May 22, 2026
4097468
refactor: add option where not partial
codefoxx May 22, 2026
01d282f
refactor: add option or else partial
codefoxx May 22, 2026
5c1a102
refactor: add option from nullable partial
codefoxx May 22, 2026
e80ceb0
refactor: add option try get value partial
codefoxx May 22, 2026
8a75df1
refactor: split error tests into partials
codefoxx May 22, 2026
e7f4490
refactor: add error none partial
codefoxx May 22, 2026
dedbc3c
refactor: add error code partial
codefoxx May 22, 2026
8c2d1e7
refactor: add error validation partial
codefoxx May 22, 2026
364ba6e
refactor: add error not found partial
codefoxx May 22, 2026
901c68e
refactor: add error conflict partial
codefoxx May 22, 2026
a0dcb50
refactor: add error unknown partial
codefoxx May 22, 2026
a465b96
refactor: add error domain mapping partial
codefoxx May 22, 2026
6a5f1df
refactor: nest partial contract tests in project file
codefoxx May 22, 2026
42c70dc
fix: avoid source variant suffix in error tests
codefoxx May 22, 2026
39807b0
fix: remove source variant suffix test file
codefoxx May 22, 2026
9b1fd27
fix: update nested error test file name
codefoxx May 22, 2026
d7915a2
test: harden feature composition contracts
codefoxx May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,123 @@
using Company.Template.Composition.Abstractions.Contexts;
using Company.Template.Composition.Abstractions.Contracts;
using Microsoft.Extensions.Configuration;

namespace Company.Template.Api.Tests.Composition;

public sealed class FeatureCompositionTests
{
[Fact]
public void AddFeatureServicesFromAssemblies_WithEmptyMarkers_ThrowsArgumentException()
{
// Arrange
ServiceCollection services = [];

// Act
Action action = () => services.AddFeatureServicesFromAssemblies();

// Assert
ArgumentException exception = action.ShouldThrow<ArgumentException>();
exception.Message.ShouldContain("At least one assembly is required.");
}

[Fact]
public void AddFeatureServicesFromAssemblies_WithNullMarker_ThrowsArgumentNullException()
{
// Arrange
ServiceCollection services = [];

// Act
Action action = () => services.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests), null!);

// Assert
action.ShouldThrow<ArgumentNullException>();
}

[Fact]
public void WithConfiguration_PassesConfigurationToServiceModule()
{
// Arrange
ServiceCollection services = [];
IConfiguration configuration = CreateConfiguration("composition-value");

// Act
services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.WithConfiguration(configuration)
.ComposeFeatures(features => features.Add<ConfigurationFeature>());

using ServiceProvider provider = services.BuildServiceProvider();

// Assert
provider.GetRequiredService<ConfigurationValue>()
.Value
.ShouldBe("composition-value");
}

[Fact]
public void RequireConfiguration_WithoutConfiguration_ThrowsClearInvalidOperationException()
{
// Arrange
ServiceCollection services = [];

// Act
Action action = () => services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features.Add<ConfigurationFeature>());

// Assert
InvalidOperationException exception = action.ShouldThrow<InvalidOperationException>();
exception.Message.ShouldContain("requires configuration");
exception.Message.ShouldContain("WithConfiguration");
}

[Fact]
public void ComposeFeatures_AppliesServiceModulesInDeterministicOrder()
{
// Arrange
ServiceCollection services = [];

// Act
services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features.Add<OrderedServiceFeature>());

using ServiceProvider provider = services.BuildServiceProvider();

// Assert
provider.GetServices<IOrderedModuleRegistration>()
.Select(registration => registration.Name)
.ShouldBe(["alpha", "beta"]);
}

[Fact]
public void ComposeFeatures_AppliesDecoratorModulesInDeterministicOrder()
{
// Arrange
ServiceCollection services = [];

// Act
services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features
.Add<OrderedDecoratorServiceFeature>()
.Decorate<OrderedDecoratorFeature>());

using ServiceProvider provider = services.BuildServiceProvider();

// Assert
provider.GetRequiredService<IOrderedDecoratorService>()
.Execute()
.ShouldBe("beta alpha inner");
}

[Fact]
public void ComposeFeatures_AppliesQueuedDecoratorsAfterServiceModules()
{
// Arrange
ServiceCollection services = [];

// Act
services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features
Expand All @@ -18,6 +126,7 @@ public void ComposeFeatures_AppliesQueuedDecoratorsAfterServiceModules()

using ServiceProvider provider = services.BuildServiceProvider();

// Assert
provider.GetRequiredService<ITestService>()
.Execute()
.ShouldBe("decorated inner");
Expand All @@ -26,8 +135,10 @@ public void ComposeFeatures_AppliesQueuedDecoratorsAfterServiceModules()
[Fact]
public void ComposeFeatures_IgnoresDecoratorModulesForDifferentDecoratorFeature()
{
// Arrange
ServiceCollection services = [];

// Act
services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features
Expand All @@ -36,6 +147,7 @@ public void ComposeFeatures_IgnoresDecoratorModulesForDifferentDecoratorFeature(

using ServiceProvider provider = services.BuildServiceProvider();

// Assert
provider.GetRequiredService<ITestService>()
.Execute()
.ShouldBe("decorated inner");
Expand All @@ -44,36 +156,58 @@ public void ComposeFeatures_IgnoresDecoratorModulesForDifferentDecoratorFeature(
[Fact]
public void ComposeFeatures_ThrowsWhenDecoratorFeatureIsQueuedTwice()
{
// Arrange
ServiceCollection services = [];

InvalidOperationException exception = Should.Throw<InvalidOperationException>(() =>
{
services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features
.Decorate<TestDecoratorFeature>()
.Decorate<TestDecoratorFeature>());
});
// Act
Action action = () => services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features
.Decorate<TestDecoratorFeature>()
.Decorate<TestDecoratorFeature>());

// Assert
InvalidOperationException exception = action.ShouldThrow<InvalidOperationException>();
exception.Message.ShouldContain("was queued more than once");
}

[Fact]
public void ComposeFeatures_ThrowsWhenDecoratorModuleIsMissing()
{
// Arrange
ServiceCollection services = [];

InvalidOperationException exception = Should.Throw<InvalidOperationException>(() =>
{
services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features.Decorate<MissingDecoratorFeature>());
});
// Act
Action action = () => services
.AddFeatureServicesFromAssemblies(typeof(FeatureCompositionTests))
.ComposeFeatures(features => features.Decorate<MissingDecoratorFeature>());

// Assert
InvalidOperationException exception = action.ShouldThrow<InvalidOperationException>();
exception.Message.ShouldContain("No service decorator modules were found");
exception.Message.ShouldContain(nameof(MissingDecoratorFeature));
}

private static IConfiguration CreateConfiguration(string value)
{
Dictionary<string, string?> values = new()
{
["Feature:Value"] = value
};

return new ConfigurationBuilder()
.AddInMemoryCollection(values)
.Build();
}

public sealed class ConfigurationFeature : IFeature;

public sealed class OrderedServiceFeature : IFeature;

public sealed class OrderedDecoratorServiceFeature : IFeature;

public sealed class OrderedDecoratorFeature : IFeature;

public sealed class TestServiceFeature : IFeature;

public sealed class TestDecoratorFeature : IFeature;
Expand All @@ -82,6 +216,58 @@ public sealed class OtherDecoratorFeature : IFeature;

public sealed class MissingDecoratorFeature : IFeature;

public sealed record ConfigurationValue(string Value);

public interface IOrderedModuleRegistration
{
string Name { get; }
}

public sealed record OrderedModuleRegistration(string Name) : IOrderedModuleRegistration;

public interface IOrderedDecoratorService
{
string Execute();
}

public sealed class OrderedDecoratorService : IOrderedDecoratorService
{
public string Execute()
{
return "inner";
}
}

public sealed class AlphaOrderedDecoratorService : IOrderedDecoratorService
{
private readonly IOrderedDecoratorService _inner;

public AlphaOrderedDecoratorService(IOrderedDecoratorService inner)
{
_inner = inner;
}

public string Execute()
{
return $"alpha {_inner.Execute()}";
}
}

public sealed class BetaOrderedDecoratorService : IOrderedDecoratorService
{
private readonly IOrderedDecoratorService _inner;

public BetaOrderedDecoratorService(IOrderedDecoratorService inner)
{
_inner = inner;
}

public string Execute()
{
return $"beta {_inner.Execute()}";
}
}

public interface ITestService
{
string Execute();
Expand Down Expand Up @@ -125,6 +311,60 @@ public string Execute()
}
}

public sealed class ConfigurationModule : IFeatureServiceModule<ConfigurationFeature>
{
public void Register(FeatureServiceContext context)
{
IConfiguration configuration = context.RequireConfiguration();
string value = configuration["Feature:Value"]
?? throw new InvalidOperationException("Feature value is missing.");

context.Services.AddSingleton(new ConfigurationValue(value));
}
}

public sealed class AlphaOrderedServiceModule : IFeatureServiceModule<OrderedServiceFeature>
{
public void Register(FeatureServiceContext context)
{
context.Services.AddSingleton<IOrderedModuleRegistration>(new OrderedModuleRegistration("alpha"));
}
}

public sealed class BetaOrderedServiceModule : IFeatureServiceModule<OrderedServiceFeature>
{
public void Register(FeatureServiceContext context)
{
context.Services.AddSingleton<IOrderedModuleRegistration>(new OrderedModuleRegistration("beta"));
}
}

public sealed class OrderedDecoratorServiceModule : IFeatureServiceModule<OrderedDecoratorServiceFeature>
{
public void Register(FeatureServiceContext context)
{
context.Services.AddScoped<IOrderedDecoratorService, OrderedDecoratorService>();
}
}

public sealed class AlphaOrderedDecoratorModule :
IFeatureServiceDecoratorModule<OrderedDecoratorServiceFeature, OrderedDecoratorFeature>
{
public void Decorate(FeatureServiceContext context)
{
context.Services.Decorate<IOrderedDecoratorService, AlphaOrderedDecoratorService>();
}
}

public sealed class BetaOrderedDecoratorModule :
IFeatureServiceDecoratorModule<OrderedDecoratorServiceFeature, OrderedDecoratorFeature>
{
public void Decorate(FeatureServiceContext context)
{
context.Services.Decorate<IOrderedDecoratorService, BetaOrderedDecoratorService>();
}
}

public sealed class TestServiceModule : IFeatureServiceModule<TestServiceFeature>
{
public void Register(FeatureServiceContext context)
Expand Down
Loading
Loading