diff --git a/Rubberduck.Core/Properties/Settings.Designer.cs b/Rubberduck.Core/Properties/Settings.Designer.cs index 017f2c6c14..90eeb2cf96 100644 --- a/Rubberduck.Core/Properties/Settings.Designer.cs +++ b/Rubberduck.Core/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Rubberduck.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.6.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/Rubberduck.Core/Rubberduck.Core.csproj b/Rubberduck.Core/Rubberduck.Core.csproj index 9c4dc78bb9..cda42e9170 100644 --- a/Rubberduck.Core/Rubberduck.Core.csproj +++ b/Rubberduck.Core/Rubberduck.Core.csproj @@ -93,4 +93,17 @@ 2.0.20525 + + + True + True + Settings.settings + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + diff --git a/Rubberduck.Core/Settings/GeneralSettings.cs b/Rubberduck.Core/Settings/GeneralSettings.cs index aac14af9fa..f9b9664735 100644 --- a/Rubberduck.Core/Settings/GeneralSettings.cs +++ b/Rubberduck.Core/Settings/GeneralSettings.cs @@ -13,6 +13,7 @@ public interface IGeneralSettings DisplayLanguageSetting Language { get; set; } bool CanShowSplash { get; set; } bool CanCheckVersion { get; set; } + string ApiBaseUrl { get; set; } bool IncludePreRelease { get; set; } bool CompileBeforeParse { get; set; } bool IsSmartIndenterPrompted { get; set; } @@ -45,6 +46,7 @@ public DisplayLanguageSetting Language public bool CanShowSplash { get; set; } public bool CanCheckVersion { get; set; } + public string ApiBaseUrl { get; set; } public bool IncludePreRelease { get; set; } public bool CompileBeforeParse { get; set; } public bool IsSmartIndenterPrompted { get; set; } @@ -103,7 +105,8 @@ public bool Equals(GeneralSettings other) EnableExperimentalFeatures.Count == other.EnableExperimentalFeatures.Count && EnableExperimentalFeatures.All(other.EnableExperimentalFeatures.Contains) && SetDpiUnaware == other.SetDpiUnaware && - EnableFolderDragAndDrop == other.EnableFolderDragAndDrop; + EnableFolderDragAndDrop == other.EnableFolderDragAndDrop && + ApiBaseUrl == other.ApiBaseUrl; } } } \ No newline at end of file diff --git a/Rubberduck.Core/UI/Command/VersionCheckCommand.cs b/Rubberduck.Core/UI/Command/VersionCheckCommand.cs index ed6fd27e6c..1a00fc42f5 100644 --- a/Rubberduck.Core/UI/Command/VersionCheckCommand.cs +++ b/Rubberduck.Core/UI/Command/VersionCheckCommand.cs @@ -53,40 +53,31 @@ protected override async void OnExecute(object parameter) Logger.Info("Executing version check..."); var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - await _versionCheck - .GetLatestVersionAsync(settings, tokenSource.Token) - .ContinueWith(t => - { - if (t.IsFaulted) - { - Logger.Warn(t.Exception); - return; - } + var latest = await _versionCheck.GetLatestVersionAsync(settings, tokenSource.Token); - if (_versionCheck.CurrentVersion < t.Result) - { - var proceed = true; - if (_versionCheck.IsDebugBuild || !settings.IncludePreRelease) - { - // if the latest version has a revision number and isn't a pre-release build, - // avoid prompting since we can't know if the build already includes the latest version. - proceed = t.Result.Revision == 0; - } + if (_versionCheck.CurrentVersion < latest) + { + var proceed = true; + if (_versionCheck.IsDebugBuild || !settings.IncludePreRelease) + { + // if the latest version has a revision number and isn't a pre-release build, + // avoid prompting since we can't know if the build already includes the latest version. + proceed = latest.Revision == 0; + } - if (proceed) - { - PromptAndBrowse(t.Result, settings.IncludePreRelease); - } - else - { - Logger.Info("Version check skips notification of an existing newer version available."); - } - } - else - { - Logger.Info("Version check completed: running current latest."); - } - }); + if (proceed) + { + PromptAndBrowse(latest, settings.IncludePreRelease); + } + else + { + Logger.Info("Version check skips notification of an existing newer version available."); + } + } + else if (latest != default) + { + Logger.Info("Version check completed: running current latest."); + } } private void PromptAndBrowse(Version latestVersion, bool includePreRelease) diff --git a/Rubberduck.Core/UI/Splash2021.cs b/Rubberduck.Core/UI/Splash2021.cs index 227f33d439..f9bce49444 100644 --- a/Rubberduck.Core/UI/Splash2021.cs +++ b/Rubberduck.Core/UI/Splash2021.cs @@ -1,6 +1,5 @@ using System.Drawing; using System.Windows.Forms; -using Rubberduck.VersionCheck; namespace Rubberduck.UI { @@ -11,9 +10,9 @@ public Splash2021() InitializeComponent(); } - public Splash2021(IVersionCheckService versionCheck) : this() + public Splash2021(string versionString) : this() { - VersionLabel.Text = string.Format(Resources.RubberduckUI.Rubberduck_AboutBuild, versionCheck.VersionString); + VersionLabel.Text = string.Format(Resources.RubberduckUI.Rubberduck_AboutBuild, versionString); VersionLabel.Parent = pictureBox1; VersionLabel.BackColor = Color.Transparent; } diff --git a/Rubberduck.Core/VersionCheck/ApiClientBase.cs b/Rubberduck.Core/VersionCheck/ApiClientBase.cs index 54d5b8c220..f94f5f588e 100644 --- a/Rubberduck.Core/VersionCheck/ApiClientBase.cs +++ b/Rubberduck.Core/VersionCheck/ApiClientBase.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Rubberduck.Settings; using System; using System.Net; using System.Net.Http; @@ -9,40 +10,69 @@ namespace Rubberduck.Client.Abstract { - public abstract class ApiClientBase : IDisposable + public interface IHttpClientProvider + { + HttpClient GetClient(); + } + + public sealed class HttpClientProvider : IHttpClientProvider, IDisposable + { + private readonly Lazy _client; + + public HttpClientProvider(Func getClient) + { + _client = new Lazy(getClient); + } + + public HttpClient GetClient() + { + return _client.Value; + } + + public void Dispose() + { + if (_client.IsValueCreated) + { + _client.Value.Dispose(); + } + } + } + + public abstract class ApiClientBase { protected static readonly string UserAgentName = "Rubberduck"; - protected static readonly string BaseUrl = "https://api.rubberduckvba.com/api/v1/"; protected static readonly string ContentTypeApplicationJson = "application/json"; protected static readonly int MaxAttempts = 3; protected static readonly TimeSpan RetryCooldownDelay = TimeSpan.FromSeconds(1); - protected readonly Lazy _client; + protected readonly IHttpClientProvider _clientProvider; + protected readonly string _baseUrl; - protected ApiClientBase() + protected ApiClientBase(IGeneralSettings settings, IHttpClientProvider clientProvider) { - _client = new Lazy(() => GetClient()); + _clientProvider = clientProvider; + _baseUrl = string.IsNullOrWhiteSpace(settings.ApiBaseUrl) ? "https://api.rubberduckvba.com/api/v1/" : settings.ApiBaseUrl; } protected HttpClient GetClient() { ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; - var client = new HttpClient(); - return ConfigureClient(client); + var client = _clientProvider.GetClient(); + ConfigureClient(client); + return client; } - protected virtual HttpClient ConfigureClient(HttpClient client) + protected virtual void ConfigureClient(HttpClient client) { var userAgentVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(3); var userAgentHeader = new ProductInfoHeaderValue(UserAgentName, userAgentVersion); client.DefaultRequestHeaders.UserAgent.Add(userAgentHeader); - return client; } protected virtual async Task GetResponse(string route, CancellationToken? cancellationToken = null) { - var uri = new Uri($"{BaseUrl}{route}"); + var uri = new Uri($"{_baseUrl}{route}"); var attempt = 0; var token = cancellationToken ?? CancellationToken.None; @@ -102,7 +132,7 @@ protected virtual async Task GetResponse(string route, Cancell protected virtual async Task Post(string route, TArgs args, CancellationToken? cancellationToken = null) { - var uri = new Uri($"{BaseUrl}{route}"); + var uri = new Uri($"{_baseUrl}{route}"); string json; try { @@ -167,13 +197,5 @@ protected virtual async Task Post(string route, TArgs a return default; } } - - public void Dispose() - { - if (_client.IsValueCreated) - { - _client.Value.Dispose(); - } - } } } \ No newline at end of file diff --git a/Rubberduck.Core/VersionCheck/PublicApiClient.cs b/Rubberduck.Core/VersionCheck/PublicApiClient.cs index 7f19450852..fc6fcdab48 100644 --- a/Rubberduck.Core/VersionCheck/PublicApiClient.cs +++ b/Rubberduck.Core/VersionCheck/PublicApiClient.cs @@ -1,16 +1,26 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Rubberduck.Client.Abstract; +using Rubberduck.Settings; namespace Rubberduck.VersionCheck { - public class PublicApiClient : ApiClientBase + public interface IPublicApiClient + { + Task> GetLatestTagsAsync(CancellationToken token); + } + + public class PublicApiClient : ApiClientBase, IPublicApiClient { private static readonly string PublicTagsEndPoint = "public/tags"; + public PublicApiClient(IGeneralSettings settings, IHttpClientProvider clientProvider) + : base(settings, clientProvider) + { + } + public async Task> GetLatestTagsAsync(CancellationToken token) { return await GetResponse(PublicTagsEndPoint, token); diff --git a/Rubberduck.Core/VersionCheck/VersionCheckService.cs b/Rubberduck.Core/VersionCheck/VersionCheckService.cs index 2d2e0ab86d..3ce72f66a7 100644 --- a/Rubberduck.Core/VersionCheck/VersionCheckService.cs +++ b/Rubberduck.Core/VersionCheck/VersionCheckService.cs @@ -11,11 +11,15 @@ namespace Rubberduck.VersionCheck public class VersionCheckService : IVersionCheckService { private static readonly ILogger _logger = LogManager.GetCurrentClassLogger(); + private readonly IPublicApiClient _client; /// That would be the version of the assembly for the _Extension class. - public VersionCheckService(Version version) + /// + public VersionCheckService(Version version, IPublicApiClient client) { CurrentVersion = version; + _client = client; + #if DEBUG IsDebugBuild = true; #endif @@ -32,9 +36,9 @@ public async Task GetLatestVersionAsync(GeneralSettings settings, Cance return _latestVersion; } - using (var client = new PublicApiClient()) + try { - var tags = await client.GetLatestTagsAsync(token); + var tags = await _client.GetLatestTagsAsync(token); var next = tags.Single(e => e.IsPreRelease); var main = tags.Single(e => !e.IsPreRelease); @@ -42,9 +46,13 @@ public async Task GetLatestVersionAsync(GeneralSettings settings, Cance _latestVersion = settings.IncludePreRelease ? next.Version : main.Version; _logger.Info($"Check prerelease: {settings.IncludePreRelease}; latest: v{_latestVersion.ToString(4)}"); + } + catch ( Exception ex ) + { + _logger.Warn(ex, "Version check failed."); - return _latestVersion; } + return _latestVersion; } public Version CurrentVersion { get; } diff --git a/Rubberduck.Core/app.config b/Rubberduck.Core/app.config index b2fcaac379..3bb8bfca4b 100644 --- a/Rubberduck.Core/app.config +++ b/Rubberduck.Core/app.config @@ -221,6 +221,7 @@ 0 false + https://api.rubberduckvba.com/api/v1/ diff --git a/Rubberduck.Main/Extension.cs b/Rubberduck.Main/Extension.cs index 0adfc3b543..4475bcc04f 100644 --- a/Rubberduck.Main/Extension.cs +++ b/Rubberduck.Main/Extension.cs @@ -194,7 +194,7 @@ private void InitializeAddIn() if (_initialSettings?.CanShowSplash ?? false) { - splash = new Splash2021(new VersionCheckService(typeof(Splash2021).Assembly.GetName().Version)); + splash = new Splash2021(string.Format(RubberduckUI.Rubberduck_AboutBuild, Assembly.GetExecutingAssembly().GetName().Version.ToString(3))); splash.Show(); splash.Refresh(); } diff --git a/RubberduckTests/VersionCheckTests.cs b/RubberduckTests/VersionCheckTests.cs index fe9fb39ac1..be0f27bc9c 100644 --- a/RubberduckTests/VersionCheckTests.cs +++ b/RubberduckTests/VersionCheckTests.cs @@ -1,5 +1,7 @@ using System; using System.Threading; +using System.Threading.Tasks; +using System.Web; using Moq; using NUnit.Framework; using Rubberduck.Interaction; @@ -10,6 +12,24 @@ namespace RubberduckTests { + [TestFixture] + public class VersionCheckServiceTests + { + [Test] + public async Task GetLatestVersionThrowsHttpException_IsHandled() + { + var appVersion = new Version(); + var apiClient = new Mock(); + apiClient.Setup(m => m.GetLatestTagsAsync(It.IsAny())).Throws(); + + var sut = new VersionCheckService(appVersion, apiClient.Object); + + var result = await sut.GetLatestVersionAsync(new GeneralSettings(), CancellationToken.None); + + Assert.IsNull(result); + } + } + [TestFixture] public class VersionCheckTests { @@ -34,12 +54,12 @@ private VersionCheckCommand CreateSUT(Configuration config, Version currentVersi mockConfig.Setup(m => m.Read()).Returns(() => config); mockService = new Mock(); - + mockService.Setup(m => m.CurrentVersion) .Returns(() => currentVersion); mockService.Setup(m => m.GetLatestVersionAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(() => latestVersion); + .ReturnsAsync(() => latestVersion); return new VersionCheckCommand(mockService.Object, mockPrompt.Object, mockProcess.Object, mockConfig.Object); }