Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,28 @@ Internal classes and methods must be validated by exercising the public API that
- Public entry points provide sufficient coverage of internal code paths.
- The internal implementation exists solely as a helper or utility for public-facing functionality.

## 10. ExcludeFromCodeCoverage Prohibition

**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes:

- Test classes or test methods
- Production code
- Configuration code
- Any other code path

### Rationale

- Excluding code from coverage hides gaps and creates false confidence in test completeness.
- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics.
- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed).

### Alternative Approaches

- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path.
- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage.
- **Configuration-only code**: Move to configuration files or extract into testable methods.
- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary.

---
description: 'Writing Performance Tests in Cuemon'
applyTo: "tuning/**, **/*Benchmark*.cs"
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@ Example: `✨ Add DateSpan.TryParse overload`
4. **Clear messages** — the subject line should be understandable without a body.
5. **Atomic commits** — each commit should be independently buildable and testable.

## Git Operations Safeguards

Agents must never automatically commit code changes or push to remote repositories. Both actions require explicit user approval:

- **Commits**: Always request confirmation from the user before staging and committing code. Present a clear summary of the changes and wait for approval before executing the commit.
- **Remote Operations**: Do not push, pull, fetch, or interact with `origin` or any remote repository without explicit user instruction. These operations modify repository history and can cause data loss if performed unexpectedly.

**Rationale:** Automatic commits can clutter history with incomplete work, temporary debugging code, or unintended changes. Unexpected remote operations risk overwriting or losing commits on shared branches. Always require explicit user approval before performing these actions.

## Agent Workflow

1. Identify the correct project area (`src/`, `test/`, `tuning/`, `tooling/`).
Expand Down
27 changes: 23 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,36 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba

## [10.5.3] - 2026-06-03

This is a patch release focused on fixing request service provider resolution for wrapped providers and improving test coverage for dependency injection scenarios.
This is a patch release focused on fixing request service provider resolution for wrapped providers and significantly expanding test coverage across 15 assemblies to achieve >=95% coverage. The release consolidates ad-hoc coverage tests into integrated, maintainable test suites.

### Fixed

- `ServiceProviderExtensions.GetServiceDescriptors()` method to properly traverse and handle wrapped service providers, including detection and rejection of cyclic provider graphs; the method now correctly resolves descriptors when the provider is wrapped by third-party components such as AspVersioning's InjectApiVersion and throws `NotSupportedException` with descriptive messages when ambiguous or cyclic provider structures are encountered,
- `UseFaultDescriptorExceptionHandler()` middleware to work correctly when request services are wrapped by external decorators or proxies.
- `UseFaultDescriptorExceptionHandler()` middleware to work correctly when request services are wrapped by external decorators or proxies,
- `CollectionExtensions` class in the Cuemon.Extensions.Collections.Generic namespace to add null guard validation in the `Concat<T>` method.

### Added

- `ServiceProviderExtensionsTest` unit test class with comprehensive test coverage for `GetServiceDescriptors()` method, including scenarios for delegating providers, ambiguous multi-provider cases, and cyclic provider graph detection,
- Functional test in `ApplicationBuilderExtensionsTest` to verify fault descriptor exception handling works correctly with wrapped request services.
- Comprehensive unit test coverage for Cuemon.Core: `EradicateTest`, `ExceptionInsightsTest`, `MutableTupleFactoryTest`, `StringFactoryTest`, `WatcherTest`, `GenerateTest`, `StringReplacePairTest`,
- Comprehensive unit test coverage for Cuemon.IO: `StreamFactoryTest`, `StreamOptionsTest`, `TextReaderDecoratorExtensionsTest`,
- Comprehensive unit test coverage for Cuemon.Xml: `XmlDocumentFactoryTest`, `XPathDocumentFactoryTest`, and extension tests for Stream, String, XmlReader, XmlWriter, Linq.String decorators,
- Comprehensive unit test coverage for Cuemon.Diagnostics: `FaultResolverTest`, `TimeMeasureTest`,
- Comprehensive unit test coverage for Cuemon.Runtime.Caching: `CacheEntryTest`, `CacheEntryEventArgsTest`, `CacheInvalidationTest`, `SlimMemoryCacheTest`,
- Comprehensive unit test coverage for Cuemon.Extensions.Collections.Generic: `QueueExtensionsTest` and collection extension test updates,
- Comprehensive unit test coverage for Cuemon.Extensions.Runtime.Caching: `CacheEnumerableExtensionsTest`,
- Comprehensive unit test coverage for Cuemon.Extensions.Text.Json: `StringEnumConverterTest`, `StringFlagsEnumConverterTest`, `DynamicJsonConverterTest`, `JsonNamingPolicyExtensionsTest`, `JsonSerializerOptionsExtensionsTest`, and net48 TFM support,
- Comprehensive unit test coverage for Cuemon.AspNetCore: `MiddlewareTest`, `HttpStatusCodeExceptionTest`, `InternalServerErrorExceptionTest`, `HttpRequestDecoratorExtensionsTest`, `HttpResponseDecoratorExtensionsTest`, `HeaderDictionaryDecoratorExtensionsTest`, `Int32DecoratorExtensionsTest`, `HttpRequestEvidenceTest`, `HttpExceptionDescriptorDecoratorExtensionsTest`, `HttpExceptionDescriptorResponseFormatterTest`, `DynamicCacheBustingTest`,
- Comprehensive unit test coverage for Cuemon.AspNetCore.Mvc: `BreadcrumbTest`, `ResultClassesTest`, `ConfigurableFilterBaseTest`, `MvcFaultDescriptorOptionsTest`, `HttpCacheHeaderOptionsTest`, `HttpEntityTagHeaderFilterTest`, `DisableModelBindingAttributeTest`, `FormatterBaseTest`,
- Comprehensive unit test coverage for Cuemon.AspNetCore.Authentication: `AuthenticatorTest`, `AuthenticationHandlerFeatureTest`, `MemoryNonceTrackerTest`, `NonceTrackerEntryTest`, `DigestHashFactoryTest`, `MiddlewareConstructorTest`,
- Comprehensive unit test coverage for Cuemon.Extensions.AspNetCore: `ApplicationBuilderExtensionsTest`, `ServiceCollectionExtensionsCoverageTest`, Headers and Throttling integration tests, `HttpExceptionDescriptorResponseFormatterExtensionsTest`, `XmlConverterExtensionsTest`,
- Comprehensive unit test coverage for Cuemon.Extensions.AspNetCore.Mvc: `FilterCollectionExtensionsTest`, `MvcBuilderExtensionsTest`,
- Comprehensive unit test coverage for Cuemon.Extensions.AspNetCore.Authentication: `AuthorizationResponseHandlerOptionsTest`,
- Comprehensive unit test coverage for Cuemon.Extensions.Xml: `HierarchyExtensionsTest`, `XElementExtensionsTest`, `XmlConverterExtensionsTest`, `XmlSerializerOptionsExtensionsTest`, `XmlExtensionsTest`,
- Comprehensive unit test coverage for Cuemon.Extensions.Net: `HttpMethodExtensionsTest`, `SlimHttpClientFactoryTest`, `HttpStatusCodeExtensionsTest`, `StringExtensionsTest`, `ByteArrayDecoratorExtensionsTest`.

### Changed

- Enhanced code documentation and test guidelines in `AGENTS.md` with clarifications on testing best practices.

## [10.5.2] - 2026-05-18

Expand Down
5 changes: 5 additions & 0 deletions src/Cuemon.Core/Reflection/AssemblyContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public static class AssemblyContext
/// <exception cref="ArgumentException">
/// <paramref name="setup"/> failed to configure an instance of <see cref="AssemblyContextOptions"/> in a valid state.
/// </exception>
/// <remarks>
/// When <see cref="AssemblyContextOptions.IncludeReferencedAssemblies"/> is <c>true</c>,
/// referenced assemblies may be loaded into the current application domain during traversal.
/// This can change subsequent results returned by <see cref="AppDomain.GetAssemblies"/>.
/// </remarks>
public static IReadOnlyList<Assembly> GetCurrentDomainAssemblies(Action<AssemblyContextOptions> setup = null)
{
Validator.ThrowIfInvalidConfigurator(setup, out var options);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using Cuemon.Extensions.AspNetCore.Text.Json.Formatters;
using Cuemon.Extensions.Text.Json.Formatters;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -20,10 +20,9 @@ public static class MvcCoreBuilderExtensions
/// <param name="setup">The <see cref="JsonFormatterOptions"/> which may be configured.</param>
/// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="builder"/> cannot be null -or-
/// <paramref name="setup"/> cannot be null.
/// <paramref name="builder"/> cannot be null.
/// </exception>
public static IMvcCoreBuilder AddJsonFormatters(this IMvcCoreBuilder builder, Action<JsonFormatterOptions> setup)
public static IMvcCoreBuilder AddJsonFormatters(this IMvcCoreBuilder builder, Action<JsonFormatterOptions> setup = null)
{
Validator.ThrowIfNull(builder);
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, JsonSerializationMvcOptionsSetup>());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Cuemon.Collections.Generic;

namespace Cuemon.Extensions.Collections.Generic
Expand All @@ -17,6 +17,7 @@ public static class CollectionExtensions
/// <returns>An instance of <see cref="PartitionerCollection{T}"/>.</returns>
public static PartitionerCollection<T> ToPartitioner<T>(this ICollection<T> collection, int partitionSize = 128)
{
Validator.ThrowIfNull(collection);
return new PartitionerCollection<T>(collection, partitionSize);
}

Expand All @@ -39,6 +40,8 @@ public static void AddRange<T>(this ICollection<T> collection, params T[] source
/// <param name="source">The sequence of elements that should be added to <paramref name="collection"/>.</param>
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> source)
{
Validator.ThrowIfNull(collection);
Validator.ThrowIfNull(source);
if (collection is List<T> list)
{
list.AddRange(source);
Expand Down
6 changes: 5 additions & 1 deletion src/Cuemon.Extensions.Core/Wrapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
Expand Down Expand Up @@ -96,8 +96,12 @@ protected Wrapper()
/// </summary>
/// <param name="instance">The instance that this wrapper object represents.</param>
/// <param name="memberReference">The member from where <paramref name="instance"/> was referenced.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="instance"/> is null.
/// </exception>
public Wrapper(T instance, MemberInfo memberReference = null)
{
Validator.ThrowIfNull(instance);
_instance = instance;
_instanceType = instance.GetType();
_memberReference = memberReference;
Expand Down
2 changes: 1 addition & 1 deletion src/Cuemon.Net/Http/HttpMethodConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class HttpMethodConverter

private static IDictionary<string, HttpMethods> InitStringToHttpMethodLookupTable()
{
var result = new Dictionary<string, HttpMethods>();
var result = new Dictionary<string, HttpMethods>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in new EnumReadOnlyDictionary<HttpMethods>().Select(pair => pair.Value))
{
result.Add(pair, ParserFactory.FromEnum().Parse<HttpMethods>(pair));
Expand Down
1 change: 1 addition & 0 deletions src/Cuemon.Net/Http/HttpWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class HttpWatcher : Watcher
/// <param name="setup">The <see cref="HttpWatcherOptions" /> which may be configured.</param>
public HttpWatcher(Uri location, Action<HttpWatcherOptions> setup = null) : base(Patterns.ConfigureExchange<HttpWatcherOptions, WatcherOptions>(setup))
{
Validator.ThrowIfNull(location);
var options = Patterns.Configure(setup);
Location = location;
ClientFactory = options.ClientFactory;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Security.Claims;
using Codebelt.Extensions.Xunit;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Xunit;

namespace Cuemon.AspNetCore.Authentication
{
public class AuthenticationHandlerFeatureTest : Test
{
public AuthenticationHandlerFeatureTest(ITestOutputHelper output) : base(output)
{
}

[Fact]
public void Set_ShouldPropagateAuthenticateResultAndUserToHttpFeatures()
{
var context = new DefaultHttpContext();
var principal = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "Agent") }, "scheme"));
var result = AuthenticateResult.Success(new AuthenticationTicket(principal, "scheme"));

AuthenticationHandlerFeature.Set(result, context);

var authenticateFeature = Assert.IsType<AuthenticationHandlerFeature>(context.Features.Get<IAuthenticateResultFeature>());
var httpAuthenticationFeature = Assert.IsType<AuthenticationHandlerFeature>(context.Features.Get<IHttpAuthenticationFeature>());

Assert.Same(authenticateFeature, httpAuthenticationFeature);
Assert.Same(result, authenticateFeature.AuthenticateResult);
Assert.Same(principal, authenticateFeature.User);
}

[Fact]
public void UserSetter_ShouldClearAuthenticateResult()
{
var result = AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), "scheme"));
var sut = new AuthenticationHandlerFeature(result);
var principal = new ClaimsPrincipal(new ClaimsIdentity());

sut.User = principal;

Assert.Same(principal, sut.User);
Assert.Null(sut.AuthenticateResult);
}
}
}
91 changes: 91 additions & 0 deletions test/Cuemon.AspNetCore.Authentication.Tests/AuthenticatorTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Security;
using System.Security.Claims;
using Cuemon.Collections.Generic;
using Codebelt.Extensions.Xunit;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Xunit;

namespace Cuemon.AspNetCore.Authentication
{
public class AuthenticatorTest : Test
{
public AuthenticatorTest(ITestOutputHelper output) : base(output)
{
}

[Fact]
public void Authenticate_ShouldFail_WhenSecureConnectionIsRequired()
{
var context = new DefaultHttpContext();

var result = Authenticator.Authenticate(context, true, (_, authorization) => authorization, PrincipalParserSuccess);

Assert.False(result.Succeeded);
Assert.Equal("An SSL connection is required for the request.", result.Failure.Message);
}

[Fact]
public void Authenticate_ShouldFail_WhenAuthorizationHeaderIsMissing()
{
var context = new DefaultHttpContext();
context.Request.IsHttps = true;

var result = Authenticator.Authenticate(context, true, (_, authorization) => authorization, PrincipalParserSuccess);

Assert.False(result.Succeeded);
Assert.Equal("Authorization header missing.", result.Failure.Message);
}

[Fact]
public void Authenticate_ShouldFail_WhenAuthorizationParserReturnsNull()
{
var context = new DefaultHttpContext();
context.Request.Headers.Append(HeaderNames.Authorization, "ignored");

var result = Authenticator.Authenticate<string>(context, false, (_, _) => null, PrincipalParserSuccess);

Assert.False(result.Succeeded);
Assert.Equal("Invalid credentials.", result.Failure.Message);
}

[Fact]
public void Authenticate_ShouldReturnPrincipal_WhenPrincipalParserSucceeds()
{
var context = new DefaultHttpContext();
context.Request.Headers.Append(HeaderNames.Authorization, "token");

var result = Authenticator.Authenticate(context, false, (_, authorization) => authorization, PrincipalParserSuccess);

Assert.True(result.Succeeded);
Assert.Equal("Agent", result.Result.Identity.Name);
}

[Fact]
public void TryAuthenticate_ShouldCaptureThrownExceptions()
{
var context = new DefaultHttpContext();
context.Request.Headers.Append(HeaderNames.Authorization, "token");

var succeeded = Authenticator.TryAuthenticate(context, false, (_, authorization) => authorization, PrincipalParserThrowing, out var principal);

Assert.False(succeeded);
var failure = Assert.IsType<InvalidOperationException>(principal.Failure);
Assert.Equal("outer", failure.Message);
}

private static bool PrincipalParserSuccess(HttpContext context, string credentials, out ConditionalValue<ClaimsPrincipal> principal)
{
var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "Agent") }, "scheme");
principal = new SuccessfulValue<ClaimsPrincipal>(new ClaimsPrincipal(identity));
return true;
}

private static bool PrincipalParserThrowing(HttpContext context, string credentials, out ConditionalValue<ClaimsPrincipal> principal)
{
principal = new UnsuccessfulValue<ClaimsPrincipal>(new SecurityException("inner"));
throw new InvalidOperationException("outer");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using Cuemon.Security.Cryptography;
using Codebelt.Extensions.Xunit;
using Xunit;

namespace Cuemon.AspNetCore.Authentication.Digest
{
public class DigestHashFactoryTest : Test
{
public DigestHashFactoryTest(ITestOutputHelper output) : base(output)
{
}

[Fact]
public void CreateCrypto_ShouldDefaultToSha256()
{
var sut = DigestHashFactory.CreateCrypto();
var expected = UnkeyedHashFactory.CreateCrypto(UnkeyedCryptoAlgorithm.Sha256);

Assert.Equal(expected.GetType(), sut.GetType());
}

[Theory]
[InlineData(DigestCryptoAlgorithm.Md5, UnkeyedCryptoAlgorithm.Md5)]
[InlineData(DigestCryptoAlgorithm.Md5Session, UnkeyedCryptoAlgorithm.Md5)]
[InlineData(DigestCryptoAlgorithm.Sha256, UnkeyedCryptoAlgorithm.Sha256)]
[InlineData(DigestCryptoAlgorithm.Sha256Session, UnkeyedCryptoAlgorithm.Sha256)]
[InlineData(DigestCryptoAlgorithm.Sha512Slash256, UnkeyedCryptoAlgorithm.Sha512Slash256)]
[InlineData(DigestCryptoAlgorithm.Sha512Slash256Session, UnkeyedCryptoAlgorithm.Sha512Slash256)]
public void CreateCrypto_ShouldMapDigestAlgorithmsToExpectedHashImplementations(DigestCryptoAlgorithm algorithm, UnkeyedCryptoAlgorithm expectedAlgorithm)
{
var sut = DigestHashFactory.CreateCrypto(algorithm);
var expected = UnkeyedHashFactory.CreateCrypto(expectedAlgorithm);

Assert.Equal(expected.GetType(), sut.GetType());
}

[Fact]
public void CreateCrypto_ShouldThrowArgumentOutOfRangeException_WhenAlgorithmIsUnsupported()
{
Assert.Throws<ArgumentOutOfRangeException>(() => DigestHashFactory.CreateCrypto((DigestCryptoAlgorithm)42));
}
}
}
Loading
Loading