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 PackageServiceVersionDetectionTests { private readonly IPackageService _packageService; private readonly Mock _configurationMock; private readonly Mock _giteaPackageServiceMock; public PackageServiceVersionDetectionTests() { _configurationMock = new Mock(); _giteaPackageServiceMock = new Mock(); _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); } #region Release Type Detection Tests [Theory] [InlineData("1.0.0")] [InlineData("2.5.3")] [InlineData("10.20.30")] [InlineData("2025.2.14.1")] public void FilterPackagesToDelete_WithProdReleases_ShouldIdentifyAsNonDeletable(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Prod releases should not be in delete list (CLEAN_PROD_VERSIONS=false by default) Assert.DoesNotContain(result, p => p.Id == 1); Assert.Empty(result); // No packages should be deleted without CLEAN_PROD_VERSIONS=true } [Theory] [InlineData("1.0.0-alpha")] [InlineData("1.0.0-ALPHA")] [InlineData("2.5.3-alpha123")] [InlineData("1.0.0-beta")] [InlineData("1.0.0-BETA")] [InlineData("1.0.0-rc")] [InlineData("1.0.0-RC1")] [InlineData("1.0.0-preview")] [InlineData("1.0.0-PREVIEW")] public void FilterPackagesToDelete_WithPrereleaseVersions_ShouldIdentifyCorrectly(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Prerelease should be kept (default KEEP_PRERELEASE_VERSIONS=3, so 1 package is kept) Assert.NotNull(result); Assert.Empty(result); // Single prerelease should be kept as it's within the limit } [Theory] [InlineData("1.0.0-dev")] [InlineData("1.0.0-DEV")] [InlineData("2.5.3-dev123")] [InlineData("1.0.0-development")] [InlineData("1.0.0-DEV")] public void FilterPackagesToDelete_WithDevVersions_ShouldIdentifyCorrectly(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Dev version should be kept (default KEEP_DEV_VERSIONS=5, so 1 package is kept) Assert.NotNull(result); Assert.Empty(result); // Single dev version should be kept as it's within the limit } #endregion #region Semantic Version Detection Tests [Theory] [InlineData("1.0")] [InlineData("1.0.0")] [InlineData("1.0.0.0")] [InlineData("2.5")] [InlineData("2.5.3")] [InlineData("2.5.3.100")] [InlineData("0.0.0.0")] [InlineData("99.99.99.99")] public void FilterPackagesToDelete_WithValidSemanticVersions_ShouldProcess(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should process without throwing and not delete prod releases Assert.NotNull(result); Assert.DoesNotContain(result, p => p.Id == 1); // Prod version should not be deleted } [Theory] [InlineData("1")] // Only 1 component [InlineData("1.0.0.0.0")] // 5 components [InlineData("1.a.0.0")] // Non-numeric [InlineData("a.b.c.d")] // All non-numeric public void FilterPackagesToDelete_WithInvalidSemanticVersions_ShouldSkip(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, new() { Id = 2, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Invalid version should be skipped, only valid semantic version (id=2) is processed Assert.NotNull(result); // Should not crash and should only consider the valid version Assert.DoesNotContain(result, p => p.Id == 2); // Valid prod should not be deleted } [Theory] [InlineData("101.0.0.0")] [InlineData("200.5.3.100")] [InlineData("1000.0.0.0")] public void FilterPackagesToDelete_WithLargeMajorVersion_ShouldTreatAsDateVersion(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Large major versions (>100) are detected as date versions, not semantic // These will fail date validation (invalid month/day) and be skipped Assert.NotNull(result); } #endregion #region Date Version Detection Tests [Theory] [InlineData("2024.1.1.1")] [InlineData("2025.2.14.1")] [InlineData("2026.12.31.999")] [InlineData("2000.6.15.100")] [InlineData("1999.1.1.1")] public void FilterPackagesToDelete_WithValidDateVersions_ShouldProcess(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should process without throwing and not delete prod date versions Assert.NotNull(result); Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted } [Theory] [InlineData("2025.1.1")] // 3 components (missing build) [InlineData("2025.1.1.1.1")] // 5 components [InlineData("2025.13.1.1")] // Month out of range [InlineData("2025.0.1.1")] // Month zero [InlineData("2025.2.32.1")] // Day out of range [InlineData("2025.2.0.1")] // Day zero [InlineData("999.2.14.1")] // Year too small [InlineData("3000.2.14.1")] // Year too large [InlineData("2025.a.14.1")] // Non-numeric month public void FilterPackagesToDelete_WithInvalidDateVersions_ShouldSkip(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, new() { Id = 2, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Invalid date version should be skipped, valid semantic version (id=2) is processed as prod Assert.NotNull(result); Assert.DoesNotContain(result, p => p.Id == 2); // Valid semantic prod should not be deleted } [Theory] [InlineData("1000.1.1.1")] // Minimum valid year [InlineData("2999.12.31.999")] // Maximum valid year [InlineData("2025.1.1.1")] // January 1 [InlineData("2025.12.31.1")] // December 31 public void FilterPackagesToDelete_WithDateVersionBoundaries_ShouldProcess(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Boundary date versions should be recognized as valid Assert.NotNull(result); Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted } #endregion #region Case Sensitivity Tests [Theory] [InlineData("1.0.0-alpha")] [InlineData("1.0.0-ALPHA")] [InlineData("1.0.0-Alpha")] [InlineData("1.0.0-AlPhA")] public void FilterPackagesToDelete_WithDifferentCasePrerelease_ShouldHandleCorrectly(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - All case variations should be recognized as prerelease Assert.NotNull(result); Assert.Empty(result); // Single prerelease should be kept } [Theory] [InlineData("1.0.0-dev")] [InlineData("1.0.0-DEV")] [InlineData("1.0.0-Dev")] [InlineData("1.0.0-DeV")] public void FilterPackagesToDelete_WithDifferentCaseDev_ShouldHandleCorrectly(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - All case variations should be recognized as dev version Assert.NotNull(result); Assert.Empty(result); // Single dev version should be kept } #endregion #region Suffix/Identifier Tests [Theory] [InlineData("1.0.0-alpha1")] [InlineData("1.0.0-alpha.1")] [InlineData("1.0.0-beta2")] [InlineData("1.0.0-rc.1")] [InlineData("2025.2.14.1-dev.1")] [InlineData("2025.2.14.1-dev123")] public void FilterPackagesToDelete_WithCompoundSuffixes_ShouldIdentifyCorrectly(string version) { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Compound suffixes should be recognized and not cause errors Assert.NotNull(result); Assert.Empty(result); // Single prerelease/dev should be kept } #endregion #region Empty and Null Tests [Fact] public void FilterPackagesToDelete_WithEmptyList_ShouldReturnEmpty() { // Arrange var packages = new List(); // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Empty input should return empty result Assert.NotNull(result); Assert.Empty(result); } [Fact] public void FilterPackagesToDelete_WithNullableInputs_ShouldHandleGracefully() { // Arrange var packages = new List { new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } }; // Act var result = _packageService.FilterPackagesToDelete(packages); // Assert - Should handle without throwing and not delete prod Assert.NotNull(result); Assert.Empty(result); // Prod should not be deleted } #endregion }