diff --git a/WotBlitzStatisticsPro.Blazor.Tests/Services/NavigatorInterceptorTests.cs b/WotBlitzStatisticsPro.Blazor.Tests/Services/NavigatorInterceptorTests.cs index b39622c..eb84e25 100644 --- a/WotBlitzStatisticsPro.Blazor.Tests/Services/NavigatorInterceptorTests.cs +++ b/WotBlitzStatisticsPro.Blazor.Tests/Services/NavigatorInterceptorTests.cs @@ -1,22 +1,30 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using Moq; using NUnit.Framework; +using WotBlitzStatisticsPro.Blazor.GraphQl; using WotBlitzStatisticsPro.Blazor.Messages; +using WotBlitzStatisticsPro.Blazor.Model; using WotBlitzStatisticsPro.Blazor.Services; namespace WotBlitzStatisticsPro.Blazor.Tests.Services { - public class NavigatorInterceptorTests + public class NavigatorInterceptorTests: TestContextBase { private NavigationMessagesInterceptor _service; private MockNavigationManager _navigationManager; + private Mock _localStorageMock; + [SetUp] public void Setup() { + _localStorageMock = new(); + _navigationManager = new MockNavigationManager(); - _service = new NavigationMessagesInterceptor(_navigationManager); + _service = new NavigationMessagesInterceptor(_navigationManager, _localStorageMock.Object, MediatorMock.Object); } @@ -30,5 +38,90 @@ namespace WotBlitzStatisticsPro.Blazor.Tests.Services _navigationManager.Uri.Should().Be($"{_navigationManager.BaseUri}player/{accountId}"); } + + [Test] + public async Task ShouldCallProlongWargamingAccessTokenMessageIfFiveDaysBeforeTokenExpiration() + { + int accountId = 42; + string oldToken = "OldTocken"; + long expiration = Convert.ToInt64((DateTime.Now.AddDays(4) - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds); + _localStorageMock.Setup(s => s.GetItemAsync(Constants.LoginInfoLocalStorageKey)) + .ReturnsAsync(new LoginInfo { AccountId = accountId, AccessToken = oldToken, ExpiresAt = expiration}); + + var message = new OpenPlayerInfoMessage(accountId, true); + + await _service.Handle(message, CancellationToken.None); + + MediatorMock.Verify(m => m.Publish( + It.IsAny(), + It.IsAny()), Times.Once); + MediatorMock.Verify(m => m.Publish( + It.IsAny(), + It.IsAny()), Times.Never); + } + + [Test] + public async Task ShouldNotCallProlongWargamingAccessTokenMessageIfMoreThenFiveDaysBeforeTokenExpiration() + { + int accountId = 42; + string oldToken = "OldTocken"; + long expiration = Convert.ToInt64((DateTime.Now.AddDays(6) - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds); + _localStorageMock.Setup(s => s.GetItemAsync(Constants.LoginInfoLocalStorageKey)) + .ReturnsAsync(new LoginInfo { AccountId = accountId, AccessToken = oldToken, ExpiresAt = expiration }); + + var message = new OpenPlayerInfoMessage(accountId, true); + + await _service.Handle(message, CancellationToken.None); + + MediatorMock.Verify(m => m.Publish( + It.IsAny(), + It.IsAny()), Times.Never); + + MediatorMock.Verify(m => m.Publish( + It.IsAny(), + It.IsAny()), Times.Never); + } + + [Test] + public async Task ShouldNotCallProlongWargamingAccessTokenMessageIfPlayerNotLoffedIn() + { + int accountId = 42; + var message = new OpenPlayerInfoMessage(accountId, false); + + await _service.Handle(message, CancellationToken.None); + + MediatorMock.Verify(m => m.Publish( + It.IsAny(), + It.IsAny()), Times.Never); + + MediatorMock.Verify(m => m.Publish( + It.IsAny(), + It.IsAny()), Times.Never); + } + + [Test] + public async Task ShouldCallRedirectToWgLoginMessageIfTokenIsExpired() + { + int accountId = 42; + string oldToken = "OldTocken"; + var realm = RealmType.Na; + long expiration = Convert.ToInt64((DateTime.Now.AddDays(-1) - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds); + _localStorageMock.Setup(s => s.GetItemAsync(Constants.LoginInfoLocalStorageKey)) + .ReturnsAsync(new LoginInfo { AccountId = accountId, AccessToken = oldToken, ExpiresAt = expiration, Realm = realm }); + + var message = new OpenPlayerInfoMessage(accountId, true); + + await _service.Handle(message, CancellationToken.None); + + + + MediatorMock.Verify(m => m.Publish( + It.Is(m => m.Realm == realm), + It.IsAny()), Times.Once); + + MediatorMock.Verify(m => m.Publish( + It.IsAny(), + It.IsAny()), Times.Never); + } } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Blazor.Tests/Services/WargamingLoginServiceTests.cs b/WotBlitzStatisticsPro.Blazor.Tests/Services/WargamingLoginServiceTests.cs index e1ba634..9090d83 100644 --- a/WotBlitzStatisticsPro.Blazor.Tests/Services/WargamingLoginServiceTests.cs +++ b/WotBlitzStatisticsPro.Blazor.Tests/Services/WargamingLoginServiceTests.cs @@ -96,5 +96,30 @@ namespace WotBlitzStatisticsPro.Blazor.Tests.Services It.Is(m => m.AccountId == accountId && m.IsLoggedIn), It.IsAny()), Times.Once); } + + [Test] + public async Task ShouldCallAppropriateBackendMethodAndSaveNewTokenToLocalStorageWhenProlongWargamingAccessTokenMessageInvoked() + { + // Setup + int accountId = 42; + var realm = RealmType.Na; + string oldToken = "OldToken"; + string newToken = "NewToken"; + long newEvpiration = 129876543; + + _localStorageMock.Setup(s => s.GetItemAsync(Constants.LoginInfoLocalStorageKey)) + .ReturnsAsync(new LoginInfo { AccountId = accountId, AccessToken = oldToken, ExpiresAt = 12345678, Realm = realm }); + _graphQlBackendMock.Setup(s => s.ProlongToken(accountId, oldToken, realm)).ReturnsAsync(new LoginInfo { AccountId = accountId, AccessToken = newToken, ExpiresAt = newEvpiration, Realm = realm }); + + // Avtion + await _service.Handle(new ProlongWargamingAccessToken(), CancellationToken.None); + + // Assert + _graphQlBackendMock.Verify(s => s.ProlongToken(accountId, oldToken, realm), Times.Once); + _localStorageMock.Verify(s => s.SetItemAsync( + Constants.LoginInfoLocalStorageKey, + It.Is(l => l.Realm == realm && l.AccountId == accountId && l.AccessToken == newToken && l.ExpiresAt == newEvpiration)) + , Times.Once); + } } } diff --git a/WotBlitzStatisticsPro.Blazor/Messages/ProlongWargamingAccessToken.cs b/WotBlitzStatisticsPro.Blazor/Messages/ProlongWargamingAccessToken.cs new file mode 100644 index 0000000..b2555d0 --- /dev/null +++ b/WotBlitzStatisticsPro.Blazor/Messages/ProlongWargamingAccessToken.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace WotBlitzStatisticsPro.Blazor.Messages +{ + public class ProlongWargamingAccessToken: INotification + { + public ProlongWargamingAccessToken() + { + } + } +} diff --git a/WotBlitzStatisticsPro.Blazor/Pages/PlayerInfoLayoutBase.cs b/WotBlitzStatisticsPro.Blazor/Pages/PlayerInfoLayoutBase.cs index 567d59f..4945508 100644 --- a/WotBlitzStatisticsPro.Blazor/Pages/PlayerInfoLayoutBase.cs +++ b/WotBlitzStatisticsPro.Blazor/Pages/PlayerInfoLayoutBase.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Localization; using WotBlitzStatisticsPro.Blazor.GraphQl; +using WotBlitzStatisticsPro.Blazor.Model; using WotBlitzStatisticsPro.Blazor.Services; namespace WotBlitzStatisticsPro.Blazor.Pages @@ -30,13 +31,17 @@ namespace WotBlitzStatisticsPro.Blazor.Pages protected override async Task OnInitializedAsync() { + var loggedInInfo = await LocalStorageService.GetItemAsync(Constants.LoginInfoLocalStorageKey); var settings = await LocalStorageService.ReadSettings(); CurrentRealmType = settings.RealmType; - - (AccountInfo, AchievementsBySection) = await GraphQlBackendService.GetPlayerInfo(AccountId, CurrentRealmType); - - await base.OnInitializedAsync(); - + if(loggedInInfo == null) + { + (AccountInfo, AchievementsBySection) = await GraphQlBackendService.GetPlayerInfo(AccountId, CurrentRealmType); + } + else + { + (AccountInfo, AchievementsBySection) = await GraphQlBackendService.CollectPlayerInfo(AccountId, CurrentRealmType, loggedInInfo.AccessToken); + } } } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Blazor/Services/GraphQlBackendService.cs b/WotBlitzStatisticsPro.Blazor/Services/GraphQlBackendService.cs index 692387c..315dde2 100644 --- a/WotBlitzStatisticsPro.Blazor/Services/GraphQlBackendService.cs +++ b/WotBlitzStatisticsPro.Blazor/Services/GraphQlBackendService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using StrawberryShake; using WotBlitzStatisticsPro.Blazor.GraphQl; +using WotBlitzStatisticsPro.Blazor.Model; namespace WotBlitzStatisticsPro.Blazor.Services { @@ -99,5 +100,16 @@ namespace WotBlitzStatisticsPro.Blazor.Services _notificationsService.ReportError("Backend error",string.Join(";", messages)); } } + + public Task ProlongToken(long accountId, string oldToken, RealmType realmType) + { + throw new System.NotImplementedException(); + } + + public Task<(IAccount accountInfo, IReadOnlyList achievementsBySection)> CollectPlayerInfo(long accountId, RealmType realmType, string accessToken) + { + // ToDo: Call mutation with accessToken in the context + throw new System.NotImplementedException(); + } } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Blazor/Services/IGraphQlBackendService.cs b/WotBlitzStatisticsPro.Blazor/Services/IGraphQlBackendService.cs index f9ac999..93387ca 100644 --- a/WotBlitzStatisticsPro.Blazor/Services/IGraphQlBackendService.cs +++ b/WotBlitzStatisticsPro.Blazor/Services/IGraphQlBackendService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using WotBlitzStatisticsPro.Blazor.GraphQl; +using WotBlitzStatisticsPro.Blazor.Model; namespace WotBlitzStatisticsPro.Blazor.Services { @@ -18,6 +19,16 @@ namespace WotBlitzStatisticsPro.Blazor.Services long accountId, RealmType realmType); + Task<(IAccount accountInfo, IReadOnlyList achievementsBySection)> CollectPlayerInfo( + long accountId, + RealmType realmType, + string accessToken); + Task GetWgLoginUrl(RealmType realmType); + + Task ProlongToken( + long accountId, + string oldToken, + RealmType realmType); } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Blazor/Services/Mock/GraphQlBackendMockService.cs b/WotBlitzStatisticsPro.Blazor/Services/Mock/GraphQlBackendMockService.cs index 302f439..22682e7 100644 --- a/WotBlitzStatisticsPro.Blazor/Services/Mock/GraphQlBackendMockService.cs +++ b/WotBlitzStatisticsPro.Blazor/Services/Mock/GraphQlBackendMockService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Bogus; using Bogus.Extensions; using WotBlitzStatisticsPro.Blazor.GraphQl; +using WotBlitzStatisticsPro.Blazor.Model; namespace WotBlitzStatisticsPro.Blazor.Services.Mock { @@ -388,5 +389,14 @@ namespace WotBlitzStatisticsPro.Blazor.Services.Mock return images[faker.Random.Number(images.Length - 1)]; } + public Task ProlongToken(long accountId, string oldToken, RealmType realmType) + { + return Task.FromResult(new LoginInfo { AccountId = accountId, AccessToken = oldToken, Realm = realmType, ExpiresAt = 1631182800, NickName = "Fake nick"}); + } + + public Task<(IAccount accountInfo, IReadOnlyList achievementsBySection)> CollectPlayerInfo(long accountId, RealmType realmType, string accessToken) + { + return GetPlayerInfo(accountId, realmType); + } } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Blazor/Services/NavigationMessagesInterceptor.cs b/WotBlitzStatisticsPro.Blazor/Services/NavigationMessagesInterceptor.cs index 1839d94..be0d7c9 100644 --- a/WotBlitzStatisticsPro.Blazor/Services/NavigationMessagesInterceptor.cs +++ b/WotBlitzStatisticsPro.Blazor/Services/NavigationMessagesInterceptor.cs @@ -1,23 +1,56 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using MediatR; using Microsoft.AspNetCore.Components; using WotBlitzStatisticsPro.Blazor.Messages; +using WotBlitzStatisticsPro.Blazor.Model; namespace WotBlitzStatisticsPro.Blazor.Services { public class NavigationMessagesInterceptor: INotificationHandler { private readonly NavigationManager _navigationManager; + private readonly IMediator _mediator; + private readonly ILocalStorageService _localStorageService; - public NavigationMessagesInterceptor(NavigationManager navigationManager) + public NavigationMessagesInterceptor( + NavigationManager navigationManager, + ILocalStorageService localStorageService, + IMediator mediator) { _navigationManager = navigationManager; + _localStorageService = localStorageService; + _mediator = mediator; } - public Task Handle(OpenPlayerInfoMessage notification, CancellationToken cancellationToken) + public async Task Handle(OpenPlayerInfoMessage notification, CancellationToken cancellationToken) { + if(notification.IsLoggedIn) + { + await CheckExpirationDateAndProlongIfNeeded(); + } _navigationManager.NavigateTo($"/player/{notification.AccountId}"); - return Task.CompletedTask; + } + + private async Task CheckExpirationDateAndProlongIfNeeded() + { + int daysBeforeProlong = 5; + var loggedInData = await _localStorageService.GetItemAsync(Constants.LoginInfoLocalStorageKey); + + if(loggedInData != null) + { + var tokenExpiration = (new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)) + .AddSeconds(loggedInData.ExpiresAt).ToLocalTime(); + var expirationDaysDifference = (tokenExpiration - DateTime.Now).TotalDays; + if(expirationDaysDifference <= 0) + { + await _mediator.Publish(new RedirectToWgLoginMessage(loggedInData.Realm)); + } + if(expirationDaysDifference > 0 && expirationDaysDifference <= daysBeforeProlong) + { + await _mediator.Publish(new ProlongWargamingAccessToken()); + } + } } } } \ No newline at end of file diff --git a/WotBlitzStatisticsPro.Blazor/Services/WargamingLoginService.cs b/WotBlitzStatisticsPro.Blazor/Services/WargamingLoginService.cs index 83eaf44..930f68e 100644 --- a/WotBlitzStatisticsPro.Blazor/Services/WargamingLoginService.cs +++ b/WotBlitzStatisticsPro.Blazor/Services/WargamingLoginService.cs @@ -13,7 +13,8 @@ namespace WotBlitzStatisticsPro.Blazor.Services public class WargamingLoginService: INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler { private readonly IGraphQlBackendService _graphQlBackendService; private readonly DialogService _dialogService; @@ -68,5 +69,14 @@ namespace WotBlitzStatisticsPro.Blazor.Services // Send OpenPlayerInfoMessage with isLoggedIn = true await _mediator.Publish(new OpenPlayerInfoMessage(notification.AccountId, true)); } + + public async Task Handle(ProlongWargamingAccessToken notification, CancellationToken cancellationToken) + { + var loggedInData = await _localStorage.GetItemAsync(Constants.LoginInfoLocalStorageKey); + var newLoginInfo = await _graphQlBackendService.ProlongToken(loggedInData.AccountId, loggedInData.AccessToken, loggedInData.Realm); + loggedInData.AccessToken = newLoginInfo.AccessToken; + loggedInData.ExpiresAt = newLoginInfo.ExpiresAt; + await _localStorage.SetItemAsync(Constants.LoginInfoLocalStorageKey, loggedInData); + } } } \ No newline at end of file