diff --git a/docs/articles/bankid.md b/docs/articles/bankid.md index 9eacc2af..3cd8f665 100644 --- a/docs/articles/bankid.md +++ b/docs/articles/bankid.md @@ -210,6 +210,43 @@ services ``` +### Simulated environment with custom collect states + +The simulated environment uses a normal collect flow by default. For automated tests, a faster flow can be selected. The `FastCollectStates` sequence contains only the states needed to reach a successful result. + +```csharp +services + .AddBankId(bankId => + { + bankId.UseSimulatedEnvironment(options => + { + options.CollectStates = BankIdSimulatedAppApiClient.FastCollectStates; + }); + }); +``` + +The `AllSuccessfulCollectStates` sequence can be used to verify that the UI handles the different successful collect hint codes and text lengths. It intentionally does not include failed terminal states, so the simulated authentication can complete successfully. Set it with `options.CollectStates = BankIdSimulatedAppApiClient.AllSuccessfulCollectStates;`. + +For a specialized test, provide your own sequence. The final state should have `CollectStatus.Complete` so that the simulated authentication can finish successfully. + +```csharp +services + .AddBankId(bankId => + { + bankId.UseSimulatedEnvironment(options => + { + options.CollectStates = new List + { + new(CollectStatus.Pending, CollectHintCode.Started), + new(CollectStatus.Complete, CollectHintCode.UserSign) + }; + }); + }); +``` + +These examples require the `ActiveLogin.Authentication.BankId.Api` and `ActiveLogin.Authentication.BankId.Core` namespaces. The simulated client also applies a response delay to mimic the real flow. Delay is not configured by `UseSimulatedEnvironment`; if a test needs to remove it, resolve the registered `BankIdSimulatedAppApiClient` from dependency injection and set its `Delay` property to `TimeSpan.Zero`. + + ### Test environment This will use the real REST API for BankID, connecting to the Test environment. diff --git a/src/ActiveLogin.Authentication.BankId.Api/BankIdSimulatedAppApiClient.cs b/src/ActiveLogin.Authentication.BankId.Api/BankIdSimulatedAppApiClient.cs index ba7b44c3..fdcbd624 100644 --- a/src/ActiveLogin.Authentication.BankId.Api/BankIdSimulatedAppApiClient.cs +++ b/src/ActiveLogin.Authentication.BankId.Api/BankIdSimulatedAppApiClient.cs @@ -26,6 +26,34 @@ public class BankIdSimulatedAppApiClient : IBankIdAppApiClient new CollectState(CollectStatus.Complete, CollectHintCode.UserSign) }; + /// + /// Gets a new collect-state sequence that resembles a normal authentication flow. + /// + public static List NormalCollectStates => new(DefaultCollectStates); + + /// + /// Gets a new collect-state sequence with the fewest states needed to complete authentication. + /// + public static List FastCollectStates => new() + { + new CollectState(CollectStatus.Pending, CollectHintCode.OutstandingTransaction), + new CollectState(CollectStatus.Complete, CollectHintCode.UserSign) + }; + + /// + /// Gets a new collect-state sequence that covers the UI-relevant successful hint codes. + /// + public static List AllSuccessfulCollectStates => new() + { + new CollectState(CollectStatus.Pending, CollectHintCode.OutstandingTransaction), + new CollectState(CollectStatus.Pending, CollectHintCode.NoClient), + new CollectState(CollectStatus.Pending, CollectHintCode.Started), + new CollectState(CollectStatus.Pending, CollectHintCode.UserMrtd), + new CollectState(CollectStatus.Pending, CollectHintCode.UserCallConfirm), + new CollectState(CollectStatus.Pending, CollectHintCode.UserSign), + new CollectState(CollectStatus.Complete, CollectHintCode.UserSign) + }; + private readonly string _givenName; private readonly string _surname; private readonly string _name; @@ -38,7 +66,7 @@ public class BankIdSimulatedAppApiClient : IBankIdAppApiClient private TimeSpan _delay = TimeSpan.FromMilliseconds(250); public BankIdSimulatedAppApiClient() - : this(DefaultCollectStates) + : this(NormalCollectStates) { } @@ -53,7 +81,7 @@ public BankIdSimulatedAppApiClient(string givenName, string surname) } public BankIdSimulatedAppApiClient(string givenName, string surname, string personalIdentityNumber) - : this(givenName, surname, personalIdentityNumber, DefaultCollectStates) + : this(givenName, surname, personalIdentityNumber, NormalCollectStates) { } @@ -63,19 +91,29 @@ public BankIdSimulatedAppApiClient(string givenName, string surname, string pers } public BankIdSimulatedAppApiClient(string givenName, string surname, string name, string personalIdentityNumber) - : this(givenName, surname, name, personalIdentityNumber, DefaultBankIdIssueDate, DefaultUniqueHardwareId, DefaultCollectStates) + : this(givenName, surname, name, personalIdentityNumber, DefaultBankIdIssueDate, DefaultUniqueHardwareId, NormalCollectStates) { } public BankIdSimulatedAppApiClient(string givenName, string surname, string name, string personalIdentityNumber, string bankIdIssueDate, string uniqueHardwareId, List collectStates) { + if (collectStates == null) + { + throw new ArgumentNullException(nameof(collectStates)); + } + + if (collectStates.Count == 0) + { + throw new ArgumentException("At least one collect state must be configured.", nameof(collectStates)); + } + _givenName = givenName; _surname = surname; _name = name; _personalIdentityNumber = personalIdentityNumber; _bankIdIssueDate = bankIdIssueDate; _uniqueHardwareId = uniqueHardwareId; - _collectStates = collectStates; + _collectStates = new List(collectStates); } public TimeSpan Delay diff --git a/src/ActiveLogin.Authentication.BankId.AspNetCore/README.md b/src/ActiveLogin.Authentication.BankId.AspNetCore/README.md index 3ebaa71e..c9becee8 100644 --- a/src/ActiveLogin.Authentication.BankId.AspNetCore/README.md +++ b/src/ActiveLogin.Authentication.BankId.AspNetCore/README.md @@ -26,6 +26,8 @@ services }); ``` +For faster automated tests, configure the simulated environment with `BankIdSimulatedAppApiClient.FastCollectStates`. See the [simulated environment documentation](../../docs/articles/bankid.md) for custom collect-state examples. + ### Production ```csharp diff --git a/src/ActiveLogin.Authentication.BankId.Core/BankIdSimulatedEnvironmentOptions.cs b/src/ActiveLogin.Authentication.BankId.Core/BankIdSimulatedEnvironmentOptions.cs new file mode 100644 index 00000000..ea1cd85b --- /dev/null +++ b/src/ActiveLogin.Authentication.BankId.Core/BankIdSimulatedEnvironmentOptions.cs @@ -0,0 +1,14 @@ +using ActiveLogin.Authentication.BankId.Api; + +namespace ActiveLogin.Authentication.BankId.Core; + +/// +/// Configuration for the simulated BankID environment. +/// +public sealed class BankIdSimulatedEnvironmentOptions +{ + /// + /// The sequence of states returned by collect calls. + /// + public List CollectStates { get; set; } = BankIdSimulatedAppApiClient.NormalCollectStates; +} \ No newline at end of file diff --git a/src/ActiveLogin.Authentication.BankId.Core/IBankIdBuilderExtensions.cs b/src/ActiveLogin.Authentication.BankId.Core/IBankIdBuilderExtensions.cs index b1e5e93e..16ca7207 100644 --- a/src/ActiveLogin.Authentication.BankId.Core/IBankIdBuilderExtensions.cs +++ b/src/ActiveLogin.Authentication.BankId.Core/IBankIdBuilderExtensions.cs @@ -267,7 +267,29 @@ public static IBankIdBuilder UseProductionEnvironment(this IBankIdBuilder builde public static IBankIdBuilder UseSimulatedEnvironment(this IBankIdBuilder builder) { return UseSimulatedEnvironment(builder, - x => new BankIdSimulatedAppApiClient(), + x => new BankIdSimulatedAppApiClient(BankIdSimulatedAppApiClient.NormalCollectStates), + x => new BankIdSimulatedVerifyApiClient() + ); + } + + /// + /// Use simulated (in memory) environment. To be used for automated testing. + /// + /// + /// Configures the collect-state sequence. + /// + public static IBankIdBuilder UseSimulatedEnvironment(this IBankIdBuilder builder, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var options = new BankIdSimulatedEnvironmentOptions(); + configure(options); + + return UseSimulatedEnvironment(builder, + x => new BankIdSimulatedAppApiClient(options.CollectStates), x => new BankIdSimulatedVerifyApiClient() ); } diff --git a/test/ActiveLogin.Authentication.BankId.Api.Test/BankIdSimulatedAppApiClient_Tests.cs b/test/ActiveLogin.Authentication.BankId.Api.Test/BankIdSimulatedAppApiClient_Tests.cs index d6d4bd88..d5145f2b 100644 --- a/test/ActiveLogin.Authentication.BankId.Api.Test/BankIdSimulatedAppApiClient_Tests.cs +++ b/test/ActiveLogin.Authentication.BankId.Api.Test/BankIdSimulatedAppApiClient_Tests.cs @@ -193,4 +193,85 @@ public async Task CancelAsync_CancelsTheCollectFlow() // Assert await Assert.ThrowsAsync(() => bankIdClient.CollectAsync(new CollectRequest(authResponse.OrderRef))); } + + [Fact] + public async Task CollectAsync_WithCustomCollectStates__ShouldReturnConfiguredStates() + { + // Arange + var states = new List + { + new(CollectStatus.Pending, CollectHintCode.NoClient), + new(CollectStatus.Complete, CollectHintCode.UserSign) + }; + var bankIdClient = new BankIdSimulatedAppApiClient(states) + { + Delay = TimeSpan.Zero + }; + + // Act + var authResponse = await bankIdClient.AuthAsync(new AuthRequest("1.1.1.1")); + var firstCollectResponse = await bankIdClient.CollectAsync(new CollectRequest(authResponse.OrderRef)); + var secondCollectResponse = await bankIdClient.CollectAsync(new CollectRequest(authResponse.OrderRef)); + + // Assert + Assert.Equal(CollectStatus.Pending, firstCollectResponse.GetCollectStatus()); + Assert.Equal(CollectHintCode.NoClient, firstCollectResponse.GetCollectHintCode()); + Assert.Equal(CollectStatus.Complete, secondCollectResponse.GetCollectStatus()); + Assert.Equal(CollectHintCode.UserSign, secondCollectResponse.GetCollectHintCode()); + } + + [Fact] + public async Task CollectAsync_WithFastCollectStates__ShouldCompleteAfterOnePendingState() + { + // Arange + var bankIdClient = new BankIdSimulatedAppApiClient(BankIdSimulatedAppApiClient.FastCollectStates) + { + Delay = TimeSpan.Zero + }; + + // Act + var authResponse = await bankIdClient.AuthAsync(new AuthRequest("1.1.1.1")); + var firstCollectResponse = await bankIdClient.CollectAsync(new CollectRequest(authResponse.OrderRef)); + var secondCollectResponse = await bankIdClient.CollectAsync(new CollectRequest(authResponse.OrderRef)); + + // Assert + Assert.Equal(CollectStatus.Pending, firstCollectResponse.GetCollectStatus()); + Assert.Equal(CollectStatus.Complete, secondCollectResponse.GetCollectStatus()); + } + + [Fact] + public async Task CollectAsync_WithCustomCollectStates__ShouldNotBeAffectedByChangesToOriginalList() + { + // Arange + var states = new List + { + new(CollectStatus.Pending, CollectHintCode.NoClient), + new(CollectStatus.Complete, CollectHintCode.UserSign) + }; + var bankIdClient = new BankIdSimulatedAppApiClient(states) + { + Delay = TimeSpan.Zero + }; + states.Clear(); + + // Act + var authResponse = await bankIdClient.AuthAsync(new AuthRequest("1.1.1.1")); + var collectResponse = await bankIdClient.CollectAsync(new CollectRequest(authResponse.OrderRef)); + + // Assert + Assert.Equal(CollectStatus.Pending, collectResponse.GetCollectStatus()); + Assert.Equal(CollectHintCode.NoClient, collectResponse.GetCollectHintCode()); + } + + [Fact] + public void Constructor_WithNullCollectStates__ShouldThrowArgumentNullException() + { + Assert.Throws(() => new BankIdSimulatedAppApiClient(null!)); + } + + [Fact] + public void Constructor_WithEmptyCollectStates__ShouldThrowArgumentException() + { + Assert.Throws(() => new BankIdSimulatedAppApiClient(new List())); + } } diff --git a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdBuilder_Tests.cs b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdBuilder_Tests.cs index 3c80f0a6..87d82768 100644 --- a/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdBuilder_Tests.cs +++ b/test/ActiveLogin.Authentication.BankId.Core.Test/BankIdBuilder_Tests.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; using ActiveLogin.Authentication.BankId.Api; +using ActiveLogin.Authentication.BankId.Api.Models; using Microsoft.Extensions.DependencyInjection; @@ -30,4 +33,29 @@ public void AddSimulatedApiErrors_Throws_If_No_IBankIdAppApiClient_Exists_In_Ser } + [Fact] + public async Task UseSimulatedEnvironment_WithOptions__RegistersConfiguredCollectStates() + { + var services = new ServiceCollection(); + var builder = new BankIdBuilder(services); + var states = new List + { + new(CollectStatus.Pending, CollectHintCode.NoClient), + new(CollectStatus.Complete, CollectHintCode.UserSign) + }; + + builder.UseSimulatedEnvironment(options => options.CollectStates = states); + + using var serviceProvider = services.BuildServiceProvider(); + var client = Assert.IsType(serviceProvider.GetRequiredService()); + client.Delay = TimeSpan.Zero; + var authResponse = await client.AuthAsync(new AuthRequest("1.1.1.1")); + var firstCollectResponse = await client.CollectAsync(new CollectRequest(authResponse.OrderRef)); + var secondCollectResponse = await client.CollectAsync(new CollectRequest(authResponse.OrderRef)); + + Assert.Equal(CollectStatus.Pending, firstCollectResponse.GetCollectStatus()); + Assert.Equal(CollectHintCode.NoClient, firstCollectResponse.GetCollectHintCode()); + Assert.Equal(CollectStatus.Complete, secondCollectResponse.GetCollectStatus()); + } + }