Skip to content

Commit e992d00

Browse files
Simulated API errors (#483)
* Add BankIdApiExceptionJsonConverter to BankIdDebugEventListener #270 * add simulated api errors with tests and documentation #270 --------- Co-authored-by: Elin Fokine <elin.ohlsson@outlook.com>
1 parent 277afac commit e992d00

8 files changed

Lines changed: 445 additions & 0 deletions

File tree

docs/articles/bankid.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The most common scenbario is to use Active Login for BankID auth/login, so most
3636
+ [Handle missing or invalid state cookie](#handle-missing-or-invalid-state-cookie)
3737
+ [Multi tenant scenario](#multi-tenant-scenario)
3838
+ [Customize the UI](#customize-the-ui)
39+
+ [Simulate BankID API errors](#simulate-bankid-api-errors)
3940
+ [Event listeners](#event-listeners)
4041
+ [Store data on auth completion](#store-data-on-auth-completion)
4142
+ [Resolve the end user ip](#resolve-the-end-user-ip)
@@ -789,6 +790,35 @@ If you want, you can override the UI for Auth and Sign with different templates.
789790

790791
See [the MVC sample](https://github.com/ActiveLogin/ActiveLogin.Authentication/tree/main/samples/Standalone.MvcSample) to see this in action, as demonstrated [here](https://github.com/ActiveLogin/ActiveLogin.Authentication/tree/main/samples/Standalone.MvcSample/Areas/ActiveLogin/Views/BankIdUiAuth/_Wrapper.cshtml).
791792
793+
### Simulate BankID API errors
794+
795+
When developing and testing your application, it can be useful to simulate various BankID API errors to ensure your application handles them gracefully. ActiveLogin provides a way to simulate these errors in any environment.
796+
797+
The BankIdBuilder has an extension method `AddSimulatedBankIdApiError` that can be used to simulate errors.
798+
The method takes the parameters:
799+
- `errorRate`: The rate of errors to simulate, a value between 0 and 1. For example, 0.5 will simulate an error in 50% of the requests.
800+
- `errors`: The errors that will be used to simulate. The errors are defined in a Dictionary with the key being an `ErrorCod e` Enum and the value being the ErrorDescription.
801+
- `varyErrorTypes`: If true, the error type will be varied between the errors in the list. If false, the same random error type will be used for all API calls.
802+
803+
#### Simulated API error usage
804+
805+
The example below will fail 20% of the API calls to BankId with either a RequestTimeout or InternalError.
806+
The error type will be varied between the errors.
807+
```csharp
808+
services
809+
.AddBankId(bankId =>
810+
{
811+
bankId.UseSimulatedEnvironment();
812+
bankId.AddSimulatedApiErrors(
813+
errorRate: 0.2,
814+
errors: new Dictionary<ErrorCode, string>()
815+
{
816+
{ ErrorCode.RequestTimeout, "Timeout in API" },
817+
{ ErrorCode.InternalError, "Internal error in API" }
818+
},
819+
varyErrorTypes: true);
820+
});
821+
```
792822

793823
### Event listeners
794824

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using ActiveLogin.Authentication.BankId.Api.Models;
2+
3+
namespace ActiveLogin.Authentication.BankId.Api;
4+
5+
/// <summary>
6+
/// This decorator simulates errors from the BankID API.
7+
/// An error is thrown with a probability of <paramref name="errorRate"/>.
8+
/// The error is one of the errors in <paramref name="errors"/>.
9+
/// An error is thrown only once for each session.
10+
/// The session is based on the <see cref="Response.OrderRef"></see> property.
11+
/// </summary>
12+
/// <param name="decorated">The decorated ApiClient <see cref="IBankIdAppApiClient"></see></param>
13+
/// <param name="errorRate">Probability of error</param>
14+
/// <param name="errors">Errors to use</param>
15+
/// <param name="varyErrorTypes">If true, the error type will vary between requests</param>
16+
public class BankIdErrorSimulatedApiClientDecorator(
17+
IBankIdAppApiClient decorated,
18+
double? errorRate = null,
19+
Dictionary<ErrorCode, string>? errors = null,
20+
bool? varyErrorTypes = null) : IBankIdAppApiClient
21+
{
22+
private readonly Dictionary<ErrorCode, string> _errors = errors ?? new Dictionary<ErrorCode, string>()
23+
{
24+
{ ErrorCode.AlreadyInProgress , "Already in progress"},
25+
{ ErrorCode.Maintenance , "Maintenance"},
26+
{ ErrorCode.RequestTimeout , "Request timeout"},
27+
{ ErrorCode.InternalError , "Internal error"}
28+
};
29+
30+
private static double? ValidateErrorRate(double? rate)
31+
{
32+
if (rate > 1 || rate < 0)
33+
{
34+
throw new ArgumentException("Error rate must be between 0 and 1.");
35+
}
36+
37+
return rate;
38+
}
39+
40+
private readonly double _throwErrorThreshold = ValidateErrorRate(errorRate) ?? 0.1;
41+
private readonly bool _varyErrorTypes = varyErrorTypes ?? false;
42+
private Error? _lastError;
43+
44+
public Task<AuthResponse> AuthAsync(AuthRequest request)
45+
{
46+
return CallImplementation(x => x.AuthAsync(request));
47+
}
48+
49+
public Task<SignResponse> SignAsync(SignRequest request)
50+
{
51+
return CallImplementation(x => x.SignAsync(request));
52+
}
53+
54+
public Task<PhoneAuthResponse> PhoneAuthAsync(PhoneAuthRequest request)
55+
{
56+
return CallImplementation(x => x.PhoneAuthAsync(request));
57+
}
58+
59+
public Task<PhoneSignResponse> PhoneSignAsync(PhoneSignRequest request)
60+
{
61+
return CallImplementation(x => x.PhoneSignAsync(request));
62+
}
63+
64+
public Task<CollectResponse> CollectAsync(CollectRequest request)
65+
{
66+
return CallImplementation(x => x.CollectAsync(request));
67+
}
68+
69+
public Task<CancelResponse> CancelAsync(CancelRequest request)
70+
{
71+
return CallImplementation(x => x.CancelAsync(request));
72+
}
73+
74+
private async Task<TResponse> CallImplementation<TResponse>(Func<IBankIdAppApiClient, Task<TResponse>> call)
75+
where TResponse : class
76+
{
77+
if (ShouldThrowError())
78+
{
79+
// If _lastError is null, get a random error
80+
_lastError ??= GetRandomError();
81+
82+
// If varyErrorTypes is true, the error type will vary between requests
83+
if (_varyErrorTypes)
84+
{
85+
_lastError = GetRandomError();
86+
}
87+
88+
throw new BankIdApiException(
89+
_lastError,
90+
new HttpRequestException("Unknown"));
91+
}
92+
93+
var result = await call(decorated);
94+
95+
return result;
96+
}
97+
98+
private bool ShouldThrowError()
99+
{
100+
var r = new Random();
101+
var result = r.NextDouble() < _throwErrorThreshold;
102+
return result;
103+
}
104+
105+
private Error GetRandomError()
106+
{
107+
var r = new Random();
108+
var error = _errors.ElementAt(r.Next(0, _errors.Count));
109+
return new Error(error.Key.ToString(), error.Value);
110+
}
111+
112+
}

src/ActiveLogin.Authentication.BankId.Core/Events/Infrastructure/BankIdDebugEventListener.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System.Reflection;
12
using System.Text.Json;
23
using System.Text.Json.Serialization;
34

5+
using ActiveLogin.Authentication.BankId.Api;
46
using ActiveLogin.Identity.Swedish;
57

68
using Microsoft.Extensions.Logging;
@@ -24,6 +26,7 @@ public BankIdDebugEventListener(ILogger<BankIdDebugEventListener> logger)
2426

2527
_serializerOptions.Converters.Add(new JsonStringEnumConverter());
2628
_serializerOptions.Converters.Add(new PersonalIdentityNumberJsonConverter());
29+
_serializerOptions.Converters.Add(new BankIdApiExceptionJsonConverter());
2730
}
2831

2932
public Task HandleAsync(BankIdEvent bankIdEvent)
@@ -62,4 +65,43 @@ public override void Write(
6265
JsonSerializerOptions options) =>
6366
writer.WriteStringValue(personalIdentityNumberValue.To12DigitString());
6467
}
68+
69+
// This converter is used to serialize BankIdApiException to JSON.
70+
// It serializes all properties of the exception, except for properties
71+
// of type "Type" and properties of types in the System.Reflection namespace.
72+
// Otherwise, this will fail when serialization and deserialization
73+
// with "'System.Reflection.MethodBase' instances are not supported. Path: $.TargetSite."
74+
private class BankIdApiExceptionJsonConverter : JsonConverter<BankIdApiException>
75+
{
76+
public override BankIdApiException Read(
77+
ref Utf8JsonReader reader,
78+
Type typeToConvert,
79+
JsonSerializerOptions options) =>
80+
throw new NotImplementedException();
81+
public override void Write(
82+
Utf8JsonWriter writer,
83+
BankIdApiException exception,
84+
JsonSerializerOptions options)
85+
{
86+
writer.WriteStartObject();
87+
var exceptionType = exception.GetType();
88+
writer.WriteString("ClassName", exceptionType.FullName);
89+
var properties = exceptionType.GetProperties()
90+
.Where(e => e.PropertyType != typeof(Type))
91+
.Where(e => e.PropertyType.Namespace != typeof(MemberInfo).Namespace)
92+
.ToList();
93+
foreach (var property in properties)
94+
{
95+
var propertyValue = property.GetValue(exception, null);
96+
if (options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull && propertyValue == null)
97+
{
98+
continue;
99+
}
100+
101+
writer.WritePropertyName(property.Name);
102+
JsonSerializer.Serialize(writer, propertyValue, property.PropertyType, options);
103+
}
104+
writer.WriteEndObject();
105+
}
106+
}
65107
}

src/ActiveLogin.Authentication.BankId.Core/IBankIdBuilderExtensions.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Security.Cryptography.X509Certificates;
22

33
using ActiveLogin.Authentication.BankId.Api;
4+
using ActiveLogin.Authentication.BankId.Api.Models;
45
using ActiveLogin.Authentication.BankId.Core.Certificate;
56
using ActiveLogin.Authentication.BankId.Core.CertificatePolicies;
67
using ActiveLogin.Authentication.BankId.Core.Cryptography;
@@ -9,6 +10,7 @@
910
using ActiveLogin.Authentication.BankId.Core.ResultStore;
1011

1112
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.DependencyInjection.Extensions;
1214

1315
namespace ActiveLogin.Authentication.BankId.Core;
1416
public static class IBankIdBuilderExtensions
@@ -268,6 +270,43 @@ public static IBankIdBuilder UseSimulatedEnvironment(this IBankIdBuilder builder
268270
);
269271
}
270272

273+
/// <summary>
274+
/// Add simulated API errors to the BankID API client.
275+
/// </summary>
276+
/// <param name="builder"></param>
277+
/// <param name="errorRate">Error percentage</param>
278+
/// <param name="errors">Predefined dictionary of errors (ErrorCode, Error Message)</param>
279+
/// <param name="varyErrorTypes">If true, the error type will vary between requests</param>
280+
/// <returns></returns>
281+
public static IBankIdBuilder AddSimulatedApiErrors(this IBankIdBuilder builder,
282+
double? errorRate = null,
283+
Dictionary<ErrorCode, string>? errors = null,
284+
bool? varyErrorTypes = null)
285+
{
286+
// Make sure there is only one implementation of IBankIdAppApiClient
287+
switch (builder.Services.Count(s => s.ServiceType == typeof(IBankIdAppApiClient)))
288+
{
289+
case 0:
290+
throw new InvalidOperationException("No IBankIdAppApiClient implementation found in the service collection.");
291+
case > 1:
292+
throw new InvalidOperationException("Multiple IBankIdAppApiClient implementations found in the service collection. Only one implementation is allowed.");
293+
}
294+
295+
// Decorate the original service with the simulated error decorator
296+
var original = builder.Services.Single(x => x.ServiceType == typeof(IBankIdAppApiClient));
297+
builder.Services.Remove(original);
298+
builder.Services.Add(
299+
new ServiceDescriptor(original.ServiceType, (x) =>
300+
{
301+
var originalServiceFactory = original.ImplementationFactory?.Invoke(x) as IBankIdAppApiClient;
302+
return new BankIdErrorSimulatedApiClientDecorator(
303+
originalServiceFactory!, errorRate, errors, varyErrorTypes);
304+
},
305+
original.Lifetime));
306+
307+
return builder;
308+
}
309+
271310
private static IBankIdBuilder UseSimulatedEnvironment(this IBankIdBuilder builder, Func<IServiceProvider, IBankIdAppApiClient> bankIdSimulatedAppApiClient, Func<IServiceProvider, IBankIdVerifyApiClient> bankIdSimulatedVerifyApiClient)
272311
{
273312
SetActiveLoginContext(builder.Services, BankIdEnvironments.Simulated, BankIdSimulatedAppApiClient.Version, BankIdSimulatedVerifyApiClient.Version);

test/ActiveLogin.Authentication.BankId.Api.Test/ActiveLogin.Authentication.BankId.Api.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
</PropertyGroup>
55

66
<ItemGroup>
7+
<PackageReference Include="AutoFixture" Version="4.18.1" />
78
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
89
<PackageReference Include="Moq" Version="4.20.72" />
910
<PackageReference Include="xunit" Version="2.9.2" />

0 commit comments

Comments
 (0)