From 81a233ce13f978937f9d4a4e8aefce277156c48a Mon Sep 17 00:00:00 2001
From: Vitor M <4777793+vgmello@users.noreply.github.com>
Date: Tue, 14 Nov 2023 00:13:29 +0000
Subject: [PATCH] Self app upgrade feature (#12)
Enable support for app self upgrade
---
.github/workflows/release.yml | 5 +-
.../Ellosoft.AwsCredentialsManager.csproj | 2 +-
.../Infrastructure/FileDownloadService.cs | 48 +++++
.../Infrastructure/SemanticVersion.cs | 142 +++++++++++++++
.../Upgrade/Models/GitHubRelease.cs | 26 +++
.../Models/GithubSourceGenerationContext.cs | 12 ++
.../Infrastructure/Upgrade/UpgradeService.cs | 170 ++++++++++++++++++
src/Ellosoft.AwsCredentialsManager/Program.cs | 7 +-
.../Services/AppMetadata.cs | 36 ++++
...llosoft.AwsCredentialsManager.Tests.csproj | 14 +-
.../SemanticVersionTests.cs | 101 +++++++++++
.../UnitTest1.cs | 12 --
12 files changed, 554 insertions(+), 21 deletions(-)
create mode 100644 src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs
create mode 100644 src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs
create mode 100644 src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GitHubRelease.cs
create mode 100644 src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GithubSourceGenerationContext.cs
create mode 100644 src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs
create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs
create mode 100644 test/Ellosoft.AwsCredentialsManager.Tests/SemanticVersionTests.cs
delete mode 100644 test/Ellosoft.AwsCredentialsManager.Tests/UnitTest1.cs
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 37dc362..ca3f548 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,9 +23,10 @@ jobs:
type: "zip"
directory: win-output/
path: aws-cred-mgr.exe
- filename: "aws-cred-mgr-win-x64.zip"
+ filename: aws-cred-mgr-${{ github.ref_name }}-win-x64.zip
- name: Create Release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
- files: win-output/aws-cred-mgr-win-x64.zip
+ prerelease: ${{ contains(github.ref_name, 'beta') }}
+ files: win-output/aws-cred-mgr-${{ github.ref_name }}-win-x64.zip
diff --git a/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj b/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj
index 5c33db2..cf7c059 100644
--- a/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj
+++ b/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj
@@ -36,7 +36,7 @@
https://github.com/ellosoft/aws-cred-mgr
https://github.com/ellosoft/aws-cred-mgr
git
- 0.0.1
+ 0.0.1-alpha-4
false
diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs
new file mode 100644
index 0000000..3de4faa
--- /dev/null
+++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs
@@ -0,0 +1,48 @@
+// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
+
+namespace Ellosoft.AwsCredentialsManager.Infrastructure;
+
+public interface IFileDownloadService
+{
+ Task DownloadFileAsync(HttpClient httpClient, string downloadUrl, Stream destinationStream);
+}
+
+public class FileDownloadService : IFileDownloadService
+{
+ public Task DownloadFileAsync(HttpClient httpClient, string downloadUrl, Stream destinationStream)
+ {
+ return AnsiConsole.Progress()
+ .HideCompleted(true)
+ .AutoClear(true)
+ .StartAsync(async ctx =>
+ {
+ var startingTask = ctx.AddTask("Starting download").IsIndeterminate();
+
+ using var httpResponse = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
+
+ httpResponse.EnsureSuccessStatusCode();
+
+ startingTask.StopTask();
+
+ var totalBytes = httpResponse.Content.Headers.ContentLength ?? throw new InvalidOperationException("Invalid download size");
+
+ var downloadTask = ctx.AddTask("Downloading", new ProgressTaskSettings { MaxValue = Convert.ToDouble(totalBytes) });
+
+ await using var downloadStream = await httpResponse.Content.ReadAsStreamAsync();
+
+ var buffer = new byte[16384];
+ int bytesRead;
+
+ while ((bytesRead = await downloadStream.ReadAsync(buffer)) > 0)
+ {
+ await destinationStream.WriteAsync(buffer, 0, bytesRead);
+ downloadTask.Increment(bytesRead);
+ }
+
+ await destinationStream.FlushAsync();
+ destinationStream.Position = 0;
+
+ downloadTask.StopTask();
+ });
+ }
+}
diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs
new file mode 100644
index 0000000..6c6430f
--- /dev/null
+++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs
@@ -0,0 +1,142 @@
+// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Ellosoft.AwsCredentialsManager.Infrastructure;
+
+public record SemanticVersion : IComparable
+{
+ private Version MainVersion { get; }
+
+ private string? PreReleaseLabel { get; }
+
+ private int PreReleaseNumber { get; }
+
+ private string? BuildData { get; }
+
+ public bool IsPreRelease => PreReleaseLabel is not null;
+
+ public SemanticVersion(string versionValue)
+ {
+ if (!TryParse(versionValue, out var mainVersion, out var preReleaseLabel, out var preReleaseNumber, out var buildData))
+ throw new ArgumentException($"'{versionValue}' is not a valid SemanticVersion");
+
+ MainVersion = mainVersion;
+ PreReleaseLabel = preReleaseLabel;
+ PreReleaseNumber = preReleaseNumber;
+ BuildData = buildData;
+ }
+
+ private SemanticVersion(Version mainVersion, string? preReleaseLabel, int preReleaseNumber, string? buildData)
+ {
+ MainVersion = mainVersion;
+ PreReleaseLabel = preReleaseLabel;
+ PreReleaseNumber = preReleaseNumber;
+ BuildData = buildData;
+ }
+
+ public int CompareTo(SemanticVersion? other)
+ {
+ if (other is null)
+ return 1;
+
+ var mainVersionComparison = MainVersion.CompareTo(other.MainVersion);
+
+ if (mainVersionComparison != 0)
+ return mainVersionComparison;
+
+ var isPreRelease = !string.IsNullOrEmpty(PreReleaseLabel);
+ var otherIsPreRelease = !string.IsNullOrEmpty(other.PreReleaseLabel);
+
+ if (isPreRelease && !otherIsPreRelease)
+ return -1;
+
+ if (!isPreRelease && otherIsPreRelease)
+ return 1;
+
+ var preReleaseLabelComparison = StringComparer.OrdinalIgnoreCase.Compare(PreReleaseLabel, other.PreReleaseLabel);
+
+ if (preReleaseLabelComparison != 0)
+ return preReleaseLabelComparison;
+
+ return PreReleaseNumber.CompareTo(other.PreReleaseNumber);
+ }
+
+ public override string ToString()
+ {
+ var result = MainVersion.ToString();
+
+ if (PreReleaseLabel is not null)
+ {
+ result += $"-{PreReleaseLabel}";
+
+ if (PreReleaseNumber != 0)
+ result += $".{PreReleaseNumber}";
+ }
+
+ if (BuildData is not null)
+ result += $"+{BuildData}";
+
+ return result;
+ }
+
+ public static bool operator <(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) < 0;
+
+ public static bool operator >(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) > 0;
+
+ public static bool operator <=(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) <= 0;
+
+ public static bool operator >=(SemanticVersion left, SemanticVersion right) => left.CompareTo(right) >= 0;
+
+ public static bool TryParse(string? versionValue, [NotNullWhen(true)] out SemanticVersion? version)
+ {
+ version = null;
+
+ if (!TryParse(versionValue, out var mainVersion, out var preReleaseLabel, out var preReleaseNumber, out var buildData))
+ return false;
+
+ version = new SemanticVersion(mainVersion, preReleaseLabel, preReleaseNumber, buildData);
+
+ return true;
+ }
+
+ private static bool TryParse(string? versionValue,
+ [NotNullWhen(true)] out Version? mainVersion, out string? preReleaseLabel, out int preReleaseNumber, out string? buildData)
+ {
+ mainVersion = null;
+ preReleaseLabel = null;
+ preReleaseNumber = 0;
+ buildData = null;
+
+ if (versionValue is null)
+ return false;
+
+ var buildDataIndex = versionValue.IndexOf('+');
+
+ if (buildDataIndex > 0)
+ {
+ buildData = versionValue[(buildDataIndex + 1)..];
+ versionValue = versionValue[..buildDataIndex];
+ }
+
+ var preReleaseIndex = versionValue.IndexOf('-');
+
+ if (preReleaseIndex > 0)
+ {
+ preReleaseLabel = versionValue[(preReleaseIndex + 1)..];
+ versionValue = versionValue[..preReleaseIndex];
+
+ var preReleaseNumberIndex = preReleaseLabel.IndexOf('.');
+
+ if (preReleaseNumberIndex > 0)
+ {
+ if (!int.TryParse(preReleaseLabel[(preReleaseNumberIndex + 1)..], out preReleaseNumber))
+ return false;
+
+ preReleaseLabel = preReleaseLabel[..preReleaseNumberIndex];
+ }
+ }
+
+ return Version.TryParse(versionValue, out mainVersion);
+ }
+}
diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GitHubRelease.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GitHubRelease.cs
new file mode 100644
index 0000000..480e060
--- /dev/null
+++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GitHubRelease.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade.Models;
+
+public class GitHubRelease
+{
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("html_url")]
+ public required string Url { get; set; }
+
+ [JsonPropertyName("prerelease")]
+ public bool PreRelease { get; set; }
+
+ [JsonPropertyName("assets")]
+ public ICollection Assets { get; set; } = Array.Empty();
+
+ public class ReleaseAsset
+ {
+ [JsonPropertyName("browser_download_url")]
+ public required string DownloadUrl { get; set; }
+ }
+}
diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GithubSourceGenerationContext.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GithubSourceGenerationContext.cs
new file mode 100644
index 0000000..a2077a0
--- /dev/null
+++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/Models/GithubSourceGenerationContext.cs
@@ -0,0 +1,12 @@
+// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade.Models;
+
+[JsonSourceGenerationOptions]
+[JsonSerializable(typeof(GitHubRelease))]
+[JsonSerializable(typeof(List))]
+internal partial class GithubSourceGenerationContext : JsonSerializerContext
+{
+}
diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs
new file mode 100644
index 0000000..adf395f
--- /dev/null
+++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs
@@ -0,0 +1,170 @@
+// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.IO.Compression;
+using System.Net.Http.Json;
+using System.Runtime.InteropServices;
+using Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade.Models;
+using Ellosoft.AwsCredentialsManager.Services;
+using Serilog;
+
+namespace Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade;
+
+#pragma warning disable S1075 // URIs should not be hardcoded
+
+public class UpgradeService
+{
+ private const string GITHUB_RELEASES_URL = "https://api.github.com/repos/ellosoft/aws-cred-mgr/releases";
+ private const string GITHUB_LATEST_RELEASE_URL = "https://api.github.com/repos/ellosoft/aws-cred-mgr/releases/latest";
+
+ private readonly ILogger _logger;
+ private readonly IAnsiConsole _console;
+ private readonly IAppMetadata _appMetadata;
+ private readonly IFileDownloadService _downloadService;
+ private readonly HttpClient _httpClient;
+
+ public UpgradeService(ILogger logger) : this(
+ logger,
+ AnsiConsole.Console,
+ new AppMetadata(),
+ new FileDownloadService(),
+ CreateHttpClient())
+ {
+ }
+
+ public UpgradeService(
+ ILogger logger,
+ IAnsiConsole console,
+ IAppMetadata appMetadata,
+ IFileDownloadService downloadService,
+ HttpClient httpClient)
+ {
+ _logger = logger.ForContext();
+ _console = console;
+ _appMetadata = appMetadata;
+ _downloadService = downloadService;
+ _httpClient = httpClient;
+ }
+
+ public async Task TryUpgradeApp()
+ {
+ try
+ {
+ var currentAppVersion = _appMetadata.GetAppVersion();
+
+ if (currentAppVersion is null)
+ return;
+
+ var latestRelease = await GetLatestRelease(currentAppVersion);
+
+ if (!TryGetDownloadAssetAndVersion(latestRelease, out var downloadUrl, out var latestVersion) || latestVersion <= currentAppVersion)
+ return;
+
+ var shouldUpdate = CheckIfUserWantsToUpdate(currentAppVersion, latestRelease);
+
+ if (!shouldUpdate)
+ return;
+
+ using var zipStream = new MemoryStream();
+ await _downloadService.DownloadFileAsync(_httpClient, downloadUrl, zipStream);
+
+ var (executablePath, appFolder) = _appMetadata.GetExecutablePath();
+
+ var executableName = Path.GetFileName(executablePath);
+ var newFile = Path.Combine(appFolder, executableName + ".new");
+ var archivePath = Path.Combine(Path.GetTempPath(), executableName + ".old");
+
+ _console.MarkupLine("\r\nInstalling upgrade...");
+
+ ExtractApp(zipStream, newFile);
+ UpgradeApp(executablePath, archivePath, newFile);
+
+ _console.MarkupLine("[green]Upgrade complete! The changes will reflect next time you execute the application.\r\n[/]");
+ }
+ catch (Exception e)
+ {
+ _logger.Error(e, "Unable to upgrade app");
+ _console.MarkupLine("[yellow]Unable to upgrade app, try again later or " +
+ "download the new version from https://github.com/ellosoft/aws-cred-mgr/releases [/]");
+ }
+ }
+
+ private async Task GetLatestRelease(SemanticVersion currentAppVersion)
+ {
+ if (!currentAppVersion.IsPreRelease)
+ return await _httpClient.GetFromJsonAsync(GITHUB_LATEST_RELEASE_URL, GithubSourceGenerationContext.Default.GitHubRelease);
+
+ var releases = await _httpClient.GetFromJsonAsync(GITHUB_RELEASES_URL, GithubSourceGenerationContext.Default.ListGitHubRelease);
+
+ return releases?.Find(r => r.PreRelease);
+ }
+
+ private bool CheckIfUserWantsToUpdate(SemanticVersion currentAppVersion, GitHubRelease latestRelease)
+ {
+ var preReleaseTag = latestRelease.PreRelease ? "(Pre-release)" : String.Empty;
+
+ _console.MarkupLine(
+ $"""
+ New version available:
+ [b]Current Version:[/] {currentAppVersion}
+ [b]New Version:[/] {latestRelease.Name} {preReleaseTag}
+ [b]Release Notes:[/] {latestRelease.Url}
+
+ """);
+
+ return _console.Confirm("[yellow]Do you want to upgrade now ?[/]");
+ }
+
+ private static bool TryGetDownloadAssetAndVersion(
+ [NotNullWhen(true)] GitHubRelease? latestRelease,
+ [NotNullWhen(true)] out string? downloadUrl,
+ [NotNullWhen(true)] out SemanticVersion? version)
+ {
+ downloadUrl = null;
+ version = null;
+
+ if (latestRelease is null || latestRelease.Assets.Count == 0)
+ return false;
+
+ if (!SemanticVersion.TryParse(latestRelease.Name, out version))
+ return false;
+
+ var fileSuffix = $"{RuntimeInformation.RuntimeIdentifier}.zip";
+
+ downloadUrl = latestRelease
+ .Assets.FirstOrDefault(a => a.DownloadUrl.EndsWith(fileSuffix))?.DownloadUrl;
+
+ return downloadUrl is not null;
+ }
+
+ private static void ExtractApp(Stream zipStream, string newFile)
+ {
+ using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
+
+ var mainAppEntry = archive.Entries.FirstOrDefault(e => e.Name == "aws-cred-mgr.exe");
+
+ if (mainAppEntry is null)
+ throw new InvalidOperationException("Unable to find application file in release archive");
+
+ mainAppEntry.ExtractToFile(newFile, overwrite: true);
+ }
+
+ private static void UpgradeApp(string executablePath, string archivePath, string newFile)
+ {
+ if (File.Exists(archivePath))
+ File.Delete(archivePath);
+
+ File.Move(executablePath, archivePath);
+ File.Move(newFile, executablePath);
+ }
+
+ private static HttpClient CreateHttpClient()
+ {
+ const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0";
+
+ var httpClient = new HttpClient();
+ httpClient.DefaultRequestHeaders.Add("User-Agent", USER_AGENT);
+
+ return httpClient;
+ }
+}
diff --git a/src/Ellosoft.AwsCredentialsManager/Program.cs b/src/Ellosoft.AwsCredentialsManager/Program.cs
index 955f7a0..28c1a0e 100644
--- a/src/Ellosoft.AwsCredentialsManager/Program.cs
+++ b/src/Ellosoft.AwsCredentialsManager/Program.cs
@@ -7,6 +7,7 @@
using Ellosoft.AwsCredentialsManager.Commands.RDS;
using Ellosoft.AwsCredentialsManager.Infrastructure.Cli;
using Ellosoft.AwsCredentialsManager.Infrastructure.Logging;
+using Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade;
using Ellosoft.AwsCredentialsManager.Services.AWS;
using Ellosoft.AwsCredentialsManager.Services.AWS.Interactive;
using Ellosoft.AwsCredentialsManager.Services.Configuration;
@@ -18,6 +19,9 @@
var logger = LogRegistration.CreateNewLogger();
+var upgradeService = new UpgradeService(logger);
+await upgradeService.TryUpgradeApp();
+
var services = new ServiceCollection()
.SetupLogging(logger)
.AddSingleton()
@@ -38,7 +42,8 @@
.AddSingleton()
.AddSingleton();
-services.AddKeyedSingleton(nameof(OktaHttpClientFactory), OktaHttpClientFactory.CreateHttpClient());
+services
+ .AddKeyedSingleton(nameof(OktaHttpClientFactory), OktaHttpClientFactory.CreateHttpClient());
var registrar = new TypeRegistrar(services);
var app = new CommandApp(registrar);
diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs b/src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs
new file mode 100644
index 0000000..8bef6ba
--- /dev/null
+++ b/src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs
@@ -0,0 +1,36 @@
+// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
+
+using System.Reflection;
+using Ellosoft.AwsCredentialsManager.Infrastructure;
+
+namespace Ellosoft.AwsCredentialsManager.Services;
+
+public interface IAppMetadata
+{
+ public SemanticVersion? GetAppVersion();
+
+ (string executablePath, string appFolder) GetExecutablePath();
+}
+
+public class AppMetadata : IAppMetadata
+{
+ public SemanticVersion? GetAppVersion()
+ {
+ var versionValue = Assembly.GetEntryAssembly()?
+ .GetCustomAttribute()?
+ .InformationalVersion;
+
+ return SemanticVersion.TryParse(versionValue, out var semanticVersion) ? semanticVersion : null;
+ }
+
+ public (string executablePath, string appFolder) GetExecutablePath()
+ {
+ var executablePath = Environment.ProcessPath;
+ var appFolder = Path.GetDirectoryName(executablePath);
+
+ if (executablePath is null || appFolder is null)
+ throw new InvalidOperationException("Unable to acquire executable path or location");
+
+ return (executablePath, appFolder);
+ }
+}
diff --git a/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj b/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj
index 50346a6..1b73163 100644
--- a/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj
+++ b/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj
@@ -8,15 +8,15 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -26,4 +26,8 @@
+
+
+
+
diff --git a/test/Ellosoft.AwsCredentialsManager.Tests/SemanticVersionTests.cs b/test/Ellosoft.AwsCredentialsManager.Tests/SemanticVersionTests.cs
new file mode 100644
index 0000000..99fcb9d
--- /dev/null
+++ b/test/Ellosoft.AwsCredentialsManager.Tests/SemanticVersionTests.cs
@@ -0,0 +1,101 @@
+// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
+
+using Ellosoft.AwsCredentialsManager.Infrastructure;
+using FluentAssertions;
+
+namespace Ellosoft.AwsCredentialsManager.Tests;
+
+public class SemanticVersionTests
+{
+ [Theory]
+ [InlineData("0.0.1")]
+ [InlineData("0.0.1-alpha")]
+ [InlineData("0.0.1-alpha.1")]
+ [InlineData("0.0.1-alpha.1+data")]
+ [InlineData("0.0.1.1+data")]
+ [InlineData("0.0.1.1")]
+ [InlineData("0.0.1-alpha-test")]
+ [InlineData("0.0.1-alpha-test.1")]
+ public void TryParse_Valid_Tests(string versionValue)
+ {
+ var result = SemanticVersion.TryParse(versionValue, out var actualVersion);
+
+ Assert.True(result);
+ Assert.Equal(versionValue, actualVersion!.ToString());
+ }
+
+ [Theory]
+ [InlineData("0.0.1.0.1")]
+ [InlineData("0.0.1.-")]
+ [InlineData("0.0.1-alpha.0.1")]
+ public void TryParse_Invalid_Tests(string versionValue)
+ {
+ var result = SemanticVersion.TryParse(versionValue, out var actualVersion);
+
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData("0.0.1", "0.0.2", -1)]
+ [InlineData("0.0.1", "0.0.1", 0)]
+ [InlineData("0.0.2", "0.0.1", 1)]
+ [InlineData("0.0.1", "0.0.1-alpha", 1)]
+ [InlineData("0.0.1-alpha", "0.0.1-beta", -1)]
+ [InlineData("0.0.1-alpha", "0.0.1-beta.2", -1)]
+ [InlineData("0.0.1-alpha.1", "0.0.1-alpha.2", -1)]
+ [InlineData("0.0.1-alpha", "0.0.1-alpha.1", -1)]
+ [InlineData("0.0.1-alpha.1", "0.0.1-alpha.1", 0)]
+ [InlineData("0.0.1+data", "0.0.1+data.1", 0)]
+ [InlineData("0.0.1-alpha.1+data", "0.0.1-alpha.1", 0)]
+ public void CompareTo_Tests(string version1, string version2, int expectedResult)
+ {
+ SemanticVersion.TryParse(version1, out var v1);
+ SemanticVersion.TryParse(version2, out var v2);
+
+ var result = v1!.CompareTo(v2);
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void OrderBy_WhenVersionsAreUnsorted_ShouldSortVersions()
+ {
+ var unsortedData = new[]
+ {
+ "0.1",
+ "0.0.1",
+ "0.0.2",
+ "1.0.1",
+ "0.0.1-alpha",
+ "0.0.1-beta",
+ "10.0.1",
+ "0.0.1-alpha.1"
+ }.Select(v => new SemanticVersion(v));
+
+ var sortedData = unsortedData.Order().ToList();
+
+ var expectedResult = new[]
+ {
+ "0.0.1-alpha",
+ "0.0.1-alpha.1",
+ "0.0.1-beta",
+ "0.0.1",
+ "0.0.2",
+ "0.1",
+ "1.0.1",
+ "10.0.1"
+ }.Select(v => new SemanticVersion(v));
+
+ sortedData.Should().BeInAscendingOrder();
+ sortedData.Should().Equal(expectedResult);
+ }
+
+ [Theory]
+ [InlineData("0.0.1", "0.0.1")]
+ [InlineData("0.0.1-alpha.1+data.1", "0.0.1-alpha.1+data.1")]
+ public void Equal_Tests(string v1, string v2)
+ {
+ var result = new SemanticVersion(v1) == new SemanticVersion(v2);
+ Assert.True(result);
+ }
+}
diff --git a/test/Ellosoft.AwsCredentialsManager.Tests/UnitTest1.cs b/test/Ellosoft.AwsCredentialsManager.Tests/UnitTest1.cs
deleted file mode 100644
index a90e5c1..0000000
--- a/test/Ellosoft.AwsCredentialsManager.Tests/UnitTest1.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2023 Ellosoft Limited. All rights reserved.
-
-namespace Ellosoft.AwsCredentialsManager.Tests;
-
-public class UnitTest1
-{
- [Fact]
- public void Test1()
- {
- Assert.True(true);
- }
-}