Periodic syncing is @syncStatus.AutoSyncHealthString
- @if (syncStatus.SyncStatus != Common.Database.Status.NotRunning)
+ @if (syncStatus.SyncStatus != Api.Contract.Status.NotRunning)
{
Next Sync: @syncStatus.NextSyncTime
@@ -211,34 +211,34 @@
}
}
- private ThemeColor GetPillThemeColor(Common.Database.Status status)
+ private ThemeColor GetPillThemeColor(Api.Contract.Status status)
{
switch (syncStatus.SyncStatus)
{
- case Common.Database.Status.Running:
+ case Api.Contract.Status.Running:
return ThemeColor.Success;
- case Common.Database.Status.NotRunning:
+ case Api.Contract.Status.NotRunning:
return ThemeColor.Secondary;
- case Common.Database.Status.Dead:
+ case Api.Contract.Status.Dead:
return ThemeColor.Danger;
- case Common.Database.Status.UnHealthy:
+ case Api.Contract.Status.UnHealthy:
return ThemeColor.Warning;
}
return ThemeColor.Light;
}
- private ThemeColor GetPillTextThemeColor(Common.Database.Status status)
+ private ThemeColor GetPillTextThemeColor(Api.Contract.Status status)
{
switch (syncStatus.SyncStatus)
{
- case Common.Database.Status.Running:
+ case Api.Contract.Status.Running:
return ThemeColor.Light;
- case Common.Database.Status.NotRunning:
+ case Api.Contract.Status.NotRunning:
return ThemeColor.Light;
- case Common.Database.Status.Dead:
+ case Api.Contract.Status.Dead:
return ThemeColor.Light;
- case Common.Database.Status.UnHealthy:
+ case Api.Contract.Status.UnHealthy:
return ThemeColor.Dark;
}
diff --git a/src/SharedUI/Shared/AppSettingsForm.razor b/src/SharedUI/Shared/AppSettingsForm.razor
index d0ef9ceb8..f8648ee87 100644
--- a/src/SharedUI/Shared/AppSettingsForm.razor
+++ b/src/SharedUI/Shared/AppSettingsForm.razor
@@ -1,6 +1,8 @@
@inject IApiClient _apiClient
@inject IHxMessengerService _toaster;
+
+
@@ -32,12 +34,15 @@
@code {
private App appSettings;
+ private SettingsGarminGetResponse garminSettings;
private string configDocumentation;
+ private GarminMfaModal? _garminMfaModal;
public AppSettingsForm()
{
var settings = new SettingsGetResponse();
appSettings = settings.App;
+ garminSettings = settings.Garmin;
}
protected override Task OnInitializedAsync()
@@ -63,6 +68,18 @@
_toaster.Clear();
+ if (appSettings.EnablePolling)
+ {
+ // If polling is enabled, check if we need the user to initialize a token
+ await _garminMfaModal!.ShowAsync(ContinueSaveAsync);
+ return;
+ }
+
+ await ContinueSaveAsync();
+ }
+
+ private async Task ContinueSaveAsync()
+ {
try
{
appSettings = await _apiClient.SettingsAppPostAsync(appSettings);
diff --git a/src/SharedUI/Shared/GarminMfaModal.razor b/src/SharedUI/Shared/GarminMfaModal.razor
index 489b56edc..7511bcb4b 100644
--- a/src/SharedUI/Shared/GarminMfaModal.razor
+++ b/src/SharedUI/Shared/GarminMfaModal.razor
@@ -26,23 +26,12 @@
private bool _isErrored = false;
private Func
? _successCallBack;
- protected override async Task OnInitializedAsync()
- {
- var settings = await _apiClient.SettingsGetAsync();
- if (settings.Garmin.Upload && settings.Garmin.TwoStepVerificationEnabled)
- {
- var garminAuthResult = await _apiClient.GetGarminAuthenticationAsync();
- _isAuthenticated = garminAuthResult?.IsAuthenticated ?? false;
- _mfaEnabled = true;
- }
-
- await base.OnInitializedAsync();
- }
-
public async Task ShowAsync(Func? successCallBack = null)
{
using var tracing = Tracing.ClientTrace($"{nameof(GarminMfaModal)}.{nameof(ShowAsync)}", kind: ActivityKind.Client);
+ await InitializeFieldsAsync();
+
_isErrored = false;
_successCallBack = null;
if (_isAuthenticated || !_mfaEnabled)
@@ -126,6 +115,17 @@
}
}
+ private async Task InitializeFieldsAsync()
+ {
+ var settings = await _apiClient.SettingsGetAsync();
+ if (settings.Garmin.Upload && settings.Garmin.TwoStepVerificationEnabled)
+ {
+ var garminAuthResult = await _apiClient.GetGarminAuthenticationAsync();
+ _isAuthenticated = garminAuthResult?.IsAuthenticated ?? false;
+ _mfaEnabled = true;
+ }
+ }
+
protected void OnClosed()
{
_successCallBack = null;
diff --git a/src/Common/Database/SyncStatusDb.cs b/src/Sync/Database/SyncStatusDb.cs
similarity index 67%
rename from src/Common/Database/SyncStatusDb.cs
rename to src/Sync/Database/SyncStatusDb.cs
index 9dc8fc5a9..87b1ead06 100644
--- a/src/Common/Database/SyncStatusDb.cs
+++ b/src/Sync/Database/SyncStatusDb.cs
@@ -1,29 +1,30 @@
-using Common.Observe;
-using JsonFlatFileDataStore;
-using Prometheus;
-using Serilog;
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace Common.Database
-{
- public interface ISyncStatusDb
- {
- Task GetSyncStatusAsync();
+using Common;
+using Common.Database;
+using Common.Observe;
+using JsonFlatFileDataStore;
+using Prometheus;
+using Serilog;
+using Sync.Dto;
+using System;
+using System.Threading.Tasks;
+
+namespace Sync.Database
+{
+ public interface ISyncStatusDb
+ {
+ Task GetSyncStatusAsync();
Task UpsertSyncStatusAsync(SyncServiceStatus status);
- Task DeleteLegacySyncStatusAsync();
- }
-
- public class SyncStatusDb : DbBase, ISyncStatusDb
- {
- private static readonly ILogger _logger = LogContext.ForClass();
-
- private readonly DataStore _db;
- private readonly SyncServiceStatus _defaultSyncServiceStatus = new SyncServiceStatus();
-
- public SyncStatusDb(IFileHandling fileHandling) : base("SyncStatus", fileHandling)
- {
+ Task DeleteLegacySyncStatusAsync();
+ }
+
+ public class SyncStatusDb : DbBase, ISyncStatusDb
+ {
+ private static readonly ILogger _logger = LogContext.ForClass();
+
+ private readonly DataStore _db;
+
+ public SyncStatusDb(IFileHandling fileHandling) : base("SyncStatus", fileHandling)
+ {
_db = new DataStore(DbPath);
Init();
}
@@ -33,14 +34,16 @@ private void Init()
try
{
var settings = _db.GetItem("1");
+
+ if (_db.TryGetItem(1, out var syncStatus))
+ return;
+
+ if (_db.InsertItem("1", new SyncServiceStatus()))
+ return;
}
- catch (KeyNotFoundException)
+ catch (Exception e)
{
- var success = _db.InsertItem("1", _defaultSyncServiceStatus);
- if (!success)
- {
- _logger.Error($"Failed to init default Sync Status to Db for default user.");
- }
+ _logger.Error($"Failed to init default Sync Status to Db for default user.", e);
}
}
@@ -49,47 +52,49 @@ public Task DeleteLegacySyncStatusAsync()
return _db.DeleteItemAsync("syncServiceStatus");
}
- public Task GetSyncStatusAsync()
- {
- using var metrics = DbMetrics.DbActionDuration
- .WithLabels("get", DbName)
- .NewTimer();
- using var tracing = Tracing.Trace($"{nameof(SyncStatusDb)}.{nameof(GetSyncStatusAsync)}", TagValue.Db)
- .WithTable(DbName);
-
- try
- {
- return Task.FromResult(_db.GetItem("1")); // hardcode to admin for now
- }
- catch(KeyNotFoundException k)
- {
- _logger.Verbose("syncServiceStatus key not found in DB for user 1.", k);
- return Task.FromResult(new SyncServiceStatus());
- }
- catch (Exception e)
- {
- _logger.Error(e, "Failed to get syncServiceStatus from db for user 1");
- return Task.FromResult(_defaultSyncServiceStatus);
- }
- }
-
- public Task UpsertSyncStatusAsync(SyncServiceStatus status)
- {
- using var metrics = DbMetrics.DbActionDuration
- .WithLabels("upsert", DbName)
- .NewTimer();
- using var tracing = Tracing.Trace($"{nameof(SyncStatusDb)}.{nameof(UpsertSyncStatusAsync)}", TagValue.Db)
- .WithTable(DbName);
-
- try
- {
- return _db.ReplaceItemAsync("1", status, upsert: true); // hardcode to admin for now
- }
- catch (Exception e)
- {
- _logger.Error(e, "Failed to upsert syncServiceStatus to db for user 1");
- return Task.FromResult(false);
- }
- }
- }
-}
+ public async Task GetSyncStatusAsync()
+ {
+ using var metrics = DbMetrics.DbActionDuration
+ .WithLabels("get", DbName)
+ .NewTimer();
+ using var tracing = Tracing.Trace($"{nameof(SyncStatusDb)}.{nameof(GetSyncStatusAsync)}", TagValue.Db)
+ .WithTable(DbName);
+
+ try
+ {
+ if (_db.TryGetItem(1, out var syncStatus))
+ return syncStatus;
+
+ if (await _db.InsertItemAsync("1", new SyncServiceStatus()))
+ return new SyncServiceStatus();
+
+ _logger.Error("Failed to save default SyncServiceStatus to Sync DB.");
+ return new SyncServiceStatus();
+ }
+ catch (Exception e)
+ {
+ _logger.Error(e, "Failed to get syncServiceStatus from db for user 1");
+ return new SyncServiceStatus();
+ }
+ }
+
+ public Task UpsertSyncStatusAsync(SyncServiceStatus status)
+ {
+ using var metrics = DbMetrics.DbActionDuration
+ .WithLabels("upsert", DbName)
+ .NewTimer();
+ using var tracing = Tracing.Trace($"{nameof(SyncStatusDb)}.{nameof(UpsertSyncStatusAsync)}", TagValue.Db)
+ .WithTable(DbName);
+
+ try
+ {
+ return _db.ReplaceItemAsync("1", status, upsert: true); // hardcode to admin for now
+ }
+ catch (Exception e)
+ {
+ _logger.Error(e, "Failed to upsert syncServiceStatus to db for user 1");
+ return Task.FromResult(false);
+ }
+ }
+ }
+}
diff --git a/src/Common/Database/SyncServiceStatus.cs b/src/Sync/Dto/SyncServiceStatus.cs
similarity index 74%
rename from src/Common/Database/SyncServiceStatus.cs
rename to src/Sync/Dto/SyncServiceStatus.cs
index 8930ba4fb..dd4621a06 100644
--- a/src/Common/Database/SyncServiceStatus.cs
+++ b/src/Sync/Dto/SyncServiceStatus.cs
@@ -1,6 +1,6 @@
using System;
-namespace Common.Database
+namespace Sync.Dto
{
public class SyncServiceStatus
{
@@ -8,10 +8,10 @@ public class SyncServiceStatus
public DateTime? NextSyncTime { get; set; }
public DateTime? LastSuccessfulSyncTime { get; set; }
public Status SyncStatus { get; set; }
- public string LastErrorMessage { get; set; }
+ public string LastErrorMessage { get; set; } = string.Empty;
}
- public enum Status
+ public enum Status : byte
{
NotRunning = 0,
Running = 1,
diff --git a/src/Sync/SyncService.cs b/src/Sync/SyncService.cs
index 16431c45a..4b6b940c3 100644
--- a/src/Sync/SyncService.cs
+++ b/src/Sync/SyncService.cs
@@ -1,5 +1,4 @@
using Common;
-using Common.Database;
using Common.Dto;
using Common.Dto.Peloton;
using Common.Observe;
@@ -11,6 +10,8 @@
using Peloton;
using Prometheus;
using Serilog;
+using Sync.Database;
+using Sync.Dto;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -172,29 +173,38 @@ public async Task SyncAsync(IEnumerable workoutIds, ICollect
}
catch (ArgumentException ae)
{
- _logger.Error(ae, $"Failed to upload to Garmin Connect. {ae.Message}");
+ _logger.Error(ae, $"Sync failed to upload to Garmin Connect. {ae.Message}");
response.SyncSuccess = false;
response.UploadToGarminSuccess = false;
- response.Errors.Add(new ServiceError() { Message = $"Failed to upload workouts to Garmin Connect. {ae.Message}" });
+ response.Errors.Add(new ServiceError() { Message = $"Failed to upload workouts to Garmin Connect. {ae.Message}", Exception = ae });
return response;
}
catch (GarminAuthenticationError gae)
{
- _logger.Error(gae, $"Sync failed to authenticate with Garmin. {gae.Message}");
+ _logger.Error(gae, $"Garmin Uploader failed to authenticate with Garmin. {gae.Message}");
response.SyncSuccess = false;
response.UploadToGarminSuccess = false;
- response.Errors.Add(new ServiceError() { Message = gae.Message });
+ response.Errors.Add(new ServiceError() { Message = gae.Message, Exception = gae });
+ return response;
+ }
+ catch (GarminUploadException gue)
+ {
+ _logger.Error(gue, $"Garmin Uploader failed to upload to Garmin Connect. {gue.Message}");
+
+ response.SyncSuccess = false;
+ response.UploadToGarminSuccess = false;
+ response.Errors.Add(new ServiceError() { Message = gue.Message, Exception = gue });
return response;
}
catch (Exception e)
{
- _logger.Error(e, "Failed to upload workouts to Garmin Connect. You can find the converted files at {@Path} \\n You can manually upload your files to Garmin Connect, or wait for P2G to try again on the next sync job.", settings.App.OutputDirectory);
+ _logger.Error(e, "Unexpected error. Failed to upload workouts to Garmin Connect. You can find the converted files at {@Path} \\n You can manually upload your files to Garmin Connect, or wait for P2G to try again on the next sync job.", settings.App.OutputDirectory);
response.SyncSuccess = false;
response.UploadToGarminSuccess = false;
- response.Errors.Add(new ServiceError() { Message = $"Failed to upload workouts to Garmin Connect. {e.Message}" });
+ response.Errors.Add(new ServiceError() { Message = $"Failed to upload workouts to Garmin Connect. {e.Message}", Exception = e });
return response;
}
finally
diff --git a/src/UnitTests/AdHocTests.cs b/src/UnitTests/AdHocTests.cs
index d96813f21..1dc5a0256 100644
--- a/src/UnitTests/AdHocTests.cs
+++ b/src/UnitTests/AdHocTests.cs
@@ -44,9 +44,12 @@ public void Setup()
.CreateLogger();
// Allows using fiddler
- FlurlHttp.Configure(cli =>
+ FlurlHttp.Clients.WithDefaults(cli =>
{
- cli.HttpClientFactory = new UntrustedCertClientFactory();
+ cli.ConfigureInnerHandler(handler =>
+ {
+ handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
+ });
});
}
@@ -201,7 +204,7 @@ public ConverterInstance(ISettingsService settings, IFileHandling fileHandler) :
public async Task> ConvertForTest(string path, Settings settings)
{
- var workoutData = fileHandler.DeserializeJson(path);
+ var workoutData = fileHandler.DeserializeJsonFile(path);
var converted = await this.ConvertInternalAsync(workoutData, settings);
return converted.Item2;
@@ -209,7 +212,7 @@ public async Task> ConvertForTest(string path, Settings settin
public async Task>> Convert(string path, Settings settings)
{
- var workoutData = fileHandler.DeserializeJson(path);
+ var workoutData = fileHandler.DeserializeJsonFile(path);
var converted = await this.ConvertInternalAsync(workoutData, settings);
return converted;
@@ -219,17 +222,6 @@ public async Task>> Convert(string path, Setting
{
base.Save(data, path);
}
- }
-
- private class UntrustedCertClientFactory : DefaultHttpClientFactory
- {
- public override HttpMessageHandler CreateMessageHandler()
- {
- return new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = (_, _, _, _) => true
- };
- }
}
}
}
diff --git a/src/UnitTests/Api.Service/SettingsUpdaterServiceTests.cs b/src/UnitTests/Api.Service/SettingsUpdaterServiceTests.cs
index cc845daa9..06debffcf 100644
--- a/src/UnitTests/Api.Service/SettingsUpdaterServiceTests.cs
+++ b/src/UnitTests/Api.Service/SettingsUpdaterServiceTests.cs
@@ -5,6 +5,7 @@
using Common.Dto;
using Common.Service;
using FluentAssertions;
+using Garmin.Auth;
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
@@ -27,34 +28,6 @@ public async Task UpdateAppSettingsAsync_With_NullRequest_Returns400()
response.Error.Message.Should().Be("Updated AppSettings must not be null or empty.");
}
- [Test]
- public async Task UpdateAppSettingsAsync_With_EnablePollingWhenGarminMFAEnabled_Throws()
- {
- var autoMocker = new AutoMocker();
- var service = autoMocker.CreateInstance();
- var settingService = autoMocker.GetMock();
- var fileHandler = autoMocker.GetMock();
-
- fileHandler
- .Setup(f => f.DirExists("blah"))
- .Returns(true)
- .Verifiable();
-
- settingService.SetupWithAny>(nameof(settingService.Object.GetSettingsAsync))
- .ReturnsAsync(new Settings() { Garmin = new() { TwoStepVerificationEnabled = true } });
-
- var request = new App()
- {
- EnablePolling = true,
- };
-
- var response = await service.UpdateAppSettingsAsync(request);
-
- response.IsErrored().Should().BeTrue();
- response.Error.Should().NotBeNull();
- response.Error.Message.Should().Be("Automatic Syncing cannot be enabled when Garmin TwoStepVerification is enabled.");
- }
-
[Test]
public async Task UpdatePelotonSettingsAsync_With_NullRequest_ReturnsError()
{
@@ -168,25 +141,64 @@ public async Task GarminPost_With_NullRequest_Returns400()
}
[Test]
- public async Task GarminPost_With_EnableGarminMFAWhenPollingEnabled_Throws()
+ public async Task GarminPost_With_EmailChange_Should_SignOut_of_Garmin()
{
var autoMocker = new AutoMocker();
var service = autoMocker.CreateInstance();
var settingService = autoMocker.GetMock();
- settingService.SetupWithAny>(nameof(settingService.Object.GetSettingsAsync))
- .ReturnsAsync(new Settings() { App = new() { EnablePolling = true } });
+ settingService
+ .SetupWithAny>(nameof(settingService.Object.GetSettingsAsync))
+ .ReturnsAsync(new Settings()
+ {
+ App = new() { EnablePolling = true },
+ Garmin = new () { Email = "ogEmail", Password = "ogPassword" }
+ });
SettingsGarminPostRequest request = new()
{
- Upload = true,
- TwoStepVerificationEnabled = true
+ Email = "newEmail",
};
var response = await service.UpdateGarminSettingsAsync(request);
- response.IsErrored().Should().BeTrue();
- response.Error.Should().NotBeNull();
- response.Error.Message.Should().Be("Garmin TwoStepVerification cannot be enabled while Automatic Syncing is enabled. Please disable Automatic Syncing first.");
+ autoMocker
+ .GetMock()
+ .Verify(x => x.SignOutAsync(), Times.Once);
+
+ response.IsErrored().Should().BeFalse();
+ response.Error.Should().BeNull();
+ response.Successful.Should().BeTrue();
+ }
+
+ [Test]
+ public async Task GarminPost_With_PasswordChange_Should_SignOut_of_Garmin()
+ {
+ var autoMocker = new AutoMocker();
+ var service = autoMocker.CreateInstance();
+ var settingService = autoMocker.GetMock();
+
+ settingService
+ .SetupWithAny>(nameof(settingService.Object.GetSettingsAsync))
+ .ReturnsAsync(new Settings()
+ {
+ App = new() { EnablePolling = true },
+ Garmin = new() { Email = "ogEmail", Password = "ogPassword" }
+ });
+
+ SettingsGarminPostRequest request = new()
+ {
+ Password = "newPassword",
+ };
+
+ var response = await service.UpdateGarminSettingsAsync(request);
+
+ autoMocker
+ .GetMock()
+ .Verify(x => x.SignOutAsync(), Times.Once);
+
+ response.IsErrored().Should().BeFalse();
+ response.Error.Should().BeNull();
+ response.Successful.Should().BeTrue();
}
}
diff --git a/src/UnitTests/Api/Controllers/SyncControllerTests.cs b/src/UnitTests/Api/Controllers/SyncControllerTests.cs
index 67e1a29a9..b70a42d89 100644
--- a/src/UnitTests/Api/Controllers/SyncControllerTests.cs
+++ b/src/UnitTests/Api/Controllers/SyncControllerTests.cs
@@ -1,10 +1,10 @@
using Api.Contract;
using Api.Controllers;
-using Common;
using Common.Dto;
using Common.Service;
-using Common.Stateful;
using FluentAssertions;
+using Garmin.Auth;
+using Garmin.Dto;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
@@ -31,8 +31,9 @@ public async Task SyncAsync_With_NullRequest_Returns400()
settings.SetupWithAny>(nameof(settings.Object.GetSettingsAsync))
.ReturnsAsync(new Settings());
- settings.SetupWithAny(nameof(settings.Object.GetGarminAuthentication))
- .Returns((GarminApiAuthentication)null);
+ var garminAuthService = autoMocker.GetMock();
+ garminAuthService.SetupWithAny>(nameof(garminAuthService.Object.GetGarminAuthenticationAsync))
+ .ReturnsAsync((GarminApiAuthentication)null);
var response = await controller.SyncAsync(null);
@@ -52,8 +53,9 @@ public async Task SyncAsync_With_DefaultRequest_Returns400()
settings.SetupWithAny>(nameof(settings.Object.GetSettingsAsync))
.ReturnsAsync(new Settings());
- settings.SetupWithAny(nameof(settings.Object.GetGarminAuthentication))
- .Returns((GarminApiAuthentication)null);
+ var garminAuthService = autoMocker.GetMock();
+ garminAuthService.SetupWithAny>(nameof(garminAuthService.Object.GetGarminAuthenticationAsync))
+ .ReturnsAsync((GarminApiAuthentication)null);
var request = new SyncPostRequest();
@@ -75,8 +77,9 @@ public async Task SyncAsync_With_EmptyWorkoutIdsRequest_Returns400()
settings.SetupWithAny>(nameof(settings.Object.GetSettingsAsync))
.ReturnsAsync(new Settings());
- settings.SetupWithAny(nameof(settings.Object.GetGarminAuthentication))
- .Returns((GarminApiAuthentication)null);
+ var garminAuthService = autoMocker.GetMock();
+ garminAuthService.SetupWithAny>(nameof(garminAuthService.Object.GetGarminAuthenticationAsync))
+ .ReturnsAsync((GarminApiAuthentication)null);
var request = new SyncPostRequest() { WorkoutIds = new List() };
@@ -98,8 +101,9 @@ public async Task SyncAsync_WhenGarminMfaEnabled_AndNoAuthTokenYet_Returns401()
settings.SetupWithAny>(nameof(settings.Object.GetSettingsAsync))
.ReturnsAsync(new Settings() { Garmin = new() { Upload = true, TwoStepVerificationEnabled = true } });
- settings.SetupWithAny(nameof(settings.Object.GetGarminAuthentication))
- .Returns((GarminApiAuthentication)null);
+ var garminAuthService = autoMocker.GetMock();
+ garminAuthService.SetupWithAny>(nameof(garminAuthService.Object.GetGarminAuthenticationAsync))
+ .ReturnsAsync((GarminApiAuthentication)null);
var request = new SyncPostRequest() { WorkoutIds = new List() { "someId" } };
diff --git a/src/UnitTests/Common/Database/DbMigrationsTests.cs b/src/UnitTests/Common/Database/DbMigrationsTests.cs
index 830cb5c5b..c77e6b108 100644
--- a/src/UnitTests/Common/Database/DbMigrationsTests.cs
+++ b/src/UnitTests/Common/Database/DbMigrationsTests.cs
@@ -7,6 +7,7 @@
using Moq;
using Moq.AutoMock;
using NUnit.Framework;
+using Sync.Database;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -64,7 +65,6 @@ public async Task MigrateToAdminUserAsync_When_LegacySettings_Migrates()
settingsDb.Verify();
settingsDb.Verify(x => x.RemoveLegacySettingsAsync(), Times.Once);
usersDb.Verify(x => x.GetUsersAsync(), Times.Once);
- mocker.GetMock().Verify(x => x.DeleteLegacySyncStatusAsync(), Times.Once);
}
[Test]
@@ -96,6 +96,5 @@ public async Task MigrateToAdminUserAsync_When_FailsToMigrate_DoesNotThrow()
settingsDb.Verify();
settingsDb.Verify(x => x.RemoveLegacySettingsAsync(), Times.Never);
usersDb.Verify(x => x.GetUsersAsync(), Times.Once);
- mocker.GetMock().Verify(x => x.DeleteLegacySyncStatusAsync(), Times.Once);
}
}
diff --git a/src/UnitTests/Common/Helpers/WorkoutHelperTests.cs b/src/UnitTests/Common/Helpers/WorkoutHelperTests.cs
index f6e3501a9..bee2e6740 100644
--- a/src/UnitTests/Common/Helpers/WorkoutHelperTests.cs
+++ b/src/UnitTests/Common/Helpers/WorkoutHelperTests.cs
@@ -33,6 +33,7 @@ public string GetTitle_Should_ReplaceInvalidChars(string title, string instructo
[TestCase("My Title", "é", ExpectedResult = "My_Title_with_é")]
[TestCase("My Title", "ä", ExpectedResult = "My_Title_with_ä")]
+ [TestCase("My Title", "&", ExpectedResult = "My_Title_with_&")]
public string GetTitle_Should_Handle_SpecialChars(string title, string instructor)
{
var workout = new Workout()
diff --git a/src/UnitTests/Conversion/FitConverterTests.cs b/src/UnitTests/Conversion/FitConverterTests.cs
index 12ee6050d..c2be1a7c4 100644
--- a/src/UnitTests/Conversion/FitConverterTests.cs
+++ b/src/UnitTests/Conversion/FitConverterTests.cs
@@ -180,7 +180,7 @@ public ConverterInstance(ISettingsService settings) : base(settings, null) { }
public async Task> ConvertForTest(string path, Settings settings)
{
- var workoutData = fileHandler.DeserializeJson(path);
+ var workoutData = fileHandler.DeserializeJsonFile(path);
var converted = await this.ConvertInternalAsync(workoutData, settings);
return converted.Item2;
diff --git a/src/UnitTests/Peloton/PelotonServiceTests.cs b/src/UnitTests/Peloton/PelotonServiceTests.cs
index 9b2501f3f..5c77b7b68 100644
--- a/src/UnitTests/Peloton/PelotonServiceTests.cs
+++ b/src/UnitTests/Peloton/PelotonServiceTests.cs
@@ -36,30 +36,30 @@ public async Task GetWorkoutDetailsAsync_Should_EnrichAllWorkouts()
var pelotonApi = autoMocker.GetMock();
pelotonApi.Setup(x => x.GetWorkoutByIdAsync("1"))
- .ReturnsAsync(JObject.FromObject(new Workout() { Ride = new Ride() { Id = "12" } }))
+ .ReturnsAsync(new Workout() { Ride = new Ride() { Id = "12" } })
.Verifiable();
pelotonApi.Setup(x => x.GetWorkoutSamplesByIdAsync("1"))
- .ReturnsAsync(new JObject())
+ .ReturnsAsync(new WorkoutSamples())
.Verifiable();
pelotonApi.Setup(x => x.GetWorkoutByIdAsync("2"))
- .ReturnsAsync(JObject.FromObject(new Workout() { Ride = new Ride() { Id = "22" } }))
+ .ReturnsAsync(new Workout() { Ride = new Ride() { Id = "22" } })
.Verifiable();
pelotonApi.Setup(x => x.GetWorkoutSamplesByIdAsync("2"))
- .ReturnsAsync(new JObject())
+ .ReturnsAsync(new WorkoutSamples())
.Verifiable();
pelotonApi.Setup(x => x.GetWorkoutByIdAsync("3"))
- .ReturnsAsync(JObject.FromObject(new Workout() { Ride = new Ride() { Id = "32" } }))
+ .ReturnsAsync(new Workout() { Ride = new Ride() { Id = "32" } })
.Verifiable();
pelotonApi.Setup(x => x.GetWorkoutSamplesByIdAsync("3"))
- .ReturnsAsync(new JObject())
- .Verifiable();
-
- // ACT
+ .ReturnsAsync(new WorkoutSamples())
+ .Verifiable();
+
+ // ACT
await pelotonService.GetWorkoutDetailsAsync(new List()
{
new Workout() { Status = "COMPLETE", Id = "1" },
@@ -122,7 +122,7 @@ public async Task GetWorkoutDetailsAsync_Should_Only_Enrich_ValidRideIds(string
var pelotonApi = autoMocker.GetMock();
pelotonApi.Setup(x => x.GetWorkoutByIdAsync("someWorkoutId"))
- .ReturnsAsync(JObject.FromObject(new Workout() { Ride = new Ride() { Id = rideId } }));
+ .ReturnsAsync(new Workout() { Ride = new Ride() { Id = rideId } });
var workouts = await pelotonService.GetWorkoutDetailsAsync("someWorkoutId");
diff --git a/src/UnitTests/Sync/SyncServiceTests.cs b/src/UnitTests/Sync/SyncServiceTests.cs
index 2004f6190..90f4288ee 100644
--- a/src/UnitTests/Sync/SyncServiceTests.cs
+++ b/src/UnitTests/Sync/SyncServiceTests.cs
@@ -1,5 +1,4 @@
using Common;
-using Common.Database;
using Common.Dto;
using Common.Dto.Peloton;
using Common.Service;
@@ -13,6 +12,8 @@
using Peloton;
using Philosowaffle.Capability.ReleaseChecks.Model;
using Sync;
+using Sync.Database;
+using Sync.Dto;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj
index be96e9894..b382f2677 100644
--- a/src/UnitTests/UnitTests.csproj
+++ b/src/UnitTests/UnitTests.csproj
@@ -8,12 +8,12 @@
-
-
+
+
-
-
-
+
+
+
diff --git a/src/WebUI/WebUI.csproj b/src/WebUI/WebUI.csproj
index 5e13f8642..77e69bb1c 100644
--- a/src/WebUI/WebUI.csproj
+++ b/src/WebUI/WebUI.csproj
@@ -24,8 +24,8 @@
-
-
+
+
diff --git a/vNextReleaseNotes.md b/vNextReleaseNotes.md
index 155b8e472..49ebb3752 100644
--- a/vNextReleaseNotes.md
+++ b/vNextReleaseNotes.md
@@ -3,7 +3,9 @@
## Features
--
+- [#585]
+ - Garmin Authentication now saves and refreshes tokens. Users using MFA will now only need provide their MFA code once.
+ - For those running via Docker, automatic syncing now works for MFA users after you have entered your code the first time.
## Fixes
@@ -14,15 +16,15 @@
- Console
- `console-stable`
- `console-latest`
- - `console-v4.X.0`
+ - `console-v4.3.0`
- `console-v4`
- Api
- `api-stable`
- `api-latest`
- - `api-v4.X.0`
+ - `api-v4.3.0`
- `api-v4`
- WebUI
- `webui-stable`
- `webui-latest`
- - `webui-v4.X.0`
+ - `webui-v4.3.0`
- `webui-v4`