Skip to content

Commit acf012e

Browse files
committed
chore: Cleanup Access Layer
1 parent 1fbc9a4 commit acf012e

File tree

4 files changed

+227
-21
lines changed

4 files changed

+227
-21
lines changed

src/app/ApplicationTemplate.Access/ApiClients/Authentication/AuthenticationApiClientMock.cs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,24 @@ namespace ApplicationTemplate.DataAccess;
1010

1111
public sealed class AuthenticationApiClientMock : IAuthenticationApiClient
1212
{
13+
private const int TokenExpirationSeconds = 600;
14+
1315
private readonly JsonSerializerOptions _serializerOptions;
1416
private readonly IOptionsMonitor<MockOptions> _mockOptionsMonitor;
17+
private readonly TimeProvider _timeProvider;
1518

16-
public AuthenticationApiClientMock(JsonSerializerOptions serializerOptions, IOptionsMonitor<MockOptions> mockOptionsMonitor)
19+
public AuthenticationApiClientMock(JsonSerializerOptions serializerOptions, IOptionsMonitor<MockOptions> mockOptionsMonitor, TimeProvider timeProvider)
1720
{
1821
_serializerOptions = serializerOptions;
1922
_mockOptionsMonitor = mockOptionsMonitor;
23+
_timeProvider = timeProvider;
2024
}
2125

2226
public async Task<AuthenticationData> CreateAccount(CancellationToken ct, string email, string password)
2327
{
2428
await SimulateDelay(ct);
2529

26-
// We authenticate the user on account creation, since we don't have a backend to register and validate the user
30+
// We authenticate the user on account creation, since we don't have a backend to register and validate the user.
2731
return CreateAuthenticationData();
2832
}
2933

@@ -48,34 +52,48 @@ public async Task<AuthenticationData> RefreshToken(CancellationToken ct, Authent
4852
return CreateAuthenticationData(unauthorizedToken.AccessToken.Payload);
4953
}
5054

51-
private AuthenticationData CreateAuthenticationData(AuthenticationToken token = null)
52-
{
53-
var encodedJwt = CreateJsonWebToken(token);
54-
55-
return new AuthenticationData()
56-
{
57-
AccessToken = new JwtData<AuthenticationToken>(encodedJwt, _serializerOptions),
58-
RefreshToken = Guid.NewGuid().ToString(format: null, CultureInfo.InvariantCulture),
59-
};
60-
}
61-
62-
private string CreateJsonWebToken(AuthenticationToken token = null)
55+
/// <summary>
56+
/// Creates a JSON Web Token.
57+
/// </summary>
58+
/// <remarks>
59+
/// This function has been made public and static for testing purposes.
60+
/// </remarks>
61+
/// <param name="token">The token to use.</param>
62+
/// <param name="email">The email or unique name to store in the token.</param>
63+
/// <param name="now">The current date and time to use for the authentication token.</param>
64+
/// <param name="serializerOptions">The serializer options to use for the token serialization.</param>
65+
/// <returns>The JSON Web token.</returns>
66+
public static string CreateJsonWebToken(AuthenticationToken token = null, string email = null, DateTimeOffset? now = null, JsonSerializerOptions serializerOptions = null)
6367
{
6468
const string header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; // alg=HS256, type=JWT
6569
const string signature = "QWqnPP8W6ymexz74P6quP-oG-wxr7vMGqrEL8y_tV6M"; // dummy stuff
6670

67-
token ??= new AuthenticationToken(default, DateTimeOffset.MinValue, DateTimeOffset.MinValue);
71+
now ??= DateTimeOffset.Now;
72+
73+
token ??= new AuthenticationToken(email: email, expiration: now.Value.AddSeconds(TokenExpirationSeconds), issuedAt: now.Value);
6874

6975
string payload;
7076
using (var stream = new MemoryStream())
7177
{
72-
JsonSerializer.Serialize(stream, token, _serializerOptions);
78+
JsonSerializer.Serialize(stream, token, serializerOptions);
7379
payload = Convert.ToBase64String(stream.ToArray());
7480
}
7581

7682
return header + '.' + payload + '.' + signature;
7783
}
7884

85+
private AuthenticationData CreateAuthenticationData(AuthenticationToken token = null, string email = null)
86+
{
87+
var now = _timeProvider.GetLocalNow();
88+
var encodedJwt = CreateJsonWebToken(token, email, now, _serializerOptions);
89+
90+
return new AuthenticationData()
91+
{
92+
AccessToken = new JwtData<AuthenticationToken>(encodedJwt, _serializerOptions),
93+
RefreshToken = Guid.NewGuid().ToString(format: null, CultureInfo.InvariantCulture),
94+
};
95+
}
96+
7997
private async Task SimulateDelay(CancellationToken ct)
8098
{
8199
if (_mockOptionsMonitor.CurrentValue.IsDelayForSimulatedApiCallsEnabled)

src/app/ApplicationTemplate.Access/ApiClients/Authentication/Data/AuthenticationToken.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ public AuthenticationToken(string email, DateTimeOffset expiration, DateTimeOffs
1313
}
1414

1515
[JsonPropertyName("unique_name")]
16-
public string Email { get; init; }
16+
public string Email { get; }
1717

1818
[JsonPropertyName("exp")]
1919
[JsonConverter(typeof(UnixTimestampJsonConverter))]
20-
public DateTimeOffset Expiration { get; init; }
20+
public DateTimeOffset Expiration { get; }
2121

2222
[JsonPropertyName("iat")]
2323
[JsonConverter(typeof(UnixTimestampJsonConverter))]
24-
public DateTimeOffset IssuedAt { get; init; }
24+
public DateTimeOffset IssuedAt { get; }
2525
}

src/app/ApplicationTemplate.Access/LocalStorage/ApplicationSettings.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.Collections.Immutable;
1+
using System.Collections.Immutable;
32

43
namespace ApplicationTemplate.DataAccess;
54

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Reactive.Linq;
4+
using System.Reactive.Subjects;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using ApplicationTemplate.Business;
8+
using ApplicationTemplate.DataAccess;
9+
using Microsoft.Extensions.Logging;
10+
using NSubstitute;
11+
using Uno.Extensions;
12+
using Xunit;
13+
14+
namespace ApplicationTemplate.Tests.Unit;
15+
16+
/// <summary>
17+
/// Tests for the <see cref="AuthenticationService"/>.
18+
/// </summary>
19+
public sealed class AuthenticationServiceShould : IDisposable
20+
{
21+
private readonly IAuthenticationService _authenticationService;
22+
private readonly IApplicationSettingsRepository _applicationSettingsRepository;
23+
private readonly IAuthenticationApiClient _authenticationRepository;
24+
private readonly BehaviorSubject<ApplicationSettings> _applicationSettingsSubject;
25+
26+
public AuthenticationServiceShould()
27+
{
28+
_applicationSettingsRepository = Substitute.For<IApplicationSettingsRepository>();
29+
_authenticationRepository = Substitute.For<IAuthenticationApiClient>();
30+
31+
_authenticationService = new AuthenticationService(
32+
Substitute.For<ILoggerFactory>(),
33+
_applicationSettingsRepository,
34+
_authenticationRepository
35+
);
36+
37+
_applicationSettingsSubject = new BehaviorSubject<ApplicationSettings>(CreateApplicationSettings());
38+
39+
_applicationSettingsRepository.GetAndObserveCurrent().Returns(_applicationSettingsSubject);
40+
_applicationSettingsRepository.GetCurrent(CancellationToken.None).Returns(Task.FromResult(_applicationSettingsSubject.Value));
41+
42+
_applicationSettingsRepository.When(x => x.SetAuthenticationData(Arg.Any<CancellationToken>(), Arg.Any<AuthenticationData>()))
43+
.Do(x => _applicationSettingsSubject.OnNext(CreateApplicationSettings(x.Arg<AuthenticationData>())));
44+
}
45+
46+
[Fact]
47+
public async Task ReturnAuthenticationData_WhenLoggingIn()
48+
{
49+
// Arrange
50+
var email = "email";
51+
var password = "password";
52+
var authenticationData = CreateAuthenticationData(email: email);
53+
54+
_authenticationRepository.Login(Arg.Any<CancellationToken>(), Arg.Any<string>(), Arg.Any<string>()).Returns(authenticationData);
55+
56+
// Act
57+
var result = await _authenticationService.Login(CancellationToken.None, email, password);
58+
59+
// Assert
60+
Assert.Equal(authenticationData.AccessToken, result.AccessToken);
61+
}
62+
63+
[Fact]
64+
public async Task ReturnAuthenticationData_WhenRefreshingTheToken()
65+
{
66+
// Arrange
67+
var email = "email";
68+
var password = "password";
69+
var refreshedEmail = "RefreshedEmail";
70+
var authenticationData = CreateAuthenticationData(email: email);
71+
var refreshAuthenticationData = CreateAuthenticationData(email: refreshedEmail);
72+
73+
_authenticationRepository.Login(Arg.Any<CancellationToken>(), Arg.Any<string>(), Arg.Any<string>()).Returns(authenticationData);
74+
_authenticationRepository.RefreshToken(Arg.Any<CancellationToken>(), Arg.Any<AuthenticationData>()).Returns(refreshAuthenticationData);
75+
76+
// Act
77+
var loginResult = await _authenticationService.Login(CancellationToken.None, email, password);
78+
var result = await _authenticationService.RefreshToken(CancellationToken.None, new HttpRequestMessage(), loginResult);
79+
80+
// Assert
81+
Assert.Equal(result.AccessToken.Payload.Email, refreshedEmail);
82+
}
83+
84+
[Fact]
85+
public async Task NotifySuscribersThatTheSessionExpired_WhenNotifySesionExpiredIsCalled()
86+
{
87+
// Arrange
88+
var notifySessionExpired = false;
89+
90+
_authenticationService.ObserveSessionExpired().Subscribe(_ => notifySessionExpired = true);
91+
92+
// Act
93+
await _authenticationService.NotifySessionExpired(CancellationToken.None, new HttpRequestMessage(), CreateAuthenticationData("email"));
94+
95+
// Assert
96+
Assert.True(notifySessionExpired);
97+
}
98+
99+
[Fact]
100+
public async Task DiscardUserSettings_OnLogout()
101+
{
102+
// Arrange
103+
// Act
104+
await _authenticationService.Logout(CancellationToken.None);
105+
106+
// Assert
107+
await _applicationSettingsRepository.Received().DiscardUserSettings(CancellationToken.None);
108+
}
109+
110+
[Fact]
111+
public async Task SignalThatTheUserIsLoggedOut_WhenThereIsNoAuthenticationDataInAppSettings()
112+
{
113+
// Arrange
114+
ResetApplicationSettings();
115+
116+
// Act
117+
var result = await _authenticationService.GetAndObserveIsAuthenticated().FirstAsync();
118+
119+
// Assert
120+
Assert.False(result);
121+
}
122+
123+
[Fact]
124+
public async Task RespondNull_WhenThereIsNoAuthenticationDataInAppSettings()
125+
{
126+
// Arrange
127+
ResetApplicationSettings();
128+
129+
// Act
130+
var result = await _authenticationService.GetAndObserveAuthenticationData().FirstAsync();
131+
132+
// Assert
133+
Assert.Null(result);
134+
}
135+
136+
[Fact]
137+
public async Task SignalThatUserIsLoggedIn_WhenTheSettingsContainsAuthenticationData()
138+
{
139+
// Arrange
140+
var authenticationData = CreateAuthenticationData("email");
141+
142+
_applicationSettingsSubject.OnNext(CreateApplicationSettings(authenticationData));
143+
144+
// Act
145+
var result = await _authenticationService.GetAndObserveIsAuthenticated().FirstAsync();
146+
147+
// Assert
148+
Assert.True(result);
149+
}
150+
151+
[Fact]
152+
public async Task GiveAuthData_WhenTheSettingsContainsAuthenticationData()
153+
{
154+
// Arrange
155+
var authenticationData = CreateAuthenticationData("email");
156+
157+
_applicationSettingsSubject.OnNext(CreateApplicationSettings(authenticationData));
158+
159+
// Act
160+
var result = await _authenticationService.GetAndObserveAuthenticationData().FirstAsync();
161+
162+
// Assert
163+
Assert.Equal(authenticationData.AccessToken, result.AccessToken);
164+
}
165+
166+
public void Dispose()
167+
{
168+
_applicationSettingsSubject.Dispose();
169+
}
170+
171+
private void ResetApplicationSettings()
172+
{
173+
_applicationSettingsSubject.OnNext(CreateApplicationSettings());
174+
}
175+
176+
private AuthenticationData CreateAuthenticationData(string email, bool isAccessTokenNull = false)
177+
{
178+
return new AuthenticationData
179+
{
180+
AccessToken = isAccessTokenNull ? null : new JwtData<AuthenticationToken>(AuthenticationApiClientMock.CreateJsonWebToken(email: email, serializerOptions: SerializationConfiguration.DefaultJsonSerializerOptions)),
181+
RefreshToken = "RefreshToken",
182+
};
183+
}
184+
185+
private ApplicationSettings CreateApplicationSettings(AuthenticationData authenticationData = null)
186+
{
187+
return new ApplicationSettings() { AuthenticationData = authenticationData };
188+
}
189+
}

0 commit comments

Comments
 (0)