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); - } -}