Skip to content
Merged
37 changes: 37 additions & 0 deletions docs/articles/bankid.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<BankIdSimulatedAppApiClient.CollectState>
{
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,34 @@ public class BankIdSimulatedAppApiClient : IBankIdAppApiClient
new CollectState(CollectStatus.Complete, CollectHintCode.UserSign)
};

/// <summary>
/// Gets a new collect-state sequence that resembles a normal authentication flow.
/// </summary>
public static List<CollectState> NormalCollectStates => new(DefaultCollectStates);

/// <summary>
/// Gets a new collect-state sequence with the fewest states needed to complete authentication.
/// </summary>
public static List<CollectState> FastCollectStates => new()
{
new CollectState(CollectStatus.Pending, CollectHintCode.OutstandingTransaction),
new CollectState(CollectStatus.Complete, CollectHintCode.UserSign)
};

/// <summary>
/// Gets a new collect-state sequence that covers the UI-relevant successful hint codes.
/// </summary>
public static List<CollectState> 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;
Expand All @@ -38,7 +66,7 @@ public class BankIdSimulatedAppApiClient : IBankIdAppApiClient
private TimeSpan _delay = TimeSpan.FromMilliseconds(250);

public BankIdSimulatedAppApiClient()
: this(DefaultCollectStates)
: this(NormalCollectStates)
{
}

Expand All @@ -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)
{
}

Expand All @@ -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)
Comment thread
torselden marked this conversation as resolved.
{
}

public BankIdSimulatedAppApiClient(string givenName, string surname, string name, string personalIdentityNumber, string bankIdIssueDate, string uniqueHardwareId, List<CollectState> 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<CollectState>(collectStates);
}

public TimeSpan Delay
Expand Down
2 changes: 2 additions & 0 deletions src/ActiveLogin.Authentication.BankId.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using ActiveLogin.Authentication.BankId.Api;

namespace ActiveLogin.Authentication.BankId.Core;

/// <summary>
/// Configuration for the simulated BankID environment.
/// </summary>
public sealed class BankIdSimulatedEnvironmentOptions
{
/// <summary>
/// The sequence of states returned by collect calls.
/// </summary>
public List<BankIdSimulatedAppApiClient.CollectState> CollectStates { get; set; } = BankIdSimulatedAppApiClient.NormalCollectStates;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
}

/// <summary>
/// Use simulated (in memory) environment. To be used for automated testing.
/// </summary>
/// <param name="builder"></param>
/// <param name="configure">Configures the collect-state sequence.</param>
/// <returns></returns>
public static IBankIdBuilder UseSimulatedEnvironment(this IBankIdBuilder builder, Action<BankIdSimulatedEnvironmentOptions> 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()
);
Comment thread
Copilot marked this conversation as resolved.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,85 @@ public async Task CancelAsync_CancelsTheCollectFlow()
// Assert
await Assert.ThrowsAsync<BankIdApiException>(() => bankIdClient.CollectAsync(new CollectRequest(authResponse.OrderRef)));
}

[Fact]
public async Task CollectAsync_WithCustomCollectStates__ShouldReturnConfiguredStates()
{
// Arange
Comment thread
torselden marked this conversation as resolved.
var states = new List<BankIdSimulatedAppApiClient.CollectState>
{
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<BankIdSimulatedAppApiClient.CollectState>
{
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<ArgumentNullException>(() => new BankIdSimulatedAppApiClient(null!));
}

[Fact]
public void Constructor_WithEmptyCollectStates__ShouldThrowArgumentException()
{
Assert.Throws<ArgumentException>(() => new BankIdSimulatedAppApiClient(new List<BankIdSimulatedAppApiClient.CollectState>()));
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<BankIdSimulatedAppApiClient.CollectState>
{
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<BankIdSimulatedAppApiClient>(serviceProvider.GetRequiredService<IBankIdAppApiClient>());
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());
}

}
Loading