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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `FileSize` utility with method to display bytes in a human-readable way
- `CultureInfo` extension method to get the language code of a culture
- `IEnumerable` extension methods
- `IConfiguration` extension methods
- `IServiceCollection` extension methods
- `IFormFile` extension method (AspNetCore package only)
- `DbSet` extension methods for ISortableEntity interface (EntityFrameworkCore package only)
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
namespace Neolution.Utilities.UnitTests.Extensions;

using Microsoft.Extensions.Configuration;

/// <summary>
/// Unit tests for the <see cref="IConfigurationExtensions"/> class.
/// </summary>
public class IConfigurationExtensionsTests
{
/// <summary>
/// Test that given the null configuration when get options called then throws argument null exception.
/// </summary>
[Fact]
public void GivenNullConfiguration_WhenGetOptionsCalled_ThenThrowsArgumentNullException()
{
// Arrange
IConfiguration? configuration = null;

// Act
var act = () => configuration!.GetOptions<SampleOptions>();

// Assert
var ex = Should.Throw<ArgumentNullException>(act);
ex.ParamName.ShouldBe("config");
}

/// <summary>
/// Test that given the missing section when get options called then throws invalid operation exception.
/// </summary>
[Fact]
public void GivenMissingSection_WhenGetOptionsCalled_ThenThrowsInvalidOperationException()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection([]) // No section for SampleOptions
.Build();

// Act
var act = () => configuration.GetOptions<SampleOptions>();

// Assert
var ex = Should.Throw<InvalidOperationException>(act);
ex.Message.ShouldBe("Could not find configuration section 'SampleOptions'");
}

/// <summary>
/// Test that given the valid section when get options called then binds and returns options.
/// </summary>
[Fact]
public void GivenValidSection_WhenGetOptionsCalled_ThenBindsAndReturnsOptions()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SampleOptions:Name"] = "TestName",
["SampleOptions:Level"] = "42",
})
.Build();

// Act
var options = configuration.GetOptions<SampleOptions>();

// Assert
options.Name.ShouldBe("TestName");
options.Level.ShouldBe(42);
}

/// <summary>
/// Test that given the section with no matching properties when get options called then returns instance with defaults.
/// </summary>
[Fact]
public void GivenSectionWithNoMatchingProperties_WhenGetOptionsCalled_ThenReturnsInstanceWithDefaults()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SampleOptions:Irrelevant"] = "ignored",
})
.Build();

// Act
var options = configuration.GetOptions<SampleOptions>();

// Assert
options.Name.ShouldBeNull();
options.Level.ShouldBe(0);
}

/// <summary>
/// Test that given the section with invalid numeric value when get options called then throws invalid operation exception.
/// </summary>
[Fact]
public void GivenSectionWithInvalidNumericValue_WhenGetOptionsCalled_ThenThrowsInvalidOperationException()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SampleOptions:Name"] = "Something",
["SampleOptions:Level"] = "not-an-int",
})
.Build();

// Act
var act = () => configuration.GetOptions<SampleOptions>();

// Assert
var ex = Should.Throw<InvalidOperationException>(act);
ex.Message.ShouldBe("Failed to convert configuration value at 'SampleOptions:Level' to type 'System.Int32'.");
}

/// <summary>
/// Test that given the valid section with different casing when get options called then binds successfully.
/// </summary>
[Fact]
public void GivenValidSectionWithDifferentCasing_WhenGetOptionsCalled_ThenBindsSuccessfully()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
// lower-case section name to assert case-insensitive lookup
["sampleoptions:Name"] = "CaseTest",
["sampleoptions:Level"] = "7",
})
.Build();

// Act
var options = configuration.GetOptions<SampleOptions>();

// Assert
options.Name.ShouldBe("CaseTest");
options.Level.ShouldBe(7);
}

/// <summary>
/// Test that given the null configuration when get section called then throws argument null exception.
/// </summary>
[Fact]
public void GivenNullConfiguration_WhenGetSectionCalled_ThenThrowsArgumentNullException()
{
// Arrange
IConfiguration? configuration = null;

// Act
var act = () => configuration!.GetSection<SampleOptions>();

// Assert
var ex = Should.Throw<ArgumentNullException>(act);
ex.ParamName.ShouldBe("config");
}

/// <summary>
/// Test that given the existing section when get section called then returns section.
/// </summary>
[Fact]
public void GivenExistingSection_WhenGetSectionCalled_ThenReturnsSection()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SampleOptions:Name"] = "X",
})
.Build();

// Act
var section = configuration.GetSection<SampleOptions>();

// Assert
section.Key.ShouldBe("SampleOptions");
section.Exists().ShouldBeTrue();
}

/// <summary>
/// Test that given the missing section when get section called then returns non existing section.
/// </summary>
[Fact]
public void GivenMissingSection_WhenGetSectionCalled_ThenReturnsNonExistingSection()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection([])
.Build();

// Act
var section = configuration.GetSection<SampleOptions>();

// Assert
section.Key.ShouldBe("SampleOptions");
section.Exists().ShouldBeFalse();
}

/// <summary>
/// The sample options class used for testing.
/// </summary>
public class SampleOptions
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public string? Name { get; set; }

/// <summary>
/// Gets or sets the level.
/// </summary>
public int Level { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
namespace Neolution.Utilities.UnitTests.Extensions;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

/// <summary>
/// Unit tests for the <see cref="IServiceCollectionExtensions"/> class.
/// </summary>
public class IServiceCollectionExtensionsTests
{
/// <summary>
/// Test that given the null service collection when add options called then throws argument null exception.
/// </summary>
[Fact]
public void GivenNullServiceCollection_WhenAddOptionsCalled_ThenThrowsArgumentNullException()
{
// Arrange
ServiceCollection? serviceCollection = null;
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { ["SampleOptions:Name"] = "X" })
.Build();

// Act
var act = () => serviceCollection!.AddOptions<SampleOptions>(configuration);

// Assert
var ex = Should.Throw<ArgumentNullException>(act);
ex.ParamName.ShouldBe("serviceCollection");
}

/// <summary>
/// Test that given the null configuration when add options called then throws argument null exception.
/// </summary>
[Fact]
public void GivenNullConfiguration_WhenAddOptionsCalled_ThenThrowsArgumentNullException()
{
// Arrange
var serviceCollection = new ServiceCollection();
IConfiguration? configuration = null;

// Act
var act = () => serviceCollection.AddOptions<SampleOptions>(configuration!);

// Assert
var ex = Should.Throw<ArgumentNullException>(act);
ex.ParamName.ShouldBe("configuration");
}

/// <summary>
/// Test that given the configuration with options section when add options called then options are configured.
/// </summary>
[Fact]
public void GivenConfigurationWithOptionsSection_WhenAddOptionsCalled_ThenOptionsAreConfigured()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SampleOptions:Name"] = "Configured",
["SampleOptions:Level"] = "7",
})
.Build();
var serviceCollection = new ServiceCollection();

// Act
serviceCollection.AddOptions<SampleOptions>(configuration);
var provider = serviceCollection.BuildServiceProvider();

// Assert
var options = provider.GetRequiredService<IOptions<SampleOptions>>().Value;
options.Name.ShouldBe("Configured");
options.Level.ShouldBe(7);
}

/// <summary>
/// Test that given the configuration without options section when add options called then options use defaults.
/// </summary>
[Fact]
public void GivenConfigurationWithoutOptionsSection_WhenAddOptionsCalled_ThenOptionsUseDefaults()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection([])
.Build();
var serviceCollection = new ServiceCollection();

// Act
serviceCollection.AddOptions<SampleOptions>(configuration);
var provider = serviceCollection.BuildServiceProvider();

// Assert
var options = provider.GetRequiredService<IOptions<SampleOptions>>().Value;
options.Name.ShouldBeNull();
options.Level.ShouldBe(0);
}

/// <summary>
/// Test that given the service collection when add options called then same instance is returned for fluent chaining.
/// </summary>
[Fact]
public void GivenServiceCollection_WhenAddOptionsCalled_ThenReturnsSameInstance()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection([])
.Build();
var services = new ServiceCollection();

// Act
var returned = services.AddOptions<SampleOptions>(configuration);

// Assert
returned.ShouldBeSameAs(services);
}

/// <summary>
/// Test that given multiple registrations with different configurations when add options called twice then last registration wins.
/// </summary>
[Fact]
public void GivenMultipleRegistrations_WhenAddOptionsCalledTwice_ThenLastRegistrationWins()
{
// Arrange
var configuration1 = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SampleOptions:Name"] = "First",
["SampleOptions:Level"] = "1",
})
.Build();
var configuration2 = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["SampleOptions:Name"] = "Second",
["SampleOptions:Level"] = "2",
})
.Build();

var services = new ServiceCollection();

// Act
services.AddOptions<SampleOptions>(configuration1);
services.AddOptions<SampleOptions>(configuration2);
var provider = services.BuildServiceProvider();

// Assert
var value = provider.GetRequiredService<IOptions<SampleOptions>>().Value;
value.Name.ShouldBe("Second");
value.Level.ShouldBe(2);
}

/// <summary>
/// The sample options class used for testing.
/// </summary>
public class SampleOptions
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public string? Name { get; set; }

/// <summary>
/// Gets or sets the level.
/// </summary>
public int Level { get; set; }
}
}
Loading
Loading