Skip to content

Commit e23e96b

Browse files
feat: add startup app update check flow
Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com>
1 parent d8ed918 commit e23e96b

14 files changed

Lines changed: 337 additions & 15 deletions

File tree

TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ConfigurationServiceTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public async Task ConfigurationService_GetConfiguration_ResultSuccess_And_Config
3535

3636
ConfigurationResponse expectedConfiguration = new ConfigurationResponse
3737
{
38+
ApplicationUpdateUri = "http://localhost:9210",
3839
ClientId = "clientId",
3940
ClientSecret = Guid.NewGuid().ToString(),
4041
EnableAutoUpdates = false,
@@ -62,6 +63,7 @@ public async Task ConfigurationService_GetConfiguration_ResultSuccess_And_Config
6263
configurationResult.Data.ShouldNotBeNull();
6364
configurationResult.Data.ClientSecret.ShouldBe(expectedConfiguration.ClientSecret);
6465
configurationResult.Data.ClientId.ShouldBe(expectedConfiguration.ClientId);
66+
configurationResult.Data.ApplicationUpdateUri.ShouldBe(expectedConfiguration.ApplicationUpdateUri);
6567
configurationResult.Data.EnableAutoUpdates.ShouldBe(expectedConfiguration.EnableAutoUpdates);
6668
configurationResult.Data.SecurityServiceUri.ShouldBe(expectedConfiguration.HostAddresses.Single(s => s.ServiceType == ServiceType.Security).Uri);
6769
configurationResult.Data.TransactionProcessorAclUri.ShouldBe(expectedConfiguration.HostAddresses.Single(s => s.ServiceType == ServiceType.TransactionProcessorAcl).Uri);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Newtonsoft.Json;
2+
using RichardSzalay.MockHttp;
3+
using Shouldly;
4+
using SimpleResults;
5+
using TransactionProcessor.Mobile.BusinessLogic.Logging;
6+
using TransactionProcessor.Mobile.BusinessLogic.Models;
7+
using TransactionProcessor.Mobile.BusinessLogic.Services;
8+
9+
namespace TransactionProcessor.Mobile.BusinessLogic.Tests.ServicesTests;
10+
11+
public class UpdateServiceTests
12+
{
13+
private readonly MockHttpMessageHandler MockHttpMessageHandler;
14+
15+
private readonly IUpdateService UpdateService;
16+
17+
public UpdateServiceTests()
18+
{
19+
this.MockHttpMessageHandler = new MockHttpMessageHandler();
20+
this.UpdateService = new UpdateService(_ => "http://localhost", this.MockHttpMessageHandler.ToHttpClient());
21+
}
22+
23+
[Fact]
24+
public async Task UpdateService_CheckForUpdates_ResultSuccess_And_UpdateResponseReturned()
25+
{
26+
Logger.Initialise(new NullLogger());
27+
28+
ApplicationUpdateCheckResponse expectedResponse = new()
29+
{
30+
DownloadUri = "https://updates.example.com/transactionmobile.apk",
31+
LatestVersion = "1.0.1",
32+
Message = "Install the latest version.",
33+
UpdateRequired = true
34+
};
35+
36+
this.MockHttpMessageHandler.When("http://localhost/api/applicationupdates/check")
37+
.Respond("application/json", JsonConvert.SerializeObject(expectedResponse));
38+
39+
Result<ApplicationUpdateCheckResponse> updateResult = await this.UpdateService.CheckForUpdates(TestData.ApplicationVersion,
40+
"com.transactionprocessor.mobile",
41+
"Android",
42+
TestData.DeviceIdentifier,
43+
CancellationToken.None);
44+
45+
updateResult.IsSuccess.ShouldBeTrue();
46+
updateResult.Data.ShouldNotBeNull();
47+
updateResult.Data.UpdateRequired.ShouldBeTrue();
48+
updateResult.Data.DownloadUri.ShouldBe(expectedResponse.DownloadUri);
49+
updateResult.Data.LatestVersion.ShouldBe(expectedResponse.LatestVersion);
50+
}
51+
52+
[Fact]
53+
public async Task UpdateService_CheckForUpdates_FailedHttpCall_ResultFailed()
54+
{
55+
Logger.Initialise(new NullLogger());
56+
57+
this.MockHttpMessageHandler.When("http://localhost/api/applicationupdates/check")
58+
.Respond(System.Net.HttpStatusCode.BadRequest);
59+
60+
Result<ApplicationUpdateCheckResponse> updateResult = await this.UpdateService.CheckForUpdates(TestData.ApplicationVersion,
61+
"com.transactionprocessor.mobile",
62+
"Android",
63+
TestData.DeviceIdentifier,
64+
CancellationToken.None);
65+
66+
updateResult.IsFailed.ShouldBeTrue();
67+
}
68+
}

TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,26 @@ public class LoginPageViewModelTests
2828

2929
private readonly Mock<IApplicationInfoService> ApplicationInfoService;
3030

31+
private readonly Mock<IApplicationUpdateLauncherService> ApplicationUpdateLauncherService;
32+
3133
private readonly Mock<IDialogService> DialogService;
34+
35+
private readonly Mock<IUpdateService> UpdateService;
3236
public LoginPageViewModelTests() {
3337
this.Mediator = new Mock<IMediator>();
3438
this.NavigationService = new Mock<INavigationService>();
3539
this.NavigationParameterService = new Mock<INavigationParameterService>();
3640
this.ApplicationCache = new Mock<IApplicationCache>();
3741
this.DeviceService = new Mock<IDeviceService>();
3842
this.ApplicationInfoService = new Mock<IApplicationInfoService>();
43+
this.ApplicationUpdateLauncherService = new Mock<IApplicationUpdateLauncherService>();
3944
this.DialogService = new Mock<IDialogService>();
45+
this.UpdateService = new Mock<IUpdateService>();
4046

4147
this.ViewModel = new LoginPageViewModel(this.Mediator.Object, this.NavigationService.Object, this.ApplicationCache.Object,
4248
this.DeviceService.Object, this.ApplicationInfoService.Object,
43-
this.DialogService.Object, this.NavigationParameterService.Object);
49+
this.DialogService.Object, this.NavigationParameterService.Object,
50+
this.UpdateService.Object, this.ApplicationUpdateLauncherService.Object);
4451
Logger.Initialise(new Logging.NullLogger());
4552
}
4653

@@ -127,6 +134,63 @@ public void LoginPageViewModel_LoginCommand_Execute_ErrorGettingToken_WarningToa
127134
CancellationToken.None), Times.Once);
128135
}
129136

137+
[Fact]
138+
public void LoginPageViewModel_LoginCommand_Execute_UpdateCheckFails_LogonContinues()
139+
{
140+
this.Mediator.Setup(m => m.Send(It.IsAny<GetConfigurationRequest>(), It.IsAny<CancellationToken>()))
141+
.ReturnsAsync(Result.Success(new Configuration { EnableAutoUpdates = true }));
142+
this.Mediator.Setup(m => m.Send(It.IsAny<LoginRequest>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Success(TestData.AccessToken));
143+
this.Mediator.Setup(m => m.Send(It.IsAny<LogonTransactionRequest>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Success(TestData.PerformLogonResponseModel));
144+
this.Mediator.Setup(m => m.Send(It.IsAny<GetContractProductsRequest>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Success(TestData.ContractProductList));
145+
this.Mediator.Setup(m => m.Send(It.IsAny<GetMerchantBalanceRequest>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Success(TestData.MerchantBalance));
146+
this.ApplicationInfoService.Setup(a => a.VersionString).Returns(TestData.ApplicationVersion);
147+
this.ApplicationInfoService.Setup(a => a.PackageName).Returns("com.transactionprocessor.mobile");
148+
this.DeviceService.Setup(d => d.GetPlatform()).Returns("Android");
149+
this.DeviceService.Setup(d => d.GetIdentifier()).Returns(TestData.DeviceIdentifier);
150+
this.UpdateService.Setup(u => u.CheckForUpdates(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<CancellationToken>()))
151+
.ReturnsAsync(Result.Failure("Update check failed"));
152+
153+
this.ViewModel.LogonCommand.Execute(null);
154+
155+
this.UpdateService.Verify(u => u.CheckForUpdates(TestData.ApplicationVersion,
156+
"com.transactionprocessor.mobile",
157+
"Android",
158+
TestData.DeviceIdentifier,
159+
It.IsAny<CancellationToken>()), Times.Once);
160+
this.NavigationService.Verify(n => n.GoToHome(), Times.Once);
161+
}
162+
163+
[Fact]
164+
public void LoginPageViewModel_LoginCommand_Execute_UpdateRequired_UpdateLauncherIsCalled_And_LogonStops()
165+
{
166+
this.Mediator.Setup(m => m.Send(It.IsAny<GetConfigurationRequest>(), It.IsAny<CancellationToken>()))
167+
.ReturnsAsync(Result.Success(new Configuration { EnableAutoUpdates = true }));
168+
this.ApplicationInfoService.Setup(a => a.VersionString).Returns(TestData.ApplicationVersion);
169+
this.ApplicationInfoService.Setup(a => a.PackageName).Returns("com.transactionprocessor.mobile");
170+
this.DeviceService.Setup(d => d.GetPlatform()).Returns("Android");
171+
this.DeviceService.Setup(d => d.GetIdentifier()).Returns(TestData.DeviceIdentifier);
172+
this.UpdateService.Setup(u => u.CheckForUpdates(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<CancellationToken>()))
173+
.ReturnsAsync(Result.Success(new ApplicationUpdateCheckResponse
174+
{
175+
DownloadUri = "https://updates.example.com/transactionmobile.apk",
176+
LatestVersion = "1.0.1",
177+
Message = "Install update",
178+
UpdateRequired = true
179+
}));
180+
this.DialogService.Setup(d => d.ShowDialog(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>())).ReturnsAsync(true);
181+
182+
this.ViewModel.LogonCommand.Execute(null);
183+
184+
this.ApplicationUpdateLauncherService.Verify(l => l.LaunchUpdateAsync("https://updates.example.com/transactionmobile.apk", It.IsAny<CancellationToken>()), Times.Once);
185+
this.Mediator.Verify(x => x.Send(It.IsAny<LoginRequest>(), It.IsAny<CancellationToken>()), Times.Never);
186+
this.NavigationService.Verify(n => n.GoToHome(), Times.Never);
187+
this.DialogService.Verify(n => n.ShowWarningToast(It.Is<String>(message => message.Contains("update", StringComparison.OrdinalIgnoreCase)),
188+
null,
189+
"OK",
190+
null,
191+
CancellationToken.None), Times.Once);
192+
}
193+
130194
[Fact]
131195
public void LoginPageViewModel_LoginCommand_Execute_ErrorDuringLogonTransaction_WarningToastIsShown()
132196
{
@@ -196,4 +260,4 @@ public void LoginPageViewModel_PropertyTests_ValuesAreAsExpected(){
196260
this.ViewModel.UseTrainingMode.ShouldBeTrue();
197261
this.ViewModel.DeviceIdentifier.ShouldBe("testidentifier");
198262
}
199-
}
263+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace TransactionProcessor.Mobile.BusinessLogic.Models;
4+
5+
[ExcludeFromCodeCoverage]
6+
public class ApplicationUpdateCheckResponse
7+
{
8+
public Boolean UpdateRequired { get; set; }
9+
10+
public String? DownloadUri { get; set; }
11+
12+
public String? LatestVersion { get; set; }
13+
14+
public String? Message { get; set; }
15+
}

TransactionProcessor.Mobile.BusinessLogic/Models/TokenResponseModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class Configuration
3131

3232
public String EstateReportingUri { get; set; }
3333

34+
public String ApplicationUpdateUri { get; set; }
35+
3436
public LogLevel LogLevel { get; set; }
3537

3638
public Boolean EnableAutoUpdates { get; set; }

TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public async Task<Result<Configuration>> GetConfiguration(String deviceIdentifie
7676
response = new Configuration() {
7777
ClientSecret = apiResponse.ClientSecret,
7878
ClientId = apiResponse.ClientId,
79+
ApplicationUpdateUri = apiResponse.ApplicationUpdateUri,
7980
EnableAutoUpdates = apiResponse.EnableAutoUpdates,
8081
SecurityServiceUri = apiResponse.HostAddresses.Single(h => h.ServiceType == ServiceType.Security).Uri,
8182
TransactionProcessorAclUri =
@@ -125,4 +126,4 @@ public async Task PostDiagnosticLogs(String deviceIdentifier,
125126

126127
await this.HandleResponse(httpResponse, cancellationToken);
127128
}
128-
}
129+
}

TransactionProcessor.Mobile.BusinessLogic/Services/DataTransferObjects/ConfigurationResponse.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ public class ConfigurationResponse
1212

1313
public bool EnableAutoUpdates { get; set; }
1414

15+
public string ApplicationUpdateUri { get; set; }
16+
1517
public List<HostAddress> HostAddresses { get; set; }
1618

1719
public string Id { get; set; }
1820

1921
public LoggingLevel LogLevel { get; set; }
20-
}
22+
}

TransactionProcessor.Mobile.BusinessLogic/Services/TrainingModeServices/TrainingConfigurationService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class TrainingConfigurationService : IConfigurationService
1010
public async Task<Result<Configuration>> GetConfiguration(String deviceIdentifier,
1111
CancellationToken cancellationToken) {
1212
return Result.Success(new Configuration {
13+
ApplicationUpdateUri = String.Empty,
1314
ClientId = "dummyClientId",
1415
ClientSecret = "dummyClientSecret",
1516
EnableAutoUpdates = false,
@@ -27,4 +28,4 @@ public async Task PostDiagnosticLogs(String deviceIdentifier,
2728
CancellationToken cancellationToken) {
2829
// Do nothing
2930
}
30-
}
31+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System.Text;
2+
using Newtonsoft.Json;
3+
using SimpleResults;
4+
using TransactionProcessor.Mobile.BusinessLogic.Models;
5+
6+
namespace TransactionProcessor.Mobile.BusinessLogic.Services;
7+
8+
public interface IUpdateService
9+
{
10+
Task<Result<ApplicationUpdateCheckResponse>> CheckForUpdates(String applicationVersion,
11+
String packageName,
12+
String platform,
13+
String deviceIdentifier,
14+
CancellationToken cancellationToken);
15+
}
16+
17+
public class UpdateService : ClientProxyBase.ClientProxyBase, IUpdateService
18+
{
19+
private readonly Func<String, String> BaseAddressResolver;
20+
21+
public UpdateService(Func<String, String> baseAddressResolver,
22+
HttpClient httpClient) : base(httpClient)
23+
{
24+
this.BaseAddressResolver = baseAddressResolver;
25+
}
26+
27+
public async Task<Result<ApplicationUpdateCheckResponse>> CheckForUpdates(String applicationVersion,
28+
String packageName,
29+
String platform,
30+
String deviceIdentifier,
31+
CancellationToken cancellationToken)
32+
{
33+
String requestUri = this.BuildRequestUrl("/api/applicationupdates/check");
34+
var request = new
35+
{
36+
ApplicationVersion = applicationVersion,
37+
PackageName = packageName,
38+
Platform = platform,
39+
DeviceIdentifier = deviceIdentifier
40+
};
41+
42+
try
43+
{
44+
Logger.LogInformation($"About to check for application updates for device identifier {deviceIdentifier}");
45+
Logger.LogDebug($"Application update request details: Uri {requestUri}");
46+
47+
StringContent content = new(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
48+
HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, content, cancellationToken);
49+
Logger.LogDebug($"Application update response [{httpResponse.StatusCode}]");
50+
51+
String responseContent = await this.HandleResponse(httpResponse, cancellationToken);
52+
Logger.LogDebug($"Application update response content [{responseContent}]");
53+
54+
ApplicationUpdateCheckResponse? response = JsonConvert.DeserializeObject<ApplicationUpdateCheckResponse>(responseContent);
55+
56+
if (response == null)
57+
{
58+
return Result.Failure("Application update check did not return a valid response.");
59+
}
60+
61+
Logger.LogInformation($"Application update check for device identifier {deviceIdentifier} completed successfully");
62+
return Result.Success(response);
63+
}
64+
catch (Exception ex)
65+
{
66+
Logger.LogError($"Error checking for application updates for device identifier {deviceIdentifier} {ex.Message}.", ex);
67+
return ResultExtensions.FailureExtended($"Error checking for application updates for device identifier {deviceIdentifier}", ex);
68+
}
69+
}
70+
71+
private String BuildRequestUrl(String route)
72+
{
73+
String baseAddress = this.BaseAddressResolver("ApplicationUpdateServiceUrl");
74+
75+
return $"{baseAddress.TrimEnd('/')}{route}";
76+
}
77+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace TransactionProcessor.Mobile.BusinessLogic.UIServices;
2+
3+
public interface IApplicationUpdateLauncherService
4+
{
5+
Task LaunchUpdateAsync(String downloadUri,
6+
CancellationToken cancellationToken = default);
7+
}

0 commit comments

Comments
 (0)