using Microsoft.Extensions.Configuration; using Moq; using sh.actions.package_cleanup.Models; using sh.actions.package_cleanup.Service; using Xunit; namespace sh.actions.package_cleanup.Tests.Service; public class PackageServiceSemanticVersionTests { private Mock _configurationMock; private Mock _giteaPackageServiceMock; private IPackageService _packageService; private void SetupMocks() { _configurationMock = new Mock(); _giteaPackageServiceMock = new Mock(); _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); } #region Version Detection Tests [Theory] [InlineData("1.0.0.0")] [InlineData("2.5.3.1")] [InlineData("10.20.30.100")] [InlineData("1.0.0")] [InlineData("2.5")] public void FilterPackagesToDelete_WithSemanticVersions_ShouldIdentifyCorrectly(string version) { // Arrange SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Single prod version should not be deleted (CLEAN_PROD_VERSIONS=false by default) Assert.NotNull(result); Assert.Empty(result); } [Theory] [InlineData("1.0.0.0-dev")] [InlineData("2.5.3.1-alpha")] [InlineData("10.20.30.100-beta")] [InlineData("1.0.0-rc1")] [InlineData("2.5-preview")] public void FilterPackagesToDelete_WithSemanticVersionsAndSuffixes_ShouldIdentifyCorrectly(string version) { // Arrange SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Single prerelease/dev should be kept Assert.NotNull(result); Assert.Empty(result); } #endregion #region Prod Release Tests - Semantic Versioning [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithOnlyProdReleases_ShouldReturnEmpty() { // Arrange - CLEAN_PROD_VERSIONS not set, defaults to false SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, new() { Id = 2, Name = "test", Version = "1.1.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, new() { Id = 3, Name = "test", Version = "1.2.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Prod releases should be protected by default (CLEAN_PROD_VERSIONS=false) Assert.Empty(result); Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2 || p.Id == 3); } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithProdReleasesAndCleaningEnabled_ShouldDeleteOldest() { // Arrange SetupMocks(); _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("true"); _configurationMock.Setup(c => c["KEEP_PROD_VERSIONS"]).Returns("2"); var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, new() { Id = 2, Name = "test", Version = "1.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should delete oldest 2 (1.0.0.0 and 1.1.0.0), keep newest 2 (2.0.0.0 and 2.1.0.0) Assert.Equal(2, result.Count); Assert.Contains(result, p => p.Id == 1); Assert.Contains(result, p => p.Id == 2); Assert.DoesNotContain(result, p => p.Id == 3 || p.Id == 4); } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithMultipleProdVersions_ShouldKeepNewest() { // Arrange SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, new() { Id = 2, Name = "test", Version = "1.0.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Default is CLEAN_PROD_VERSIONS=false, so nothing should be deleted Assert.Empty(result); } #endregion #region Prerelease Tests - Semantic Versioning [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithPrereleaseVersions_ShouldKeepLatest() { // Arrange - Default KEEP_PRERELEASE_VERSIONS=3 SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, new() { Id = 2, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, new() { Id = 3, Name = "test", Version = "1.0.0-rc1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, new() { Id = 4, Name = "test", Version = "1.0.0-preview", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should delete oldest 1 (alpha, id=1), keep newest 3 (beta, rc1, preview) Assert.Single(result); Assert.Contains(result, p => p.Id == 1); // alpha is oldest, should be deleted Assert.DoesNotContain(result, p => p.Id == 2 || p.Id == 3 || p.Id == 4); // Keep newest 3 } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithManyPrereleases_ShouldEnforceLimit() { // Arrange - Custom KEEP_PRERELEASE_VERSIONS=2 SetupMocks(); _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); var packages = new List(); for (int i = 0; i < 10; i++) { packages.Add(new GiteaPackage { Id = i, Name = "test", Version = $"1.0.0-beta{i}", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-(10 - i)) }); } // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should delete 8 oldest, keep only 2 newest (Id=8,9) Assert.Equal(8, result.Count); Assert.DoesNotContain(result, p => p.Id >= 8); Assert.Contains(result, p => p.Id < 8); } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithMixedPrereleases_ShouldOrderByVersion() { // Arrange SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, new() { Id = 2, Name = "test", Version = "2.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, new() { Id = 3, Name = "test", Version = "1.5.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Default keep 3, so all should be kept Assert.Empty(result); } #endregion #region Dev Release Tests - Semantic Versioning [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithDevVersions_ShouldKeepLatest() { // Arrange - Default KEEP_DEV_VERSIONS=5 SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, new() { Id = 3, Name = "test", Version = "2.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Default keep 5, so all 3 should be kept Assert.Empty(result); } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldEnforceLimit() { // Arrange - Custom KEEP_DEV_VERSIONS=3 SetupMocks(); _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("3"); var packages = new List(); for (int i = 0; i < 10; i++) { packages.Add(new GiteaPackage { Id = i, Name = "test", Version = $"1.0.{i}-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-i) }); } // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should delete 7 oldest, keep only 3 newest (Id=7,8,9 are newest by CreatedAt) Assert.Equal(7, result.Count); Assert.DoesNotContain(result, p => p.Id >= 7); // Keep newest 3 Assert.Contains(result, p => p.Id < 7); // Delete oldest 7 } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldIdentifyAll() { // Arrange SetupMocks(); var packages = new List(); for (int i = 0; i < 20; i++) { packages.Add(new GiteaPackage { Id = i, Name = "test", Version = $"1.0.{i}-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-i) }); } // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Default keep 5, so 15 should be deleted Assert.Equal(15, result.Count); Assert.DoesNotContain(result, p => p.Id >= 15); // Keep newest 5 (Id 15-19) Assert.Contains(result, p => p.Id < 15); } #endregion #region Mixed Release Types - Semantic Versioning [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithMixedReleaseTypes_ShouldSeparateAndCleanCorrectly() { // Arrange - CLEAN_PROD_VERSIONS=false, KEEP_PRERELEASE_VERSIONS=2, KEEP_DEV_VERSIONS=2 SetupMocks(); _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("2"); var packages = new List { // Prod releases (should not be deleted - CLEAN_PROD_VERSIONS=false) new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, new() { Id = 2, Name = "test", Version = "1.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-25) }, // Prerelease (keep 2 newest) new() { Id = 3, Name = "test", Version = "1.2.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, new() { Id = 4, Name = "test", Version = "1.2.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-15) }, new() { Id = 5, Name = "test", Version = "1.2.1-rc", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, // Dev (keep 2 newest) new() { Id = 6, Name = "test", Version = "1.2.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, new() { Id = 7, Name = "test", Version = "1.2.2-dev", Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert Assert.Equal(1, result.Count); // Should delete 1 prerelease (alpha), 0 dev (keep both) Assert.DoesNotContain(result, p => p.Id <= 2); // Prod not deleted Assert.DoesNotContain(result, p => p.Id >= 6); // Keep 2 latest dev } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithLargeSet_ShouldHandleCorrectly() { // Arrange SetupMocks(); _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("false"); _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("3"); _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("5"); var packages = new List(); // Add prod releases for (int i = 0; i < 5; i++) { packages.Add(new GiteaPackage { Id = i, Name = "test", Version = $"1.{i}.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-(5 - i)) }); } // Add prerelease for (int i = 5; i < 15; i++) { packages.Add(new GiteaPackage { Id = i, Name = "test", Version = $"2.0.{i - 5}-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-(15 - i)) }); } // Add dev for (int i = 15; i < 30; i++) { packages.Add(new GiteaPackage { Id = i, Name = "test", Version = $"2.1.{i - 15}-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-(30 - i)) }); } // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert Assert.NotNull(result); // Prod releases should never be deleted Assert.DoesNotContain(result, p => p.Id < 5); // Should delete 7 oldest prerelease (keep 3 newest) var prereleaseDeleted = result.Where(p => p.Id >= 5 && p.Id < 15).ToList(); Assert.Equal(7, prereleaseDeleted.Count); // Should delete 10 oldest dev (keep 5 newest) var devDeleted = result.Where(p => p.Id >= 15).ToList(); Assert.Equal(10, devDeleted.Count); } #endregion #region Edge Cases - Semantic Versioning [Theory] [InlineData("0.0.0.0")] [InlineData("99.99.99.99")] [InlineData("100.0.0.0")] // Boundary case - might be detected as date version public void FilterPackagesToDeleteBySemanticVersion_WithBoundaryVersions_ShouldHandleCorrectly(string version) { // Arrange SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should not throw, and boundary versions should be handled properly Assert.NotNull(result); // Versions 0.0.0.0 and 99.99.99.99 are semantic, 100.0.0.0 is likely treated as date (will fail date validation) } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithVersionsHavingDifferentLengths_ShouldHandleCorrectly() { // Arrange SetupMocks(); var packages = new List { new() { Id = 1, Name = "test", Version = "1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, new() { Id = 2, Name = "test", Version = "1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, new() { Id = 3, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, new() { Id = 4, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should not throw and should not delete any prod versions Assert.NotNull(result); // CLEAN_PROD_VERSIONS=false by default, so all versions should be kept // Id=1 is invalid (only 1 component), ids 2-4 are valid but prod var validVersions = new[] { 2, 3, 4 }; foreach (var id in validVersions) { Assert.DoesNotContain(result, p => p.Id == id); } } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithZeroKeepCount_ShouldKeepAll() { // Arrange - KEEP_DEV_VERSIONS=0 means keep all SetupMocks(); _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("0"); var packages = new List(); for (int i = 0; i < 10; i++) { packages.Add(new GiteaPackage { Id = i, Name = "test", Version = $"1.0.{i}-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-i) }); } // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should keep all (0 means infinite/keep all) Assert.Empty(result); Assert.DoesNotContain(result, p => p.Id >= 0); // No packages should be deleted } [Fact] public void FilterPackagesToDeleteBySemanticVersion_WithInvalidConfigValues_ShouldUseDefaults() { // Arrange - Set invalid config values SetupMocks(); _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("invalid"); _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("maybe"); var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow }, }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should use defaults (KEEP_DEV_VERSIONS=5, CLEAN_PROD_VERSIONS=false) // With 2 packages and keep 5, no deletion should happen Assert.Empty(result); Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2); } #endregion }