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
8 changes: 4 additions & 4 deletions src/NatsDistributedCache/NatsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public NatsCache(
var options = optionsAccessor.Value;
_bucketName = !string.IsNullOrWhiteSpace(options.BucketName)
? options.BucketName
: throw new NullReferenceException("BucketName must be set");
: throw new ArgumentException(NatsCacheOptions.BucketNameRequiredMessage, nameof(NatsCacheOptions.BucketName));
_keyPrefix = string.IsNullOrEmpty(options.CacheKeyPrefix)
? string.Empty
: options.CacheKeyPrefix.TrimEnd('.');
Expand Down Expand Up @@ -328,9 +328,9 @@ await kvStore.UpdateWithTtlAsync(
}

private async Task RemoveAsync(
string key,
NatsKVDeleteOpts? natsKvDeleteOpts = null,
CancellationToken token = default)
string key,
NatsKVDeleteOpts? natsKvDeleteOpts = null,
CancellationToken token = default)
{
var kvStore = await GetKvStore().ConfigureAwait(false);
await kvStore.DeleteAsync(GetEncodedKey(key), natsKvDeleteOpts, cancellationToken: token)
Expand Down
4 changes: 4 additions & 0 deletions src/NatsDistributedCache/NatsCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ namespace CodeCargo.Nats.DistributedCache
/// </summary>
public class NatsCacheOptions : IOptions<NatsCacheOptions>
{
// Shared by the startup options validator (AddNatsDistributedCache) and the NatsCache
// constructor guard so both validation paths report an identical message.
internal const string BucketNameRequiredMessage = "BucketName must be set";

/// <summary>
/// The NATS bucket name to use for the distributed cache.
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions src/NatsDistributedCache/NatsDistributedCacheExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ public static IServiceCollection AddNatsDistributedCache(
Action<NatsCacheOptions> configureOptions,
object? connectionServiceKey = null)
{
services.AddOptions();
services.Configure(configureOptions);
services.AddOptions<NatsCacheOptions>()
.Configure(configureOptions)
.Validate(o => !string.IsNullOrWhiteSpace(o.BucketName), NatsCacheOptions.BucketNameRequiredMessage)
.ValidateOnStart();
services.AddSingleton<IDistributedCache>(sp =>
{
var optionsAccessor = sp.GetRequiredService<IOptions<NatsCacheOptions>>();
Expand Down
50 changes: 50 additions & 0 deletions test/UnitTests/Extensions/NatsDistributedCacheExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,56 @@ public void AddNatsCache_ReturnsServiceCollection()
Assert.Same(services, result);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void AddNatsCache_MissingBucketName_FailsValidationOnStart(string? bucketName)
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton(_mockNatsConnection.Object);
services.AddNatsDistributedCache(options => options.BucketName = bucketName);
var validator = services.BuildServiceProvider().GetRequiredService<IStartupValidator>();

// Act - ValidateOnStart runs this at host startup; invoke it directly to prove it fails fast
var exception = Assert.Throws<OptionsValidationException>(() => validator.Validate());

// Assert - descriptive failure, not a NullReferenceException
Assert.Contains("BucketName must be set", exception.Message);
}

[Fact]
public void AddNatsCache_ValidBucketName_PassesValidationOnStart()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton(_mockNatsConnection.Object);
services.AddNatsDistributedCache(options => options.BucketName = "cache");
var validator = services.BuildServiceProvider().GetRequiredService<IStartupValidator>();

// Act + Assert - a valid bucket name passes startup validation without throwing
validator.Validate();
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void NatsCache_DirectConstruction_MissingBucketName_ThrowsArgumentException(string? bucketName)
{
// Arrange - direct construction bypasses the DI options validation pipeline
var options = Options.Create(new NatsCacheOptions { BucketName = bucketName });

// Act - the constructor guard fails fast with a clear ArgumentException, not a NullReferenceException
var exception = Assert.Throws<ArgumentException>(
() => new NatsCache(options, _mockNatsConnection.Object));

// Assert - message and paramName point at the offending config value, not the accessor
Assert.Contains("BucketName must be set", exception.Message);
Assert.Equal(nameof(NatsCacheOptions.BucketName), exception.ParamName);
}

[Fact]
public void ToHybridCacheSerializerFactory_CreatesWorkingFactory()
{
Expand Down
Loading