diff --git a/AGENTS.md b/AGENTS.md
index 65e0638..fe02504 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -77,7 +77,7 @@ Both repositories use raw SQL via Dapper. They differ in naming conventions:
Single test project: `tests/AspNetCore.DataProtection.CustomStorage.Tests` (xUnit v3, FluentAssertions, NSubstitute, Bogus).
- `StorageWrapperTests`, `DeletableStorageWrapperTests` and `DapperDataProtectionExtensionsTests` are pure unit tests. `DataProtectionStorageExtensionsTests` runs the real DI container and `XmlKeyManager` against in-memory storages.
-- `SQLServer/` and `PostgreSQL/` tests use an `IClassFixture` (`*ContainerFixture`) that starts a Testcontainers database, calls `InitializeDb()` with the default config, and exposes `ResetDatabaseAsync()` (Respawn) to clear data between tests.
+- `SQLServer/` and `PostgreSQL/` tests share one xUnit v3 assembly fixture per provider (`*ContainerFixture`, `[assembly: AssemblyFixture]`) that starts a Testcontainers database, calls `InitializeDb()` with the default config, and exposes `ResetDatabaseAsync()` (Respawn) to clear the default table between tests. Respawn captures its table list at fixture start, so tests that create their own tables are unaffected; any test that does not reset must use its own table.
- `*EndToEndTests` wire each provider through the public extension methods and a real DI container (`AddDataProtection().PersistKeysWithDapperIn...()`, `UseDapperDataProtection()`), then protect/unprotect, including across two service providers sharing one table, and delete keys through `XmlKeyManager.DeleteKeys`. Each test uses its own table, so it does not depend on Respawn.
### CI/CD (`.github/workflows`)
diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionConfigValidator.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionConfigValidator.cs
index fbdde21..28a67d6 100644
--- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionConfigValidator.cs
+++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionConfigValidator.cs
@@ -15,9 +15,11 @@ internal sealed partial class DapperDataProtectionConfigValidator : IValidateOpt
internal const int MaxSchemaNameLength = 128;
///
- /// Maximum table name length, which leaves room for the derived index name IX_{TableName}_FriendlyName.
+ /// Maximum table name length, which leaves room for the derived index name IX_{TableName}_FriendlyName,
+ /// the longest name derived from the table name.
///
- internal const int MaxTableNameLength = MaxSchemaNameLength - 16;
+ internal static readonly int MaxTableNameLength =
+ MaxSchemaNameLength - SQLServerDataProtectionRepository.GetIndexName(string.Empty).Length;
///
public ValidateOptionsResult Validate(string? name, DapperDataProtectionConfig options)
diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs
index 1570694..b1e8417 100644
--- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs
+++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs
@@ -115,7 +115,7 @@ private static List GetIds(IReadOnlyCollection keys)
///
public void InitializeDb()
{
- var indexName = $"IX_{_config.TableName}_{nameof(DataProtectionKeyEntity.FriendlyName)}";
+ var indexName = GetIndexName(_config.TableName);
using var connection = _connectionFactory.CreateConnection();
connection.Open();
using var transaction = connection.BeginTransaction();
@@ -171,5 +171,7 @@ DECLARE @message nvarchar(2048) = N'Cannot create unique index ' + QUOTENAME(@In
transaction.Commit();
}
+ internal static string GetIndexName(string tableName) => $"IX_{tableName}_{nameof(DataProtectionKeyEntity.FriendlyName)}";
+
private static string QuoteIdentifier(string name) => $"[{name.Replace("]", "]]")}]";
}
\ No newline at end of file
diff --git a/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs b/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs
index 53f1a6e..66f6ea5 100644
--- a/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs
+++ b/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs
@@ -22,16 +22,9 @@ internal class DeletableStorageWrapper : StorageWrapper, IDe
///
///
///
- /// does not implement
public DeletableStorageWrapper(IServiceProvider services, ILoggerFactory loggerFactory)
: base(services, loggerFactory)
{
- if (!typeof(TStorage).IsAssignableTo(typeof(IDeletableDataProtectionStorage)))
- {
- throw new ArgumentException(
- $"'{typeof(TStorage).Name}' does not implement '{nameof(IDeletableDataProtectionStorage)}'.",
- nameof(TStorage));
- }
}
///
@@ -46,15 +39,7 @@ public bool DeleteElements(Action> choose
using var scope = _services.CreateScope();
var storage = (IDeletableDataProtectionStorage)scope.ServiceProvider.GetRequiredService();
- var deletableElements = new List();
- foreach (var key in storage.GetAll())
- {
- var element = TryParseKey(key);
- if (element is not null)
- {
- deletableElements.Add(new DeletableElement(key, element));
- }
- }
+ var deletableElements = ParseKeys(storage).Select(k => new DeletableElement(k.Key, k.Element)).ToList();
chooseElements(deletableElements);
diff --git a/src/AspNetCore.DataProtection.CustomStorage/StorageWrapper.cs b/src/AspNetCore.DataProtection.CustomStorage/StorageWrapper.cs
index 74cad67..80e5b1c 100644
--- a/src/AspNetCore.DataProtection.CustomStorage/StorageWrapper.cs
+++ b/src/AspNetCore.DataProtection.CustomStorage/StorageWrapper.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.Repositories;
@@ -41,25 +42,27 @@ public IReadOnlyCollection GetAllElements()
{
using var scope = _services.CreateScope();
var storage = scope.ServiceProvider.GetRequiredService();
- var elements = ImmutableArray.CreateBuilder();
+ return ParseKeys(storage).Select(k => k.Element).ToImmutableArray();
+ }
+
+ ///
+ /// read every stored key and parse its XML
+ ///
+ /// the storage to read
+ /// each stored key with its parsed element; keys with malformed XML are logged and skipped
+ protected IEnumerable<(DataProtectionKey Key, XElement Element)> ParseKeys(IDataProtectionStorage storage)
+ {
foreach (var key in storage.GetAll())
{
var element = TryParseKey(key);
if (element is not null)
{
- elements.Add(element);
+ yield return (key, element);
}
}
-
- return elements.ToImmutable();
}
- ///
- /// parse the XML of a stored key
- ///
- /// the stored key
- /// the parsed element, or null when the XML is malformed (the key is logged and skipped)
- protected XElement? TryParseKey(DataProtectionKey key)
+ private XElement? TryParseKey(DataProtectionKey key)
{
_logger.ReadingXmlFromKey(key.FriendlyName);
try
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj b/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj
index ba0b3ea..abe1a57 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj
@@ -11,13 +11,6 @@
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/DapperDataProtectionExtensionsTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/DapperDataProtectionExtensionsTests.cs
index 9c3b859..dbcc646 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/DapperDataProtectionExtensionsTests.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/DapperDataProtectionExtensionsTests.cs
@@ -30,35 +30,21 @@ public DapperDataProtectionExtensionsTests()
_services.GetService(typeof(IDbDataProtectionStorage)).Returns(_provider);
_services.GetService(typeof(ILoggerFactory)).Returns(loggerFactory);
}
- [Fact]
- public void UseDapperDataProtection_InitializeTable_ExecInitializeDb()
- {
- var config = new DapperDataProtectionConfig()
- {
- SchemaName = "",
- TableName = "",
- InitializeTable = true
- };
- _options.Value.Returns(config);
- _services.UseDapperDataProtection();
-
- _provider.Received(1).InitializeDb();
- }
- [Fact]
- public void UseDapperDataProtection_InitializeTableFalse_NotInitializeDb()
+ [Theory]
+ [InlineData(true, 1)]
+ [InlineData(false, 0)]
+ public void UseDapperDataProtection_InitializeTable_InitializesDbOnlyWhenEnabled(bool initializeTable, int expectedCalls)
{
- var config = new DapperDataProtectionConfig()
+ _options.Value.Returns(new DapperDataProtectionConfig
{
SchemaName = "",
TableName = "",
- InitializeTable = false
- };
- _options.Value.Returns(config);
+ InitializeTable = initializeTable
+ });
_services.UseDapperDataProtection();
- _provider.DidNotReceiveWithAnyArgs().InitializeDb();
+ _provider.Received(expectedCalls).InitializeDb();
}
-
}
\ No newline at end of file
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs
index f15b0dc..5173704 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs
@@ -13,7 +13,7 @@ namespace AspNetCore.DataProtection.CustomStorage.Tests;
public class DeletableStorageWrapperTests
{
- private const string KeyMaterial = "c2VjcmV0LWtleS1tYXRlcmlhbA==";
+ private const string KeyMaterial = TestConstants.KeyMaterial;
private readonly IServiceProvider _services;
private readonly IDeletableDataProtectionStorage _storage;
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs
index 4289d84..33b313c 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs
@@ -9,6 +9,9 @@
using Testcontainers.PostgreSql;
using Xunit;
+// One container for the whole assembly: tests that do not use the default table work on their own tables.
+[assembly: AssemblyFixture(typeof(AspNetCore.DataProtection.CustomStorage.Tests.PostgreSQL.PostgreSqlContainerFixture))]
+
namespace AspNetCore.DataProtection.CustomStorage.Tests.PostgreSQL;
public class PostgreSqlContainerFixture : IAsyncLifetime
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs
index 8c58431..d894a1a 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs
@@ -12,7 +12,7 @@
using Xunit;
namespace AspNetCore.DataProtection.CustomStorage.Tests.PostgreSQL;
-public class PostgreSqlDataProtectionRepositoryTests: IClassFixture
+public class PostgreSqlDataProtectionRepositoryTests
{
private readonly PostgreSqlContainerFixture _fixture;
private readonly PostgreSQLDataProtectionRepository _sut;
@@ -123,24 +123,19 @@ public async Task GetAllAsync_MatchNamesWithUnderscoresDisabled_MapsAllColumns()
public void InitializeDb_CustomSchemaAndTable_PersistsKeysInConfiguredTable()
{
const string schemaName = "custom_keys";
- var tableName = $"keys_{Guid.NewGuid().ToString("N")[..12]}";
+ var tableName = NewTableName();
using (var connection = _fixture.NpgsqlDataSource.OpenConnection())
{
connection.Execute($"CREATE SCHEMA IF NOT EXISTS {schemaName};");
}
- var options = Substitute.For>();
- options.Value.Returns(new DapperDataProtectionConfig { SchemaName = schemaName, TableName = tableName });
- var sut = new PostgreSQLDataProtectionRepository(_fixture.NpgsqlDataSource, options);
+ var sut = CreateSut(schemaName, tableName);
var key = new DataProtectionKey { FriendlyName = "CustomSchemaKey", Xml = "Xml" };
sut.InitializeDb();
sut.Insert(key);
sut.GetAll().Should().ContainSingle(x => x.FriendlyName == key.FriendlyName && x.Xml == key.Xml);
- using (var connection = _fixture.NpgsqlDataSource.OpenConnection())
- {
- connection.ExecuteScalar($"SELECT COUNT(*) FROM {schemaName}.{tableName};").Should().Be(1);
- }
+ CountRows(schemaName, tableName).Should().Be(1);
_sut.GetAll().Should().NotContain(x => x.FriendlyName == key.FriendlyName);
}
@@ -155,16 +150,10 @@ public async Task InitializeDb_ConcurrentCallsOnNewTable_AllSucceed()
// Several rounds, because a single round does not always hit the race.
for (var round = 0; round < 5; round++)
{
- var tableName = $"keys_{Guid.NewGuid().ToString("N")[..12]}";
- var options = Substitute.For>();
- options.Value.Returns(new DapperDataProtectionConfig
- {
- SchemaName = Constants.DefaultSchema,
- TableName = tableName
- });
+ var tableName = NewTableName();
// One repository per call, as on separate nodes starting at the same time.
var suts = Enumerable.Range(0, callCount)
- .Select(_ => new PostgreSQLDataProtectionRepository(_fixture.NpgsqlDataSource, options))
+ .Select(_ => CreateSut(Constants.DefaultSchema, tableName))
.ToList();
using var barrier = new Barrier(callCount);
@@ -187,7 +176,7 @@ public async Task InitializeDb_ConcurrentCallsOnNewTable_AllSucceed()
public void InitializeDb_SchemaMissing_CreatesSchemaAndTable()
{
var schemaName = $"schema_{Guid.NewGuid().ToString("N")[..12]}";
- var tableName = $"keys_{Guid.NewGuid().ToString("N")[..12]}";
+ var tableName = NewTableName();
var sut = CreateSut(schemaName, tableName);
sut.InitializeDb();
@@ -202,7 +191,7 @@ public void InitializeDb_ReservedWordSchemaName_PersistsKeys()
{
// A reserved word is accepted unquoted after a schema qualifier, but not as the schema name itself.
const string schemaName = "order";
- var tableName = $"keys_{Guid.NewGuid().ToString("N")[..12]}";
+ var tableName = NewTableName();
var sut = CreateSut(schemaName, tableName);
var key = new DataProtectionKey { FriendlyName = "ReservedWordKey", Xml = "Xml" };
@@ -264,14 +253,8 @@ public async Task Delete_KeyAlreadyDeleted_Succeeds()
[Fact]
public void Delete_LaterDeletionFails_RollsBackEarlierDeletions()
{
- var tableName = $"keys_{Guid.NewGuid().ToString("N")[..12]}";
- var options = Substitute.For>();
- options.Value.Returns(new DapperDataProtectionConfig
- {
- SchemaName = Constants.DefaultSchema,
- TableName = tableName
- });
- var sut = new PostgreSQLDataProtectionRepository(_fixture.NpgsqlDataSource, options);
+ var tableName = NewTableName();
+ var sut = CreateSut(Constants.DefaultSchema, tableName);
sut.InitializeDb();
sut.Insert(new DataProtectionKey { FriendlyName = "DeleteFirst", Xml = "xml1" });
sut.Insert(new DataProtectionKey { FriendlyName = "DeleteFails", Xml = "xml2" });
@@ -295,6 +278,8 @@ CREATE TRIGGER tr_{tableName}_fail_delete BEFORE DELETE ON {Constants.DefaultSch
sut.GetAll().Select(x => x.FriendlyName).Should().BeEquivalentTo("DeleteFirst", "DeleteFails");
}
+ private static string NewTableName() => $"keys_{Guid.NewGuid().ToString("N")[..12]}";
+
private PostgreSQLDataProtectionRepository CreateSut(string schemaName, string tableName)
{
var options = Substitute.For>();
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlEndToEndTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlEndToEndTests.cs
index 8c9568f..cbdb3b6 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlEndToEndTests.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlEndToEndTests.cs
@@ -18,7 +18,7 @@ namespace AspNetCore.DataProtection.CustomStorage.Tests.PostgreSQL;
///
/// End-to-end tests that wire the PostgreSQL provider through the public extension methods and real DI.
///
-public class PostgreSqlEndToEndTests : IClassFixture
+public class PostgreSqlEndToEndTests
{
private const string ApplicationName = "AspNetCore.DataProtection.CustomStorage.Tests";
private const string Purpose = "end-to-end";
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs
index 949a386..769038b 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs
@@ -10,6 +10,9 @@
using Xunit;
using Constants = AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer.Constants;
+// One container for the whole assembly: tests that do not use the default table work on their own tables.
+[assembly: AssemblyFixture(typeof(AspNetCore.DataProtection.CustomStorage.Tests.SQLServer.SqlServerContainerFixture))]
+
namespace AspNetCore.DataProtection.CustomStorage.Tests.SQLServer;
public class SqlServerContainerFixture : IAsyncLifetime
{
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs
index 731f152..56d6e9d 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs
@@ -13,7 +13,7 @@
using Constants = AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer.Constants;
namespace AspNetCore.DataProtection.CustomStorage.Tests.SQLServer;
-public class SqlServerDataProtectionRepositoryTests : IClassFixture
+public class SqlServerDataProtectionRepositoryTests
{
private readonly SqlServerContainerFixture _fixture;
private readonly SQLServerDataProtectionRepository _sut;
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerEndToEndTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerEndToEndTests.cs
index eea116e..b5c3bd2 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerEndToEndTests.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerEndToEndTests.cs
@@ -18,7 +18,7 @@ namespace AspNetCore.DataProtection.CustomStorage.Tests.SQLServer;
///
/// End-to-end tests that wire the SQL Server provider through the public extension methods and real DI.
///
-public class SqlServerEndToEndTests : IClassFixture
+public class SqlServerEndToEndTests
{
private const string ApplicationName = "AspNetCore.DataProtection.CustomStorage.Tests";
private const string Purpose = "end-to-end";
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs
index 357201b..ca1ebac 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs
@@ -11,72 +11,65 @@ namespace AspNetCore.DataProtection.CustomStorage.Tests;
public class StorageWrapperTests
{
- private readonly StorageWrapper _sut;
- private readonly IServiceProvider _services;
- private readonly IServiceScopeFactory _scopeFactory;
- private readonly IServiceScope _scope;
- private readonly ILoggerFactory _loggerFactory;
- private readonly ILogger _logger = Substitute.For();
+ private readonly StorageWrapper _sut;
+ private readonly IServiceProvider _services;
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly IServiceScope _scope;
+ private readonly CapturingLoggerFactory _loggerFactory = new();
private readonly IDataProtectionStorage _storage;
- public StorageWrapperTests()
- {
- _services = Substitute.For();
- _loggerFactory = Substitute.For();
- _storage = Substitute.For();
-
- // The storage is resolvable only from the scoped provider, not from the root one.
- var scopedServices = Substitute.For();
- scopedServices.GetService(typeof(IDataProtectionStorage)).Returns(_storage);
-
- _scope = Substitute.For();
- _scope.ServiceProvider.Returns(scopedServices);
-
- _scopeFactory = Substitute.For();
- _scopeFactory.CreateScope().Returns(_scope);
-
- _services.GetService(typeof(IServiceScopeFactory)).Returns(_scopeFactory);
-
- _loggerFactory.CreateLogger(Arg.Any()).Returns(_logger);
- _sut = new StorageWrapper(_services, _loggerFactory);
- }
- [Fact]
- public void Constructor_AllArgumentsNull_ThrowArgumentNullException()
- {
- Assert.Throws(() => new StorageWrapper(null!, null!));
- }
- [Fact]
- public void Constructor_LoggerFactoryIsNull_ThrowArgumentNullException()
- {
- // Act
- Action act = () =>
- {
- var _ = new StorageWrapper(_services, null!);
- };
-
- // Assert
- act.Should().Throw().WithMessage("*loggerFactory*");
- }
-
- [Fact]
- public void Constructor_ServiceProviderIsNull_ThrowArgumentNullException()
- {
- // Act
- var act = () =>
- {
- var _ = new StorageWrapper(null!, _loggerFactory);
- };
-
- // Assert
- act.Should().Throw().WithMessage("*services*");
- }
+ public StorageWrapperTests()
+ {
+ _services = Substitute.For();
+ _storage = Substitute.For();
+
+ // The storage is resolvable only from the scoped provider, not from the root one.
+ var scopedServices = Substitute.For();
+ scopedServices.GetService(typeof(IDataProtectionStorage)).Returns(_storage);
+
+ _scope = Substitute.For();
+ _scope.ServiceProvider.Returns(scopedServices);
+
+ _scopeFactory = Substitute.For();
+ _scopeFactory.CreateScope().Returns(_scope);
+
+ _services.GetService(typeof(IServiceScopeFactory)).Returns(_scopeFactory);
+
+ _sut = new StorageWrapper(_services, _loggerFactory);
+ }
+
+ [Fact]
+ public void Constructor_LoggerFactoryIsNull_ThrowArgumentNullException()
+ {
+ // Act
+ Action act = () =>
+ {
+ var _ = new StorageWrapper(_services, null!);
+ };
+
+ // Assert
+ act.Should().Throw().WithMessage("*loggerFactory*");
+ }
+
+ [Fact]
+ public void Constructor_ServiceProviderIsNull_ThrowArgumentNullException()
+ {
+ // Act
+ var act = () =>
+ {
+ var _ = new StorageWrapper(null!, _loggerFactory);
+ };
+
+ // Assert
+ act.Should().Throw().WithMessage("*services*");
+ }
[Fact]
public void StoreElement_ValidKey_CallsStorageSave()
{
_sut.StoreElement(TestConstants.XElement, TestConstants.FriendlyName);
var key = new DataProtectionKey
{
- FriendlyName = TestConstants.FriendlyName.Trim(),
+ FriendlyName = TestConstants.FriendlyName,
Xml = TestConstants.XElement.ToString(SaveOptions.DisableFormatting)
};
_storage.Received(1).Insert(Arg.Is(key));
@@ -95,62 +88,43 @@ public void StoreElement_StorageFails_ThrowsKeyInsertException()
_sut.Invoking(x => x.StoreElement(TestConstants.XElement,TestConstants.FriendlyName))
.Should().Throw();
}
- [Fact]
- public void StoreElement_ShouldCallInsertOnStorage()
- {
- // Arrange
- var element = new XElement("TestElement");
-
- // Act
- _sut.StoreElement(element, "TestName");
-
- // Assert
- _storage.Received(1).Insert(Arg.Is(key =>
- key != null && key.FriendlyName == "TestName" && key.Xml == element.ToString(SaveOptions.DisableFormatting)));
- }
- private const string KeyMaterial = "c2VjcmV0LWtleS1tYXRlcmlhbA==";
+ private const string KeyMaterial = TestConstants.KeyMaterial;
private const string KeyFriendlyName = "key-6f1c2b9e";
private static readonly XElement KeyElement = new("key", new XElement("masterKey", KeyMaterial));
[Fact]
public void GetAllElements_KeyRead_DoesNotLogKeyXml()
{
- var loggerFactory = new CapturingLoggerFactory();
- var sut = new StorageWrapper(_services, loggerFactory);
_storage.GetAll().Returns([new DataProtectionKey { FriendlyName = KeyFriendlyName, Xml = KeyElement.ToString() }]);
- sut.GetAllElements();
+ _sut.GetAllElements();
- loggerFactory.Entries.Should().Contain(e => e.Contains(KeyFriendlyName));
- loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
+ _loggerFactory.Entries.Should().Contain(e => e.Contains(KeyFriendlyName));
+ _loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
}
[Fact]
public void StoreElement_KeySaved_DoesNotLogKeyXml()
{
- var loggerFactory = new CapturingLoggerFactory();
- var sut = new StorageWrapper(_services, loggerFactory);
- sut.StoreElement(KeyElement, KeyFriendlyName);
+ _sut.StoreElement(KeyElement, KeyFriendlyName);
- loggerFactory.Entries.Should().Contain(e => e.Contains(KeyFriendlyName));
- loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
+ _loggerFactory.Entries.Should().Contain(e => e.Contains(KeyFriendlyName));
+ _loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
}
[Fact]
public void StoreElement_StorageFails_LogsErrorWithStorageExceptionAndWithoutKeyXml()
{
- var loggerFactory = new CapturingLoggerFactory();
- var sut = new StorageWrapper(_services, loggerFactory);
var storageException = new InvalidOperationException("insert failed");
_storage.WhenForAnyArgs(x => x.Insert(null!)).Throw(storageException);
- sut.Invoking(x => x.StoreElement(KeyElement, KeyFriendlyName)).Should().Throw();
+ _sut.Invoking(x => x.StoreElement(KeyElement, KeyFriendlyName)).Should().Throw();
- loggerFactory.Entries.Should().ContainSingle(e => e.Level == LogLevel.Error)
+ _loggerFactory.Entries.Should().ContainSingle(e => e.Level == LogLevel.Error)
.Which.Exception.Should().BeSameAs(storageException);
- loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
+ _loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
}
[Fact]
@@ -206,15 +180,13 @@ public void GetAllElements_RowWithMalformedXml_SkipsRowAndReturnsValidKeys()
[Fact]
public void GetAllElements_RowWithMalformedXml_LogsErrorNamingRowWithoutKeyXml()
{
- var loggerFactory = new CapturingLoggerFactory();
- var sut = new StorageWrapper(_services, loggerFactory);
_storage.GetAll().Returns([new DataProtectionKey { FriendlyName = MalformedFriendlyName, Xml = MalformedKeyXml }]);
- sut.GetAllElements().Should().BeEmpty();
+ _sut.GetAllElements().Should().BeEmpty();
- loggerFactory.Entries.Should().ContainSingle(e => e.Level == LogLevel.Error)
+ _loggerFactory.Entries.Should().ContainSingle(e => e.Level == LogLevel.Error)
.Which.Should().Match(e => e.EventId.Name == "SkippingMalformedKey" && e.Contains(MalformedFriendlyName));
- loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
+ _loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial));
}
[Fact]
diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/TestConstants.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/TestConstants.cs
index 6260cde..0331773 100644
--- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/TestConstants.cs
+++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/TestConstants.cs
@@ -5,14 +5,7 @@ namespace AspNetCore.DataProtection.CustomStorage.Tests;
public static class TestConstants
{
public static readonly string FriendlyName = "friendlyName";
- public static readonly string Xml = """
-
- Tove
- Jani
- Reminder
- Don't forget me this weekend!
-
- """;
+ public const string KeyMaterial = "c2VjcmV0LWtleS1tYXRlcmlhbA==";
public static readonly XElement XElement = new("root",
new XElement("child",
new XAttribute("id", "1"),
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index 9ef5c62..45cf016 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -7,7 +7,7 @@
- net10.0;
+ net10.0
latest
disable
enable