diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed2ccb823..cb0f16883 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: docker-tag: [ ci, lts, previous-lts ] - test: [ Streams, PersistentSubscriptions, Operations, Projections, Security, Misc ] + test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, Security, Misc ] name: Test CE (${{ matrix.docker-tag }}) with: docker-tag: ${{ matrix.docker-tag }} diff --git a/.github/workflows/dispatch-ce.yml b/.github/workflows/dispatch-ce.yml index 2611dfbd2..077384252 100644 --- a/.github/workflows/dispatch-ce.yml +++ b/.github/workflows/dispatch-ce.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - test: [ Streams, PersistentSubscriptions, Operations, Projections, Security, Misc ] + test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, Security, Misc ] name: Test CE (${{ inputs.docker-tag }}) with: docker-tag: ${{ inputs.docker-tag }} diff --git a/src/Kurrent.Client/Core/Certificates/X509Certificates.cs b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs index 3fe1006f5..9cda47a08 100644 --- a/src/Kurrent.Client/Core/Certificates/X509Certificates.cs +++ b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +#pragma warning disable SYSLIB0057 #if NET48 using Org.BouncyCastle.Crypto; @@ -13,35 +14,20 @@ namespace EventStore.Client; static class X509Certificates { - // TODO SS: Use .NET 8 X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) once the Windows32Exception issue is resolved public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { try { -#if NET9_0_OR_GREATER - using var publicCert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#if NET8_0_OR_GREATER + using var certificate = X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); #else using var publicCert = new X509Certificate2(certPemFilePath); -#endif - using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); + using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); using var certificate = publicCert.CopyWithPrivateKey(privateKey); - -#if NET48 - return new(certificate.Export(X509ContentType.Pfx)); -#else - return X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); #endif + + return new X509Certificate2(certificate.Export(X509ContentType.Pfx)); } catch (Exception ex) { throw new CryptographicException($"Failed to load private key: {ex.Message}"); } - - // Notes: - // using X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) would be the ideal choice here, - // but it's currently causing a Win32Exception specifically on Windows. Alternative implementation is used until the issue is resolved. - // - // Error: The SSL connection could not be established, see inner exception. AuthenticationException: Authentication failed because the platform - // does not support ephemeral keys. Win32Exception: No credentials are available in the security package - // - // public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) => - // X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); } } @@ -66,7 +52,7 @@ public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) public static RSA ImportPrivateKeyFromFile(this RSA rsa, string privateKeyPath) { var (content, label) = LoadPemKeyFile(privateKeyPath); - var privateKey = string.Join(string.Empty, content[1..^1]); + var privateKey = string.Join(string.Empty, content[1..^1]); var privateKeyBytes = Convert.FromBase64String(privateKey); if (label == RsaPemLabels.Pkcs8PrivateKey) diff --git a/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs index 307794019..c730b7b5a 100644 --- a/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs +++ b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs @@ -317,8 +317,7 @@ static void ConfigureClientCertificate(KurrentClientSettings settings, IReadOnly ); try { - settings.ConnectivitySettings.ClientCertificate = - X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); + settings.ConnectivitySettings.ClientCertificate = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); } catch (Exception ex) { throw new InvalidClientCertificateException("Failed to create client certificate.", ex); } diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs index b93bc9f89..c39895cf1 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs @@ -85,6 +85,11 @@ public static KurrentFixtureOptions DefaultOptions() { ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; + if (GlobalEnvironment.DockerImage.Contains("commercial")) { + defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; + defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; + } + if (port != NetworkPortProvider.DefaultEsdbPort) { if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs index 06b3663eb..7a6a98965 100644 --- a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs @@ -82,6 +82,11 @@ public static KurrentFixtureOptions DefaultOptions() { ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; + if (GlobalEnvironment.DockerImage.Contains("commercial")) { + defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; + defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; + } + if (port != NetworkPortProvider.DefaultEsdbPort) { if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; @@ -181,7 +186,7 @@ async Task GetNextAvailablePort(TimeSpan delay = default) { #if NET if (socket.Connected) await socket.DisconnectAsync(true); #else - if (socket.Connected) socket.Disconnect(true); + if (socket.Connected) socket.Disconnect(true); #endif } } diff --git a/test/Kurrent.Client.Tests/ClientCertificatesTests.cs b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs index 2903d8fb5..92d08fa51 100644 --- a/test/Kurrent.Client.Tests/ClientCertificatesTests.cs +++ b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs @@ -1,18 +1,21 @@ using EventStore.Client; using Humanizer; +using Kurrent.Client.Tests.TestNode; namespace Kurrent.Client.Tests; [Trait("Category", "Target:Misc")] [Trait("Category", "Target:Plugins")] [Trait("Category", "Type:UserCertificate")] -public class ClientCertificateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) - : KurrentPermanentTests(output, fixture) { +public class ClientCertificateTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] async Task bad_certificates_combinations_should_return_authentication_error(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://localhost:2113/?tls=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://localhost:{port}/?tls=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); @@ -24,9 +27,11 @@ async Task bad_certificates_combinations_should_return_authentication_error(stri [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), ValidClientCertificatesTestCases] async Task valid_certificates_combinations_should_write_to_stream(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://localhost:{port}/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); @@ -39,9 +44,11 @@ async Task valid_certificates_combinations_should_write_to_stream(string userCer [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] async Task basic_authentication_should_take_precedence(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://admin:changeit@localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://admin:changeit@localhost:{port}/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; var settings = KurrentClientSettings.Create(connectionString); settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); diff --git a/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs b/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs new file mode 100644 index 000000000..b60499693 --- /dev/null +++ b/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs @@ -0,0 +1,28 @@ +using System.Collections; +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +public abstract record InvalidCredentialsTestCase(TestUser User, Type ExpectedException); + +public class InvalidCredentialsTestCases : IEnumerable { + public IEnumerator GetEnumerator() { + yield return new object?[] { new MissingCredentials() }; + yield return new object?[] { new WrongUsername() }; + yield return new object?[] { new WrongPassword() }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public record MissingCredentials() : InvalidCredentialsTestCase(Fakers.Users.WithNoCredentials(), typeof(AccessDeniedException)) { + public override string ToString() => nameof(MissingCredentials); + } + + public record WrongUsername() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(false), typeof(NotAuthenticatedException)) { + public override string ToString() => nameof(WrongUsername); + } + + public record WrongPassword() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(wrongPassword: false), typeof(NotAuthenticatedException)) { + public override string ToString() => nameof(WrongPassword); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs index b699cec08..d66d4c0c4 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAllTests.cs @@ -107,7 +107,7 @@ await Assert.ThrowsAsync( .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) .AnyAsync() .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250)) + .WithTimeout(TimeSpan.FromSeconds(30)) ); } @@ -164,7 +164,7 @@ await Assert.ThrowsAsync( .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) .AnyAsync() .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250)) + .WithTimeout(TimeSpan.FromSeconds(30)) ); } diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs index 08533d1c3..48712ec68 100644 --- a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStreamTests.cs @@ -133,7 +133,7 @@ await Fixture.Subscriptions.CreateToStreamAsync( ); await Assert.ThrowsAsync( - () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromMilliseconds(250)) + () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromSeconds(30)) ); } @@ -188,7 +188,7 @@ await Fixture.Subscriptions.CreateToStreamAsync( ); await Assert.ThrowsAsync( - () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromMilliseconds(250)) + () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromSeconds(30)) ); } diff --git a/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs index 8db2eb293..15405cdcc 100644 --- a/test/Kurrent.Client.Tests/Projections/DisableProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class DisableProjectionTests(ITestOutputHelper output, DisableProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs index a321305b2..7261bfb9f 100644 --- a/test/Kurrent.Client.Tests/Projections/EnableProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class EnableProjectionTests(ITestOutputHelper output, EnableProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs similarity index 96% rename from test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs index c4bf0441c..2b2c8ef08 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionResultTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class GetProjectionResultTests(ITestOutputHelper output, GetProjectionResultTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs similarity index 96% rename from test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs index 56476f7e3..f180813ff 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionStateTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class GetProjectionStateTests(ITestOutputHelper output, GetProjectionStateTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs similarity index 93% rename from test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs index 8fea4bce2..41ed02932 100644 --- a/test/Kurrent.Client.Tests/Projections/GetProjectionStatusTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class GetProjectionStatusTests(ITestOutputHelper output, GetProjectionStatusTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs similarity index 95% rename from test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs index 10ff83c67..18f717efc 100644 --- a/test/Kurrent.Client.Tests/Projections/ListAllProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ListAllProjectionsTests(ITestOutputHelper output, ListAllProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs similarity index 95% rename from test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs index f6b0fbf81..774ad5a0f 100644 --- a/test/Kurrent.Client.Tests/Projections/ListContinuousProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs @@ -4,7 +4,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ListContinuousProjectionsTests(ITestOutputHelper output, ListContinuousProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs index fb1e537b9..d664d24fa 100644 --- a/test/Kurrent.Client.Tests/Projections/ListOneTimeProjectionsTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ListOneTimeProjectionsTests(ITestOutputHelper output, ListOneTimeProjectionsTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs similarity index 97% rename from test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs index fe45326e6..fd8d9b42a 100644 --- a/test/Kurrent.Client.Tests/Projections/ProjectionManagementTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs @@ -5,7 +5,7 @@ namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ProjectionManagementTests(ITestOutputHelper output, ProjectionManagementTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs index 4c8ef7c77..71d5f8fcc 100644 --- a/test/Kurrent.Client.Tests/Projections/ResetProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class ResetProjectionTests(ITestOutputHelper output, ResetProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs index 066d28d61..6431fbee5 100644 --- a/test/Kurrent.Client.Tests/Projections/RestartSubsystemTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs @@ -3,7 +3,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class RestartSubsystemTests(ITestOutputHelper output, RestartSubsystemTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] diff --git a/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs similarity index 94% rename from test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs rename to test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs index 17c4ba896..1af8e880b 100644 --- a/test/Kurrent.Client.Tests/Projections/UpdateProjectionTests.cs +++ b/test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs @@ -2,7 +2,7 @@ namespace Kurrent.Client.Tests.Projections; -[Trait("Category", "Target:Projections")] +[Trait("Category", "Target:ProjectionManagement")] public class UpdateProjectionTests(ITestOutputHelper output, UpdateProjectionTests.CustomFixture fixture) : KurrentTemporaryTests(output, fixture) { [Theory] diff --git a/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs b/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs new file mode 100644 index 000000000..f393e24da --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs @@ -0,0 +1,69 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class ChangePasswordTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Theory, ChangePasswordNullInputCases] + public async Task changing_user_password_with_null_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { + var ex = await Fixture.Users + .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory, ChangePasswordEmptyInputCases] + public async Task changing_user_password_with_empty_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { + var ex = await Fixture.Users + .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory(Skip = "This can't be right")] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task changing_user_password_with_user_with_insufficient_credentials_throws(string loginName, UserCredentials userCredentials) { + await Fixture.Users.CreateUserAsync(loginName, "Full Name", Array.Empty(), "password", userCredentials: TestCredentials.Root); + + await Fixture.Users + .ChangePasswordAsync(loginName, "password", "newPassword", userCredentials: userCredentials) + .ShouldThrowAsync(); + } + + [Fact] + public async Task changing_user_password_when_the_current_password_is_wrong_throws() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ChangePasswordAsync(user.LoginName, "wrong-password", "new-password", userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + } + + [Fact] + public async Task changing_user_password_with_correct_credentials() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ChangePasswordAsync(user.LoginName, user.Password, "new-password", userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + class ChangePasswordNullInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [null!, Faker.Internet.Password(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, null!, Faker.Internet.Password(), "currentPassword"]; + yield return [Faker.Person.UserName, Faker.Internet.Password(), null!, "newPassword"]; + } + } + + class ChangePasswordEmptyInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [string.Empty, Faker.Internet.Password(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, string.Empty, Faker.Internet.Password(), "currentPassword"]; + yield return [Faker.Person.UserName, Faker.Internet.Password(), string.Empty, "newPassword"]; + } + } +} diff --git a/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs new file mode 100644 index 000000000..3096e58e5 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs @@ -0,0 +1,93 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class CreateUserTests(ITestOutputHelper output, CreateUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Theory, CreateUserNullInputCases] + public async Task creating_user_with_null_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { + var ex = await Fixture.Users + .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory, CreateUserEmptyInputCases] + public async Task creating_user_with_empty_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { + var ex = await Fixture.Users + .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Fact] + public async Task creating_user_with_password_containing_ascii_chars() { + var user = Fakers.Users.Generate(); + + await Fixture.Users + .CreateUserAsync(user.LoginName, user.FullName, user.Groups, user.Password, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task creating_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) => + await Fixture.Users + .CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: testCase.User.Credentials + ) + .ShouldThrowAsync(testCase.ExpectedException); + + [Fact] + public async Task creating_user_can_be_read() { + var user = Fakers.Users.Generate(); + + await Fixture.Users + .CreateUserAsync( + user.LoginName, + user.FullName, + user.Groups, + user.Password, + userCredentials: TestCredentials.Root + ) + .ShouldNotThrowAsync(); + + var actual = await Fixture.Users.GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + var expected = new UserDetails( + user.Details.LoginName, + user.Details.FullName, + user.Details.Groups, + user.Details.Disabled, + actual.DateLastUpdated + ); + + actual.ShouldBeEquivalentTo(expected); + } + + class CreateUserNullInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [null!, Faker.Person.UserName, Faker.Lorem.Words(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, null!, Faker.Lorem.Words(), Faker.Internet.Password(), "fullName"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, null!, Faker.Internet.Password(), "groups"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, Faker.Lorem.Words(), null!, "password"]; + } + } + + class CreateUserEmptyInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [string.Empty, Faker.Person.UserName, Faker.Lorem.Words(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, string.Empty, Faker.Lorem.Words(), Faker.Internet.Password(), "fullName"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, Faker.Lorem.Words(), string.Empty, "password"]; + } + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs new file mode 100644 index 000000000..e8e4e4a9d --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs @@ -0,0 +1,69 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class DeleteUserTests(ITestOutputHelper output, DeleteUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task with_null_input_throws() { + var ex = await Fixture.Users + .DeleteUserAsync(null!, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Fact] + public async Task with_empty_input_throws() { + var ex = await Fixture.Users + .DeleteUserAsync(string.Empty, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .DeleteUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task cannot_be_read() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users.DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + var ex = await Fixture.Users + .GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.LoginName.ShouldBe(user.LoginName); + } + + [Fact] + public async Task a_second_time_throws() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users.DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + var ex = await Fixture.Users + .DeleteUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.LoginName.ShouldBe(user.LoginName); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs new file mode 100644 index 000000000..d4be66f86 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs @@ -0,0 +1,64 @@ +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class DisableUserTests(ITestOutputHelper output, DisableUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task with_null_input_throws() { + var ex = await Fixture.Users + .DisableUserAsync(null!, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + // must fix since it is returning value instead of param name + //ex.ParamName.ShouldBe("loginName"); + } + + [Fact] + public async Task with_empty_input_throws() { + var ex = await Fixture.Users + .DisableUserAsync(string.Empty, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .DisableUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task that_was_disabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + + await Fixture.Users + .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Fact] + public async Task that_is_enabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs new file mode 100644 index 000000000..7392a0dbe --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs @@ -0,0 +1,61 @@ +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class EnableUserTests(ITestOutputHelper output, EnableUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task with_null_input_throws() { + var ex = await Fixture.Users + .EnableUserAsync(null!, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Fact] + public async Task with_empty_input_throws() { + var ex = await Fixture.Users + .EnableUserAsync(string.Empty, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe("loginName"); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .EnableUserAsync(testCase.User.LoginName, userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task that_was_disabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users.DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + await Fixture.Users + .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Fact] + public async Task that_is_enabled() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs new file mode 100644 index 000000000..bb91c1a64 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs @@ -0,0 +1,12 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class GetCurrentUserTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task returns_the_current_user() { + var user = await Fixture.Users.GetCurrentUserAsync(TestCredentials.Root); + user.LoginName.ShouldBe(TestCredentials.Root.Username); + } +} diff --git a/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs new file mode 100644 index 000000000..8cf4d1ad1 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs @@ -0,0 +1,40 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class ListUserTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task returns_all_created_users() { + var seed = await Fixture.CreateTestUsers(); + + var admin = new UserDetails("admin", "Event Store Administrator", new[] { "$admins" }, false, default); + var ops = new UserDetails("ops", "Event Store Operations", new[] { "$ops" }, false, default); + + var expected = new[] { admin, ops } + .Concat(seed.Select(user => user.Details)) + .ToArray(); + + var actual = await Fixture.Users + .ListAllAsync(userCredentials: TestCredentials.Root) + .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) + .ToArrayAsync(); + + expected.ShouldBeSubsetOf(actual); + } + + [Fact] + public async Task returns_all_system_users() { + var admin = new UserDetails("admin", "Event Store Administrator", new[] { "$admins" }, false, default); + var ops = new UserDetails("ops", "Event Store Operations", new[] { "$ops" }, false, default); + + var expected = new[] { admin, ops }; + + var actual = await Fixture.Users + .ListAllAsync(userCredentials: TestCredentials.Root) + .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) + .ToArrayAsync(); + + expected.ShouldBeSubsetOf(actual); + } +} diff --git a/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs b/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs new file mode 100644 index 000000000..39b0bf718 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs @@ -0,0 +1,81 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class ResettingUserPasswordTests(ITestOutputHelper output, ResettingUserPasswordTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + public static IEnumerable NullInputCases() { + yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.Password, "loginName" }); + yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, "newPassword" }); + } + + [Theory] + [MemberData(nameof(NullInputCases))] + public async Task with_null_input_throws(string loginName, string newPassword, string paramName) { + var ex = await Fixture.Users + .ResetPasswordAsync(loginName, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + public static IEnumerable EmptyInputCases() { + yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.Password, "loginName" }); + yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, "newPassword" }); + } + + [Theory] + [MemberData(nameof(EmptyInputCases))] + public async Task with_empty_input_throws(string loginName, string newPassword, string paramName) { + var ex = await Fixture.Users + .ResetPasswordAsync(loginName, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { + await Fixture.Users.CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .ResetPasswordAsync(testCase.User.LoginName, "newPassword", userCredentials: testCase.User.Credentials) + .ShouldThrowAsync(testCase.ExpectedException); + } + + [Fact] + public async Task with_correct_credentials() { + var user = Fakers.Users.Generate(); + + await Fixture.Users.CreateUserAsync( + user.LoginName, + user.FullName, + user.Groups, + user.Password, + userCredentials: TestCredentials.Root + ); + + await Fixture.Users + .ResetPasswordAsync(user.LoginName, "new-password", userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Fact] + public async Task with_own_credentials_throws() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ResetPasswordAsync(user.LoginName, "new-password", userCredentials: user.Credentials) + .ShouldThrowAsync(); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs b/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs new file mode 100644 index 000000000..e4ffbe456 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs @@ -0,0 +1,44 @@ +using System.Net.Http.Headers; +using System.Text; +using EventStore.Client; +using static System.Convert; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class UserCredentialsTests { + const string JwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiI5OSIsIm5hbWUiOiJKb2huIFdpY2siLCJpYXQiOjE1MTYyMzkwMjJ9." + + "MEdv44JIdlLh-GgqxOTZD7DHq28xJowhQFmDnT3NDIE"; + + static readonly UTF8Encoding Utf8NoBom = new(false); + + static string EncodeCredentials(string username, string password) => ToBase64String(Utf8NoBom.GetBytes($"{username}:{password}")); + + [Fact] + public void from_username_and_password() { + var user = Fakers.Users.WithNonAsciiPassword(); + + var value = new AuthenticationHeaderValue( + Constants.Headers.BasicScheme, + EncodeCredentials(user.LoginName, user.Password) + ); + + var basicAuthInfo = value.ToString(); + + var credentials = new UserCredentials(user.LoginName, user.Password); + + credentials.Username.ShouldBe(user.LoginName); + credentials.Password.ShouldBe(user.Password); + credentials.ToString().ShouldBe(basicAuthInfo); + } + + [Fact] + public void from_bearer_token() { + var credentials = new UserCredentials(JwtToken); + + credentials.Username.ShouldBeNull(); + credentials.Password.ShouldBeNull(); + credentials.ToString().ShouldBe($"Bearer {JwtToken}"); + } +}