diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5e1bb65..dc41bd3 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -24,8 +24,8 @@ jobs: with: dotnet-version: 8.0.x - name: Restore dependencies - run: dotnet restore + run: dotnet restore OptimizelyTestContainers.slnx - name: Build - run: dotnet build --no-restore + run: dotnet build OptimizelyTestContainers.slnx --no-restore - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test OptimizelyTestContainers.slnx --no-build --verbosity normal diff --git a/src/Optimizely.TestContainers.Commerce.Tests/CommerceCatalogIntegrationTests.cs b/src/Optimizely.TestContainers.Commerce.Tests/CommerceCatalogIntegrationTests.cs index 89765a6..f0bc1e8 100644 --- a/src/Optimizely.TestContainers.Commerce.Tests/CommerceCatalogIntegrationTests.cs +++ b/src/Optimizely.TestContainers.Commerce.Tests/CommerceCatalogIntegrationTests.cs @@ -13,10 +13,20 @@ namespace Optimizely.TestContainers.Commerce.Tests; +/// +/// Integration tests for Commerce catalog functionality (catalogs, nodes, products). +/// Tests Commerce-specific content types and operations. +/// +[Collection("CommerceCatalogIntegrationTests")] public class CommerceCatalogIntegrationTests() : OptimizelyIntegrationTestBase(includeCommerce: true) { + /// + /// Configure web host with Commerce-specific Startup and services. + /// The base class provides CMS, Commerce, and Find configuration automatically. + /// protected override void ConfigureWebHostBuilder(IWebHostBuilder webHostBuilder) { + // Register the Startup class that configures Commerce services and content types webHostBuilder.UseStartup(); } diff --git a/src/Optimizely.TestContainers.Commerce.Tests/CommerceCatalogNegativeTests.cs b/src/Optimizely.TestContainers.Commerce.Tests/CommerceCatalogNegativeTests.cs new file mode 100644 index 0000000..8ff1143 --- /dev/null +++ b/src/Optimizely.TestContainers.Commerce.Tests/CommerceCatalogNegativeTests.cs @@ -0,0 +1,256 @@ +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using EPiServer; +using EPiServer.Commerce.Catalog.ContentTypes; +using EPiServer.Core; +using EPiServer.DataAccess; +using EPiServer.Security; +using Mediachase.Commerce; +using Mediachase.Commerce.Catalog; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Optimizely.TestContainers.Commerce.Tests.Models.Commerce; +using Optimizely.TestContainers.Shared; + +namespace Optimizely.TestContainers.Commerce.Tests; + +/// +/// Negative/edge case integration tests for Commerce catalog functionality. +/// Tests error handling, validation, and edge cases for Commerce operations. +/// +[Collection("CommerceCatalogNegativeTests")] +public class CommerceCatalogNegativeTests() : OptimizelyIntegrationTestBase(includeCommerce: true) +{ + /// + /// Configure web host with Commerce-specific Startup and services. + /// The base class provides CMS, Commerce, and Find configuration automatically. + /// + protected override void ConfigureWebHostBuilder(IWebHostBuilder webHostBuilder) + { + // Register the Startup class that configures Commerce services and content types + webHostBuilder.UseStartup(); + } + + [Fact] + public void Cannot_Load_NonExistent_Product() + { + // Arrange + var contentRepository = Services.GetRequiredService(); + var nonExistentReference = new ContentReference(99999); + + // Act & Assert + Assert.Throws(() => contentRepository.Get(nonExistentReference)); + } + + [Fact] + public void TryGet_Returns_False_For_NonExistent_Product() + { + // Arrange + var contentRepository = Services.GetRequiredService(); + var nonExistentReference = new ContentReference(99999); + + // Act + var result = contentRepository.TryGet(nonExistentReference, out var product); + + // Assert + Assert.False(result); + Assert.Null(product); + } + + [Fact] + public void Cannot_Save_Catalog_Without_Name() + { + // Arrange + var referenceConverter = Services.GetRequiredService(); + var contentRepository = Services.GetRequiredService(); + + var rootLink = referenceConverter.GetRootLink(); + + var catalog = contentRepository.GetDefault(rootLink); + catalog.Name = ""; // Empty name + catalog.DefaultCurrency = Currency.USD; + catalog.DefaultLanguage = "en"; + catalog.WeightBase = "kgs"; + catalog.LengthBase = "cm"; + + // Act & Assert + Assert.Throws(() => contentRepository.Save(catalog, SaveAction.Publish, AccessLevel.NoAccess)); + } + + [Fact] + public void Can_Delete_Product() + { + // Arrange + var referenceConverter = Services.GetRequiredService(); + var contentRepository = Services.GetRequiredService(); + + var rootLink = referenceConverter.GetRootLink(); + + var catalog = contentRepository.GetDefault(rootLink); + catalog.Name = "Delete Test Catalog"; + catalog.DefaultCurrency = Currency.USD; + catalog.DefaultLanguage = "en"; + catalog.WeightBase = "kgs"; + catalog.LengthBase = "cm"; + + var catalogReference = contentRepository.Save(catalog, SaveAction.Publish, AccessLevel.NoAccess); + + var node = contentRepository.GetDefault(catalogReference, CultureInfo.GetCultureInfo("en")); + node.Name = "Delete Test Node"; + var nodeReference = contentRepository.Save(node, SaveAction.Publish, AccessLevel.NoAccess); + + var product = contentRepository.GetDefault(nodeReference, CultureInfo.GetCultureInfo("en")); + product.Name = "To Be Deleted"; + product.Description = new XhtmlString("

Test

"); + var productReference = contentRepository.Save(product, SaveAction.Publish, AccessLevel.NoAccess); + + // Act (Delete) + contentRepository.Delete(productReference, true, AccessLevel.NoAccess); + + // Assert + var result = contentRepository.TryGet(productReference, out var deleted); + Assert.False(result); + } + + [Fact] + public void Can_Update_Existing_Product() + { + // Arrange + var referenceConverter = Services.GetRequiredService(); + var contentRepository = Services.GetRequiredService(); + + var rootLink = referenceConverter.GetRootLink(); + + var catalog = contentRepository.GetDefault(rootLink); + catalog.Name = "Update Test Catalog"; + catalog.DefaultCurrency = Currency.USD; + catalog.DefaultLanguage = "en"; + catalog.WeightBase = "kgs"; + catalog.LengthBase = "cm"; + + var catalogReference = contentRepository.Save(catalog, SaveAction.Publish, AccessLevel.NoAccess); + + var node = contentRepository.GetDefault(catalogReference, CultureInfo.GetCultureInfo("en")); + node.Name = "Update Test Node"; + var nodeReference = contentRepository.Save(node, SaveAction.Publish, AccessLevel.NoAccess); + + var product = contentRepository.GetDefault(nodeReference, CultureInfo.GetCultureInfo("en")); + product.Name = "Original Product Name"; + product.Description = new XhtmlString("

Original Description

"); + var productReference = contentRepository.Save(product, SaveAction.Publish, AccessLevel.NoAccess); + + // Act (Update) + var writable = contentRepository.Get(productReference).CreateWritableClone() as TestProduct; + writable!.Description = new XhtmlString("

Updated Description

"); + contentRepository.Save(writable, SaveAction.Publish, AccessLevel.NoAccess); + + // Assert + var loaded = contentRepository.Get(productReference); + Assert.Equal("

Updated Description

", loaded.Description?.ToHtmlString()); + } + + [Fact] + public void Can_Create_Product_As_Draft() + { + // Arrange + var referenceConverter = Services.GetRequiredService(); + var contentRepository = Services.GetRequiredService(); + + var rootLink = referenceConverter.GetRootLink(); + + var catalog = contentRepository.GetDefault(rootLink); + catalog.Name = "Draft Test Catalog"; + catalog.DefaultCurrency = Currency.USD; + catalog.DefaultLanguage = "en"; + catalog.WeightBase = "kgs"; + catalog.LengthBase = "cm"; + + var catalogReference = contentRepository.Save(catalog, SaveAction.Publish, AccessLevel.NoAccess); + + var node = contentRepository.GetDefault(catalogReference, CultureInfo.GetCultureInfo("en")); + node.Name = "Draft Test Node"; + var nodeReference = contentRepository.Save(node, SaveAction.Publish, AccessLevel.NoAccess); + + var product = contentRepository.GetDefault(nodeReference, CultureInfo.GetCultureInfo("en")); + product.Name = "Draft Product"; + product.Description = new XhtmlString("

Draft Description

"); + + // Act (Save as draft) + var productReference = contentRepository.Save(product, SaveAction.CheckOut, AccessLevel.NoAccess); + var loaded = contentRepository.Get(productReference); + + // Assert + Assert.NotNull(loaded); + Assert.Equal("Draft Product", loaded.Name); + Assert.False(loaded.Status == VersionStatus.Published); + } + + [Fact] + public void Cannot_Get_Wrong_Content_Type_From_Catalog() + { + // Arrange + var referenceConverter = Services.GetRequiredService(); + var contentRepository = Services.GetRequiredService(); + + var rootLink = referenceConverter.GetRootLink(); + + var catalog = contentRepository.GetDefault(rootLink); + catalog.Name = "Type Test Catalog"; + catalog.DefaultCurrency = Currency.USD; + catalog.DefaultLanguage = "en"; + catalog.WeightBase = "kgs"; + catalog.LengthBase = "cm"; + + var catalogReference = contentRepository.Save(catalog, SaveAction.Publish, AccessLevel.NoAccess); + + // Act & Assert - Try to get Catalog as Product + Assert.Throws(() => contentRepository.Get(catalogReference)); + } + + [Fact] + public void Can_Create_Multiple_Products_In_Same_Node() + { + // Arrange + var referenceConverter = Services.GetRequiredService(); + var contentRepository = Services.GetRequiredService(); + + var rootLink = referenceConverter.GetRootLink(); + + var catalog = contentRepository.GetDefault(rootLink); + catalog.Name = "Multiple Products Catalog"; + catalog.DefaultCurrency = Currency.USD; + catalog.DefaultLanguage = "en"; + catalog.WeightBase = "kgs"; + catalog.LengthBase = "cm"; + + var catalogReference = contentRepository.Save(catalog, SaveAction.Publish, AccessLevel.NoAccess); + + var node = contentRepository.GetDefault(catalogReference, CultureInfo.GetCultureInfo("en")); + node.Name = "Multiple Products Node"; + var nodeReference = contentRepository.Save(node, SaveAction.Publish, AccessLevel.NoAccess); + + // Create first product + var product1 = contentRepository.GetDefault(nodeReference, CultureInfo.GetCultureInfo("en")); + product1.Name = "Product 1"; + product1.Description = new XhtmlString("

Description 1

"); + var productRef1 = contentRepository.Save(product1, SaveAction.Publish, AccessLevel.NoAccess); + + // Create second product + var product2 = contentRepository.GetDefault(nodeReference, CultureInfo.GetCultureInfo("en")); + product2.Name = "Product 2"; + product2.Description = new XhtmlString("

Description 2

"); + + // Act + var productRef2 = contentRepository.Save(product2, SaveAction.Publish, AccessLevel.NoAccess); + + // Assert + var loaded1 = contentRepository.Get(productRef1); + var loaded2 = contentRepository.Get(productRef2); + + Assert.NotNull(loaded1); + Assert.NotNull(loaded2); + Assert.Equal("Product 1", loaded1.Name); + Assert.Equal("Product 2", loaded2.Name); + Assert.NotEqual(loaded1.ContentLink, loaded2.ContentLink); + } +} \ No newline at end of file diff --git a/src/Optimizely.TestContainers.Commerce.Tests/Models/Commerce/TestProductTests.cs b/src/Optimizely.TestContainers.Commerce.Tests/Models/Commerce/TestProductTests.cs new file mode 100644 index 0000000..ee6b5e1 --- /dev/null +++ b/src/Optimizely.TestContainers.Commerce.Tests/Models/Commerce/TestProductTests.cs @@ -0,0 +1,143 @@ +using System.ComponentModel.DataAnnotations; +using EPiServer.Commerce.Catalog.ContentTypes; +using EPiServer.Commerce.Catalog.DataAnnotations; +using EPiServer.Core; +using EPiServer.DataAbstraction; +using EPiServer.DataAnnotations; + +namespace Optimizely.TestContainers.Commerce.Tests.Models.Commerce; + +public class TestProductTests +{ + [Fact] + public void TestProduct_Should_Have_CatalogContentType_Attribute() + { + // Arrange & Act + var attribute = typeof(TestProduct).GetCustomAttributes(typeof(CatalogContentTypeAttribute), false) + .FirstOrDefault() as CatalogContentTypeAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal(Guid.Parse("0B06DE9B-6AE3-40FB-909E-E718CCC260AE"), Guid.Parse(attribute.GUID)); + Assert.Equal("Test Product", attribute.DisplayName); + Assert.Equal("Test product for integration tests.", attribute.Description); + } + + [Fact] + public void TestProduct_Should_Inherit_From_ProductContent() + { + // Arrange & Act + var isProductContent = typeof(ProductContent).IsAssignableFrom(typeof(TestProduct)); + + // Assert + Assert.True(isProductContent); + } + + [Fact] + public void Description_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(TestProduct).GetProperty(nameof(TestProduct.Description)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void Description_Property_Should_Have_Display_Attribute() + { + // Arrange + var property = typeof(TestProduct).GetProperty(nameof(TestProduct.Description)); + + // Act + var displayAttribute = property?.GetCustomAttributes(typeof(DisplayAttribute), false) + .FirstOrDefault() as DisplayAttribute; + + // Assert + Assert.NotNull(displayAttribute); + Assert.Equal("Description", displayAttribute.Name); + Assert.Equal(SystemTabNames.Content, displayAttribute.GroupName); + Assert.Equal(1, displayAttribute.Order); + } + + [Fact] + public void Description_Property_Should_Have_Searchable_Attribute() + { + // Arrange + var property = typeof(TestProduct).GetProperty(nameof(TestProduct.Description)); + + // Act + var attribute = property?.GetCustomAttributes(typeof(SearchableAttribute), false) + .FirstOrDefault(); + + // Assert + Assert.NotNull(attribute); + } + + [Fact] + public void Description_Property_Should_Have_CultureSpecific_Attribute() + { + // Arrange + var property = typeof(TestProduct).GetProperty(nameof(TestProduct.Description)); + + // Act + var attribute = property?.GetCustomAttributes(typeof(CultureSpecificAttribute), false) + .FirstOrDefault(); + + // Assert + Assert.NotNull(attribute); + } + + [Fact] + public void Description_Property_Should_Have_Tokenize_Attribute() + { + // Arrange + var property = typeof(TestProduct).GetProperty(nameof(TestProduct.Description)); + + // Act + var attribute = property?.GetCustomAttributes(typeof(TokenizeAttribute), false) + .FirstOrDefault(); + + // Assert + Assert.NotNull(attribute); + } + + [Fact] + public void Description_Property_Should_Have_IncludeInDefaultSearch_Attribute() + { + // Arrange + var property = typeof(TestProduct).GetProperty(nameof(TestProduct.Description)); + + // Act + var attribute = property?.GetCustomAttributes(typeof(IncludeInDefaultSearchAttribute), false) + .FirstOrDefault(); + + // Assert + Assert.NotNull(attribute); + } + + [Fact] + public void TestProduct_Can_Be_Instantiated() + { + // Act + var testProduct = new TestProduct(); + + // Assert + Assert.NotNull(testProduct); + } + + [Fact] + public void Description_Property_Can_Be_Set_And_Retrieved() + { + // Arrange + var testProduct = new TestProduct(); + var expectedDescription = new XhtmlString("

Test product description

"); + + // Act + testProduct.Description = expectedDescription; + + // Assert + Assert.Equal(expectedDescription, testProduct.Description); + } +} \ No newline at end of file diff --git a/src/Optimizely.TestContainers.Commerce.Tests/Optimizely.TestContainers.Commerce.Tests.csproj b/src/Optimizely.TestContainers.Commerce.Tests/Optimizely.TestContainers.Commerce.Tests.csproj index fc4b8b3..fb0add9 100644 --- a/src/Optimizely.TestContainers.Commerce.Tests/Optimizely.TestContainers.Commerce.Tests.csproj +++ b/src/Optimizely.TestContainers.Commerce.Tests/Optimizely.TestContainers.Commerce.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable false @@ -20,12 +20,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -44,9 +45,5 @@ PreserveNewest - - - - - - + + \ No newline at end of file diff --git a/src/Optimizely.TestContainers.Commerce.Tests/Properties/AssemblyInfo.cs b/src/Optimizely.TestContainers.Commerce.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b7bf16a --- /dev/null +++ b/src/Optimizely.TestContainers.Commerce.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Xunit; + +// We do not want integration tests to run in parallel at all to avoid deadlocks and database disposing itself multiple times +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/src/Optimizely.TestContainers.Commerce.Tests/StartupTests.cs b/src/Optimizely.TestContainers.Commerce.Tests/StartupTests.cs new file mode 100644 index 0000000..29193dc --- /dev/null +++ b/src/Optimizely.TestContainers.Commerce.Tests/StartupTests.cs @@ -0,0 +1,140 @@ +using EPiServer; +using EPiServer.Scheduler; +using Mediachase.Commerce.Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Moq; + +namespace Optimizely.TestContainers.Commerce.Tests; + +public class StartupTests +{ + [Fact] + public void ConfigureServices_Should_Add_CMS_And_Commerce_Services() + { + // Arrange + var services = new ServiceCollection(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + // Act + startup.ConfigureServices(services); + + // Assert - Check that core CMS services are registered + Assert.Contains(services, s => s.ServiceType == typeof(IContentRepository)); + Assert.Contains(services, s => s.ServiceType == typeof(IContentLoader)); + + // Check that Commerce services are registered + Assert.Contains(services, s => s.ServiceType == typeof(ReferenceConverter)); + } + + [Fact] + public void ConfigureServices_In_Development_Should_Configure_Scheduler_Options() + { + // Arrange + var services = new ServiceCollection(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Development); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + // Act + startup.ConfigureServices(services); + + // Assert - Check that scheduler options are configured + var serviceProvider = services.BuildServiceProvider(); + var schedulerOptions = serviceProvider.GetService>(); + Assert.NotNull(schedulerOptions); + Assert.False(schedulerOptions.Value.Enabled); + } + + [Fact] + public void ConfigureServices_In_Production_Should_Not_Configure_Scheduler_Options() + { + // Arrange + var services = new ServiceCollection(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + // Act + startup.ConfigureServices(services); + + // Assert - Scheduler should use default configuration + var serviceProvider = services.BuildServiceProvider(); + var schedulerOptions = serviceProvider.GetService>(); + + Assert.NotNull(schedulerOptions); + } + + [Fact] + public void Configure_Should_Setup_Middleware_Pipeline() + { + // Arrange + var mockAppBuilder = new Mock(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + mockAppBuilder.Setup(x => x.Use(It.IsAny>())) + .Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.New()).Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.ApplicationServices).Returns(new ServiceCollection().BuildServiceProvider()); + + // Act + startup.Configure(mockAppBuilder.Object, mockEnvironment.Object); + + // Assert + mockAppBuilder.Verify(x => x.Use(It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public void Configure_In_Development_Should_Use_DeveloperExceptionPage() + { + // Arrange + var mockAppBuilder = new Mock(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Development); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + mockAppBuilder.Setup(x => x.Use(It.IsAny>())) + .Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.New()).Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.ApplicationServices).Returns(new ServiceCollection().BuildServiceProvider()); + + // Act + startup.Configure(mockAppBuilder.Object, mockEnvironment.Object); + + // Assert + mockAppBuilder.Verify(x => x.Use(It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public void Startup_Constructor_Should_Accept_WebHostEnvironment() + { + // Arrange + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + // Act + var startup = new Startup(mockEnvironment.Object); + + // Assert + Assert.NotNull(startup); + } +} diff --git a/src/Optimizely.TestContainers.Shared/Optimizely.TestContainers.Shared.csproj b/src/Optimizely.TestContainers.Shared/Optimizely.TestContainers.Shared.csproj index ef25340..40de16f 100644 --- a/src/Optimizely.TestContainers.Shared/Optimizely.TestContainers.Shared.csproj +++ b/src/Optimizely.TestContainers.Shared/Optimizely.TestContainers.Shared.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable false @@ -16,9 +16,12 @@ - - + + + + + diff --git a/src/Optimizely.TestContainers.Shared/OptimizelyIntegrationTestBase.cs b/src/Optimizely.TestContainers.Shared/OptimizelyIntegrationTestBase.cs index 41b9d35..6026b3e 100644 --- a/src/Optimizely.TestContainers.Shared/OptimizelyIntegrationTestBase.cs +++ b/src/Optimizely.TestContainers.Shared/OptimizelyIntegrationTestBase.cs @@ -45,37 +45,54 @@ public virtual async Task InitializeAsync() services.Configure(o => { o.SetConnectionString(cmsDatabaseConnectionString); + }); + + if (!string.IsNullOrWhiteSpace(commerceDatabaseConnectionString)) + { + services.Configure(o => + { + o.ConnectionStrings.Add(new ConnectionStringOptions + { + ConnectionString = commerceDatabaseConnectionString, + Name = "EcfSqlConnection" + }); + }); + } }) .ConfigureAppConfiguration((context, configBuilder) => { var testSettings = new Dictionary { - ["ConnectionStrings:EPiServerDB"] = cmsDatabaseConnectionString, - ["ConnectionStrings:EcfSqlConnection"] = commerceDatabaseConnectionString, + ["ConnectionStrings:EPiServerDB"] = cmsDatabaseConnectionString }; - + + if(!string.IsNullOrWhiteSpace(commerceDatabaseConnectionString)) + { + testSettings["ConnectionStrings:EcfSqlConnection"] = commerceDatabaseConnectionString; + } + configBuilder.AddInMemoryCollection(testSettings); }); - // To configure apps separately with Cms and Commerce Startup files in separate projects + // This enables the integration tests to configure apps separately with Cms and Commerce Startup files in separate projects ConfigureWebHostBuilder(webHostBuilder); }) .ConfigureCmsDefaults() .Build(); - // Run initialization engine (simulate application startup) + Services = _host.Services; + + // Run initialization engine (simulate application startup) var initializer = _host.Services.GetRequiredService(); if (initializer.InitializationState != InitializationState.Initialized) - initializer.Initialize(); - - Services = _host.Services; + initializer.Initialize(); await _host.StartAsync(); } protected abstract void ConfigureWebHostBuilder(IWebHostBuilder webHostBuilder); - + public async Task DisposeAsync() { await _host.StopAsync(); diff --git a/src/OptimizelyTestContainers.Tests/MediaIntegrationTests.cs b/src/OptimizelyTestContainers.Tests/MediaIntegrationTests.cs new file mode 100644 index 0000000..ce7d45a --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/MediaIntegrationTests.cs @@ -0,0 +1,328 @@ +using System.Reflection; +using EPiServer; +using EPiServer.Core; +using EPiServer.DataAccess; +using EPiServer.Framework.Blobs; +using EPiServer.Security; +using EPiServer.Web; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Optimizely.TestContainers.Models.Media; +using Optimizely.TestContainers.Shared; +using OptimizelyTestContainers.Tests.Models.Media; +using OptimizelyTestContainers.Tests.Models.Pages; + +namespace OptimizelyTestContainers.Tests; + +/// +/// Integration tests for media content types (ImageFile, VideoFile, GenericMedia). +/// Tests blob storage, media properties, and asset management using the unified fixture pattern. +/// +[Collection("MediaIntegrationTests")] +public class MediaIntegrationTests() : OptimizelyIntegrationTestBase(includeCommerce: true) +{ + /// + /// Configure web host with CMS-specific Startup and services. + /// The base class provides Commerce and Find configuration automatically. + /// + protected override void ConfigureWebHostBuilder(IWebHostBuilder webHostBuilder) + { + // Register the Startup class that configures CMS services and content types + webHostBuilder.UseStartup(); + + // Register additional test-specific services + webHostBuilder.ConfigureServices(services => + { + services.AddTransient(); + }); + } + + [Fact] + public void Can_Create_And_Read_ImageFile() + { + // Arrange + var repo = Services.GetRequiredService(); + var blobFactory = Services.GetRequiredService(); + + // Import test data to get StartPage + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + // Get Assets folder + var assetsFolder = repo.GetChildren(ContentReference.GlobalBlockFolder).FirstOrDefault() + ?? repo.GetDefault(ContentReference.GlobalBlockFolder); + + if (assetsFolder.ContentLink.ID == 0) + { + assetsFolder.Name = "Assets"; + repo.Save(assetsFolder, SaveAction.Publish, AccessLevel.NoAccess); + } + + // Create ImageFile + var imageFile = repo.GetDefault(assetsFolder.ContentLink); + imageFile.Name = "test-image.jpg"; + imageFile.Copyright = "© 2024 Test Company"; + + // Create a simple blob with test data + var blob = blobFactory.CreateBlob(imageFile.BinaryDataContainer, ".jpg"); + using (var stream = blob.OpenWrite()) + { + var testData = new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }; // JPEG header + stream.Write(testData, 0, testData.Length); + } + imageFile.BinaryData = blob; + + // Act (Save and Load ImageFile) + var savedRef = repo.Save(imageFile, SaveAction.Publish, AccessLevel.NoAccess); + var loaded = repo.Get(savedRef); + + // Assert + Assert.NotNull(loaded); + Assert.Equal("test-image.jpg", loaded.Name); + Assert.Equal("© 2024 Test Company", loaded.Copyright); + Assert.NotNull(loaded.BinaryData); + } + + [Fact] + public void Can_Create_And_Read_VideoFile() + { + // Arrange + var repo = Services.GetRequiredService(); + var blobFactory = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + // Get Assets folder + var assetsFolder = repo.GetChildren(ContentReference.GlobalBlockFolder).FirstOrDefault() + ?? repo.GetDefault(ContentReference.GlobalBlockFolder); + + if (assetsFolder.ContentLink.ID == 0) + { + assetsFolder.Name = "Assets"; + repo.Save(assetsFolder, SaveAction.Publish, AccessLevel.NoAccess); + } + + // Create VideoFile + var videoFile = repo.GetDefault(assetsFolder.ContentLink); + videoFile.Name = "test-video.mp4"; + videoFile.Copyright = "© 2024 Video Productions"; + videoFile.PreviewImage = ContentReference.EmptyReference; + + // Create a simple blob with test data + var blob = blobFactory.CreateBlob(videoFile.BinaryDataContainer, ".mp4"); + using (var stream = blob.OpenWrite()) + { + var testData = new byte[] { 0x00, 0x00, 0x00, 0x18 }; // MP4 signature + stream.Write(testData, 0, testData.Length); + } + videoFile.BinaryData = blob; + + // Act (Save and Load VideoFile) + var savedRef = repo.Save(videoFile, SaveAction.Publish, AccessLevel.NoAccess); + var loaded = repo.Get(savedRef); + + // Assert + Assert.NotNull(loaded); + Assert.Equal("test-video.mp4", loaded.Name); + Assert.Equal("© 2024 Video Productions", loaded.Copyright); + Assert.NotNull(loaded.BinaryData); + } + + [Fact] + public void Can_Create_And_Read_GenericMedia() + { + // Arrange + var repo = Services.GetRequiredService(); + var blobFactory = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + // Get Assets folder + var assetsFolder = repo.GetChildren(ContentReference.GlobalBlockFolder).FirstOrDefault() + ?? repo.GetDefault(ContentReference.GlobalBlockFolder); + + if (assetsFolder.ContentLink.ID == 0) + { + assetsFolder.Name = "Assets"; + repo.Save(assetsFolder, SaveAction.Publish, AccessLevel.NoAccess); + } + + // Create GenericMedia + var genericMedia = repo.GetDefault(assetsFolder.ContentLink); + genericMedia.Name = "test-document.pdf"; + genericMedia.Description = "Test media file description"; + + // Create a simple blob with test data + var blob = blobFactory.CreateBlob(genericMedia.BinaryDataContainer, ".pdf"); + using (var stream = blob.OpenWrite()) + { + var testData = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // PDF signature + stream.Write(testData, 0, testData.Length); + } + genericMedia.BinaryData = blob; + + // Act (Save and Load GenericMedia) + var savedRef = repo.Save(genericMedia, SaveAction.Publish, AccessLevel.NoAccess); + var loaded = repo.Get(savedRef); + + // Assert + Assert.NotNull(loaded); + Assert.Equal("test-document.pdf", loaded.Name); + Assert.Equal("Test media file description", loaded.Description); + Assert.NotNull(loaded.BinaryData); + } + + [Fact] + public void ImageFile_Properties_Should_Persist_After_Save() + { + // Arrange + var repo = Services.GetRequiredService(); + var blobFactory = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + // Get Assets folder + var assetsFolder = repo.GetChildren(ContentReference.GlobalBlockFolder).FirstOrDefault() + ?? repo.GetDefault(ContentReference.GlobalBlockFolder); + + if (assetsFolder.ContentLink.ID == 0) + { + assetsFolder.Name = "Assets"; + repo.Save(assetsFolder, SaveAction.Publish, AccessLevel.NoAccess); + } + + var imageFile = repo.GetDefault(assetsFolder.ContentLink); + var expectedCopyright = "© Test Copyright 2024"; + imageFile.Name = "copyright-test.jpg"; + imageFile.Copyright = expectedCopyright; + + var blob = blobFactory.CreateBlob(imageFile.BinaryDataContainer, ".jpg"); + using (var stream = blob.OpenWrite()) + { + var testData = new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }; + stream.Write(testData, 0, testData.Length); + } + imageFile.BinaryData = blob; + + // Act + var savedRef = repo.Save(imageFile, SaveAction.Publish, AccessLevel.NoAccess); + var loaded = repo.Get(savedRef); + + // Assert + Assert.Equal(expectedCopyright, loaded.Copyright); + } + + [Fact] + public void VideoFile_PreviewImage_Should_Persist_After_Save() + { + // Arrange + var repo = Services.GetRequiredService(); + var blobFactory = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + // Get Assets folder + var assetsFolder = repo.GetChildren(ContentReference.GlobalBlockFolder).FirstOrDefault() + ?? repo.GetDefault(ContentReference.GlobalBlockFolder); + + if (assetsFolder.ContentLink.ID == 0) + { + assetsFolder.Name = "Assets"; + assetsFolder.ContentLink = repo.Save(assetsFolder, SaveAction.Publish, AccessLevel.NoAccess); + } + + var videoFile = repo.GetDefault(assetsFolder.ContentLink); + var expectedPreviewImage = new ContentReference(999); + videoFile.Name = "preview-test.mp4"; + videoFile.Copyright = "Test"; + videoFile.PreviewImage = expectedPreviewImage; + + var blob = blobFactory.CreateBlob(videoFile.BinaryDataContainer, ".mp4"); + using (var stream = blob.OpenWrite()) + { + var testData = new byte[] { 0x00, 0x00, 0x00, 0x18 }; + stream.Write(testData, 0, testData.Length); + } + videoFile.BinaryData = blob; + + // Act + var savedRef = repo.Save(videoFile, SaveAction.Publish, AccessLevel.NoAccess); + var loaded = repo.Get(savedRef); + + // Assert + Assert.Equal(expectedPreviewImage, loaded.PreviewImage); + } +} \ No newline at end of file diff --git a/src/OptimizelyTestContainers.Tests/Models/Media/GenericMediaTests.cs b/src/OptimizelyTestContainers.Tests/Models/Media/GenericMediaTests.cs new file mode 100644 index 0000000..8b12928 --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/Models/Media/GenericMediaTests.cs @@ -0,0 +1,65 @@ +using EPiServer.Core; +using EPiServer.DataAnnotations; +using OptimizelyTestContainers.Tests.Models.Media; + +namespace OptimizelyTestContainers.Tests.Models.Media; + +public class GenericMediaTests +{ + [Fact] + public void GenericMedia_Should_Have_ContentType_Attribute() + { + // Arrange & Act + var attribute = typeof(GenericMedia).GetCustomAttributes(typeof(ContentTypeAttribute), false) + .FirstOrDefault() as ContentTypeAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal("EE3BD195-7CB0-4756-AB5F-E5E223CD9820", attribute.GUID); + } + + [Fact] + public void GenericMedia_Should_Inherit_From_MediaData() + { + // Arrange & Act + var isMediaData = typeof(MediaData).IsAssignableFrom(typeof(GenericMedia)); + + // Assert + Assert.True(isMediaData); + } + + [Fact] + public void Description_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(GenericMedia).GetProperty(nameof(GenericMedia.Description)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void GenericMedia_Can_Be_Instantiated() + { + // Act + var genericMedia = new GenericMedia(); + + // Assert + Assert.NotNull(genericMedia); + } + + [Fact] + public void Description_Property_Can_Be_Set_And_Retrieved() + { + // Arrange + var genericMedia = new GenericMedia(); + var expectedDescription = "This is a test media description"; + + // Act + genericMedia.Description = expectedDescription; + + // Assert + Assert.Equal(expectedDescription, genericMedia.Description); + } +} diff --git a/src/OptimizelyTestContainers.Tests/Models/Media/ImageFileTests.cs b/src/OptimizelyTestContainers.Tests/Models/Media/ImageFileTests.cs new file mode 100644 index 0000000..6c624b6 --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/Models/Media/ImageFileTests.cs @@ -0,0 +1,78 @@ +using EPiServer.Core; +using EPiServer.DataAnnotations; +using EPiServer.Framework.DataAnnotations; +using OptimizelyTestContainers.Tests.Models.Media; + +namespace OptimizelyTestContainers.Tests.Models.Media; + +public class ImageFileTests +{ + [Fact] + public void ImageFile_Should_Have_ContentType_Attribute() + { + // Arrange & Act + var attribute = typeof(ImageFile).GetCustomAttributes(typeof(ContentTypeAttribute), false) + .FirstOrDefault() as ContentTypeAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal("0A89E464-56D4-449F-AEA8-2BF774AB8730", attribute.GUID); + } + + [Fact] + public void ImageFile_Should_Have_MediaDescriptor_Attribute() + { + // Arrange & Act + var attribute = typeof(ImageFile).GetCustomAttributes(typeof(MediaDescriptorAttribute), false) + .FirstOrDefault() as MediaDescriptorAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal("jpg,jpeg,jpe,ico,gif,bmp,png", attribute.ExtensionString); + } + + [Fact] + public void ImageFile_Should_Inherit_From_ImageData() + { + // Arrange & Act + var isImageData = typeof(ImageData).IsAssignableFrom(typeof(ImageFile)); + + // Assert + Assert.True(isImageData); + } + + [Fact] + public void Copyright_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(ImageFile).GetProperty(nameof(ImageFile.Copyright)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void ImageFile_Can_Be_Instantiated() + { + // Act + var imageFile = new ImageFile(); + + // Assert + Assert.NotNull(imageFile); + } + + [Fact] + public void Copyright_Property_Can_Be_Set_And_Retrieved() + { + // Arrange + var imageFile = new ImageFile(); + var expectedCopyright = "© 2024 Test Company"; + + // Act + imageFile.Copyright = expectedCopyright; + + // Assert + Assert.Equal(expectedCopyright, imageFile.Copyright); + } +} diff --git a/src/OptimizelyTestContainers.Tests/Models/Media/VideoFileTests.cs b/src/OptimizelyTestContainers.Tests/Models/Media/VideoFileTests.cs new file mode 100644 index 0000000..862a19c --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/Models/Media/VideoFileTests.cs @@ -0,0 +1,120 @@ +using System.ComponentModel.DataAnnotations; +using EPiServer.Core; +using EPiServer.DataAnnotations; +using EPiServer.Framework.DataAnnotations; +using EPiServer.Web; +using Optimizely.TestContainers.Models.Media; + +namespace OptimizelyTestContainers.Tests.Models.Media; + +public class VideoFileTests +{ + [Fact] + public void VideoFile_Should_Have_ContentType_Attribute() + { + // Arrange & Act + var attribute = typeof(VideoFile).GetCustomAttributes(typeof(ContentTypeAttribute), false) + .FirstOrDefault() as ContentTypeAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal("85468104-E06F-47E5-A317-FC9B83D3CBA6", attribute.GUID); + } + + [Fact] + public void VideoFile_Should_Have_MediaDescriptor_Attribute() + { + // Arrange & Act + var attribute = typeof(VideoFile).GetCustomAttributes(typeof(MediaDescriptorAttribute), false) + .FirstOrDefault() as MediaDescriptorAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal("flv,mp4,webm", attribute.ExtensionString); + } + + [Fact] + public void VideoFile_Should_Inherit_From_VideoData() + { + // Arrange & Act + var isVideoData = typeof(VideoData).IsAssignableFrom(typeof(VideoFile)); + + // Assert + Assert.True(isVideoData); + } + + [Fact] + public void Copyright_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(VideoFile).GetProperty(nameof(VideoFile.Copyright)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void PreviewImage_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(VideoFile).GetProperty(nameof(VideoFile.PreviewImage)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void PreviewImage_Property_Should_Have_UIHint_Attribute() + { + // Arrange + var property = typeof(VideoFile).GetProperty(nameof(VideoFile.PreviewImage)); + + // Act + var attribute = property?.GetCustomAttributes(typeof(UIHintAttribute), false) + .FirstOrDefault() as UIHintAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal(UIHint.Image, attribute.UIHint); + } + + [Fact] + public void VideoFile_Can_Be_Instantiated() + { + // Act + var videoFile = new VideoFile(); + + // Assert + Assert.NotNull(videoFile); + } + + [Fact] + public void Copyright_Property_Can_Be_Set_And_Retrieved() + { + // Arrange + var videoFile = new VideoFile(); + var expectedCopyright = "© 2024 Video Productions"; + + // Act + videoFile.Copyright = expectedCopyright; + + // Assert + Assert.Equal(expectedCopyright, videoFile.Copyright); + } + + [Fact] + public void PreviewImage_Property_Can_Be_Set_And_Retrieved() + { + // Arrange + var videoFile = new VideoFile(); + var expectedReference = new ContentReference(123); + + // Act + videoFile.PreviewImage = expectedReference; + + // Assert + Assert.Equal(expectedReference, videoFile.PreviewImage); + } +} diff --git a/src/OptimizelyTestContainers.Tests/Models/Pages/NewsPageTests.cs b/src/OptimizelyTestContainers.Tests/Models/Pages/NewsPageTests.cs new file mode 100644 index 0000000..b4b267d --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/Models/Pages/NewsPageTests.cs @@ -0,0 +1,66 @@ +using EPiServer.Core; +using EPiServer.DataAnnotations; +using OptimizelyTestContainers.Tests.Models.Pages; + +namespace OptimizelyTestContainers.Tests.Models.Pages; + +public class NewsPageTests +{ + [Fact] + public void NewsPage_Should_Have_ContentType_Attribute() + { + // Arrange & Act + var attribute = typeof(NewsPage).GetCustomAttributes(typeof(ContentTypeAttribute), false) + .FirstOrDefault() as ContentTypeAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal("7B873919-11AC-4DF4-B9E8-09F414F76164", attribute.GUID); + Assert.Equal("News Page", attribute.DisplayName); + } + + [Fact] + public void NewsPage_Should_Inherit_From_PageData() + { + // Arrange & Act + var isPageData = typeof(PageData).IsAssignableFrom(typeof(NewsPage)); + + // Assert + Assert.True(isPageData); + } + + [Fact] + public void Title_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(NewsPage).GetProperty(nameof(NewsPage.Title)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void NewsPage_Can_Be_Instantiated() + { + // Act + var newsPage = new NewsPage(); + + // Assert + Assert.NotNull(newsPage); + } + + [Fact] + public void Title_Property_Can_Be_Set_And_Retrieved() + { + // Arrange + var newsPage = new NewsPage(); + var expectedTitle = "Test News Title"; + + // Act + newsPage.Title = expectedTitle; + + // Assert + Assert.Equal(expectedTitle, newsPage.Title); + } +} diff --git a/src/OptimizelyTestContainers.Tests/Models/Pages/StartPageTests.cs b/src/OptimizelyTestContainers.Tests/Models/Pages/StartPageTests.cs new file mode 100644 index 0000000..e1c7ac1 --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/Models/Pages/StartPageTests.cs @@ -0,0 +1,83 @@ +using System.ComponentModel.DataAnnotations; +using EPiServer.Core; +using EPiServer.DataAbstraction; +using EPiServer.DataAnnotations; +using OptimizelyTestContainers.Tests.Models.Pages; + +namespace OptimizelyTestContainers.Tests.Models.Pages; + +public class StartPageTests +{ + [Fact] + public void StartPage_Should_Have_ContentType_Attribute() + { + // Arrange & Act + var attribute = typeof(StartPage).GetCustomAttributes(typeof(ContentTypeAttribute), false) + .FirstOrDefault() as ContentTypeAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal("19671657-B684-4D95-A61F-8DD4FE60D559", attribute.GUID); + } + + [Fact] + public void StartPage_Should_Inherit_From_PageData() + { + // Arrange & Act + var isPageData = typeof(PageData).IsAssignableFrom(typeof(StartPage)); + + // Assert + Assert.True(isPageData); + } + + [Fact] + public void MainContentArea_Property_Should_Have_Display_Attribute() + { + // Arrange + var property = typeof(StartPage).GetProperty(nameof(StartPage.MainContentArea)); + + // Act + var displayAttribute = property?.GetCustomAttributes(typeof(DisplayAttribute), false) + .FirstOrDefault() as DisplayAttribute; + + // Assert + Assert.NotNull(displayAttribute); + Assert.Equal(SystemTabNames.Content, displayAttribute.GroupName); + Assert.Equal(320, displayAttribute.Order); + } + + [Fact] + public void MainContentArea_Property_Should_Have_CultureSpecific_Attribute() + { + // Arrange + var property = typeof(StartPage).GetProperty(nameof(StartPage.MainContentArea)); + + // Act + var attribute = property?.GetCustomAttributes(typeof(CultureSpecificAttribute), false) + .FirstOrDefault(); + + // Assert + Assert.NotNull(attribute); + } + + [Fact] + public void MainContentArea_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(StartPage).GetProperty(nameof(StartPage.MainContentArea)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void StartPage_Can_Be_Instantiated() + { + // Act + var startPage = new StartPage(); + + // Assert + Assert.NotNull(startPage); + } +} diff --git a/src/OptimizelyTestContainers.Tests/NewsPageNegativeTests.cs b/src/OptimizelyTestContainers.Tests/NewsPageNegativeTests.cs new file mode 100644 index 0000000..34ad44c --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/NewsPageNegativeTests.cs @@ -0,0 +1,287 @@ +using System.Reflection; +using EPiServer; +using EPiServer.Core; +using EPiServer.DataAccess; +using EPiServer.Security; +using EPiServer.Web; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Optimizely.TestContainers.Shared; +using OptimizelyTestContainers.Tests.Models.Pages; + +namespace OptimizelyTestContainers.Tests; + +public class NewsPageNegativeTests() : OptimizelyIntegrationTestBase(includeCommerce: false) +{ + protected override void ConfigureWebHostBuilder(IWebHostBuilder webHostBuilder) + { + webHostBuilder.UseStartup(); + + webHostBuilder.ConfigureServices(services => + { + services.AddTransient(); + }); + } + + [Fact] + public void Cannot_Load_NonExistent_NewsPage() + { + // Arrange + var repo = Services.GetRequiredService(); + var nonExistentReference = new ContentReference(99999); + + // Act & Assert + Assert.Throws(() => repo.Get(nonExistentReference)); + } + + [Fact] + public void TryGet_Returns_False_For_NonExistent_Content() + { + // Arrange + var repo = Services.GetRequiredService(); + var nonExistentReference = new ContentReference(99999); + + // Act + var result = repo.TryGet(nonExistentReference, out var content); + + // Assert + Assert.False(result); + Assert.Null(content); + } + + [Fact] + public void Cannot_Save_NewsPage_Without_Name() + { + // Arrange + var repo = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + var allSites = siteDefinitionRepo.List(); + var site = allSites.First(); + + // Create NewsPage without name + var news = repo.GetDefault(site.StartPage); + news.Name = ""; // Empty name + news.Title = "Test Title"; + + // Act & Assert + Assert.Throws(() => repo.Save(news, SaveAction.Publish, AccessLevel.NoAccess)); + } + + [Fact] + public void Can_Save_NewsPage_As_Draft() + { + // Arrange + var repo = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + var allSites = siteDefinitionRepo.List(); + var site = allSites.First(); + + // Create NewsPage + var news = repo.GetDefault(site.StartPage); + news.Name = "Draft News"; + news.Title = "Draft Title"; + + // Act (Save as draft) + var savedRef = repo.Save(news, SaveAction.CheckOut, AccessLevel.NoAccess); + var loaded = repo.Get(savedRef); + + // Assert + Assert.NotNull(loaded); + Assert.Equal("Draft Title", loaded.Title); + Assert.False(loaded.Status == VersionStatus.Published); + } + + [Fact] + public void Can_Delete_NewsPage() + { + // Arrange + var repo = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + var allSites = siteDefinitionRepo.List(); + var site = allSites.First(); + + // Create and save NewsPage + var news = repo.GetDefault(site.StartPage); + news.Name = "To Be Deleted"; + news.Title = "Delete Test"; + var savedRef = repo.Save(news, SaveAction.Publish, AccessLevel.NoAccess); + + // Act (Delete) + repo.Delete(savedRef, true, AccessLevel.NoAccess); + + // Assert + var result = repo.TryGet(savedRef, out var deleted); + Assert.False(result); + } + + [Fact] + public void Can_Create_Multiple_NewsPages_With_Same_Title() + { + // Arrange + var repo = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + var allSites = siteDefinitionRepo.List(); + var site = allSites.First(); + + // Create first NewsPage + var news1 = repo.GetDefault(site.StartPage); + news1.Name = "Duplicate Test 1"; + news1.Title = "Same Title"; + var savedRef1 = repo.Save(news1, SaveAction.Publish, AccessLevel.NoAccess); + + // Create second NewsPage with same title + var news2 = repo.GetDefault(site.StartPage); + news2.Name = "Duplicate Test 2"; + news2.Title = "Same Title"; + + // Act + var savedRef2 = repo.Save(news2, SaveAction.Publish, AccessLevel.NoAccess); + + // Assert - Both should be saved successfully + var loaded1 = repo.Get(savedRef1); + var loaded2 = repo.Get(savedRef2); + + Assert.NotNull(loaded1); + Assert.NotNull(loaded2); + Assert.Equal("Same Title", loaded1.Title); + Assert.Equal("Same Title", loaded2.Title); + Assert.NotEqual(loaded1.ContentLink, loaded2.ContentLink); + } + + [Fact] + public void Can_Update_Existing_NewsPage() + { + // Arrange + var repo = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Setup site definition + var siteDefinitionRepo = Services.GetRequiredService(); + siteDefinitionRepo.Save(new SiteDefinition() + { + Name = "TestSite", + StartPage = startPage.ContentLink, + SiteUrl = new Uri("http://localhost"), + }); + + var allSites = siteDefinitionRepo.List(); + var site = allSites.First(); + + // Create NewsPage + var news = repo.GetDefault(site.StartPage); + news.Name = "Original Name"; + news.Title = "Original Title"; + var savedRef = repo.Save(news, SaveAction.Publish, AccessLevel.NoAccess); + + // Act (Update) + var writable = repo.Get(savedRef).CreateWritableClone() as NewsPage; + writable!.Title = "Updated Title"; + repo.Save(writable, SaveAction.Publish, AccessLevel.NoAccess); + + // Assert + var loaded = repo.Get(savedRef); + Assert.Equal("Updated Title", loaded.Title); + } + + [Fact] + public void Cannot_Get_Wrong_Content_Type() + { + // Arrange + var repo = Services.GetRequiredService(); + + // Import test data + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var episerverDataFile = Path.Combine(basePath, "DefaultSiteContent.episerverdata"); + var dataImporter = Services.GetRequiredService(); + /* TODO: + OptimizelyTestContainers.Tests.NewsPageNegativeTests.Cannot_Save_NewsPage_Without_Name (10s 443ms): Error Message: + System.Exception : Failed to Deserialize object to Dynamic Data Store. BinaryFormatter serialization and deserial + ization have been removed. See https://aka.ms/binaryformatter for more information. + Stack Trace: + at OptimizelyTestContainers.Tests.OptimizelyDataImporter.Import(String importFilePath) in D:\Git\Valtech\Optimi + zelyTestContainers\src\OptimizelyTestContainers.Tests\OptimizelyDataImporter.cs:line 35 + + */ + dataImporter.Import(episerverDataFile); + + var startPage = repo.GetChildren(ContentReference.RootPage).First(); + + // Act & Assert - Try to get StartPage as NewsPage + Assert.Throws(() => repo.Get(startPage.ContentLink)); + } +} diff --git a/src/OptimizelyTestContainers.Tests/OptimizelyDataImporter.cs b/src/OptimizelyTestContainers.Tests/OptimizelyDataImporter.cs index 79dd2bf..9e33f4a 100644 --- a/src/OptimizelyTestContainers.Tests/OptimizelyDataImporter.cs +++ b/src/OptimizelyTestContainers.Tests/OptimizelyDataImporter.cs @@ -9,10 +9,11 @@ public class OptimizelyDataImporter(ILogger logger, IDat { public void Import(string importFilePath) { + /* contentEvents.PublishedContent += (s, e) => { logger.LogInformation("Published: {ContentName}", e.Content.Name); - }; + };*/ using var stream = File.OpenRead(importFilePath); @@ -28,22 +29,25 @@ public void Import(string importFilePath) var importLog = dataImporter.Import(stream, ContentReference.RootPage, options); var errors = importLog.Errors.ToList(); - var warnings = importLog.Warnings.ToList(); + - if (errors.Count != 0) + if (errors.Count > 0) { - throw new Exception(errors.First()); + throw new AggregateException(errors.Select(err => new Exception(err))); } + /* + + var warnings = importLog.Warnings.ToList(); if (warnings.Count == 0) { return; } - + foreach (var warning in warnings) { logger.LogWarning(warning); Console.WriteLine(warning); - } + }*/ } } \ No newline at end of file diff --git a/src/OptimizelyTestContainers.Tests/OptimizelyDataImporterTests.cs b/src/OptimizelyTestContainers.Tests/OptimizelyDataImporterTests.cs new file mode 100644 index 0000000..d00b221 --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/OptimizelyDataImporterTests.cs @@ -0,0 +1,196 @@ +using EPiServer.Core; +using EPiServer.Core.Transfer; +using EPiServer.Enterprise; +using Microsoft.Extensions.Logging; +using Moq; + +namespace OptimizelyTestContainers.Tests; + +public class OptimizelyDataImporterTests +{ + private readonly Mock> _mockLogger = new(); + private readonly Mock _mockDataImporter = new(); + private readonly Mock _mockContentEvents = new(); + + [Fact] + public void Import_Should_Subscribe_To_PublishedContent_Event() + { + // Arrange + var importer = new OptimizelyDataImporter(_mockLogger.Object, _mockDataImporter.Object, _mockContentEvents.Object); + var mockImportLog = CreateMockImportLog(); + var tempFile = CreateTempImportFile(); + + _mockDataImporter.Setup(x => x.Import(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockImportLog); + + // Act + importer.Import(tempFile); + + // Assert + _mockContentEvents.VerifyAdd(x => x.PublishedContent += It.IsAny>(), Times.Once); + + // Cleanup + File.Delete(tempFile); + } + + [Fact] + public void Import_Should_Call_DataImporter_With_Correct_Options() + { + // Arrange + var importer = new OptimizelyDataImporter(_mockLogger.Object, _mockDataImporter.Object, _mockContentEvents.Object); + var mockImportLog = CreateMockImportLog(); + var tempFile = CreateTempImportFile(); + + _mockDataImporter.Setup(x => x.Import(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockImportLog); + + // Act + importer.Import(tempFile); + + // Assert + _mockDataImporter.Verify(x => x.Import( + It.IsAny(), + ContentReference.RootPage, + It.Is(o => + o.KeepIdentity == true && + o.EnsureContentNameUniqueness == false && + o.ValidateDestination == true && + o.TransferType == TypeOfTransfer.Importing && + o.AutoCloseStream == true + )), Times.Once); + + // Cleanup + File.Delete(tempFile); + } + + [Fact] + public void Import_Should_Throw_Exception_When_Errors_Present() + { + // Arrange + var importer = new OptimizelyDataImporter(_mockLogger.Object, _mockDataImporter.Object, _mockContentEvents.Object); + var mockImportLog = CreateMockImportLog(errors: new List { "Test error message" }); + var tempFile = CreateTempImportFile(); + + _mockDataImporter.Setup(x => x.Import(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockImportLog); + + // Act & Assert + var exception = Assert.Throws(() => importer.Import(tempFile)); + Assert.Equal("Test error message", exception.Message); + + // Cleanup + File.Delete(tempFile); + } + + [Fact] + public void Import_Should_Log_Warnings_When_Present() + { + // Arrange + var importer = new OptimizelyDataImporter(_mockLogger.Object, _mockDataImporter.Object, _mockContentEvents.Object); + var mockImportLog = CreateMockImportLog(warnings: new List { "Test warning 1", "Test warning 2" }); + var tempFile = CreateTempImportFile(); + + _mockDataImporter.Setup(x => x.Import(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockImportLog); + + // Act + importer.Import(tempFile); + + // Assert + _mockLogger.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("Test warning 1")), + It.IsAny(), + It.IsAny>()), + Times.Once); + + _mockLogger.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("Test warning 2")), + It.IsAny(), + It.IsAny>()), + Times.Once); + + // Cleanup + File.Delete(tempFile); + } + + [Fact] + public void Import_Should_Not_Log_Warnings_When_None_Present() + { + // Arrange + var importer = new OptimizelyDataImporter(_mockLogger.Object, _mockDataImporter.Object, _mockContentEvents.Object); + var mockImportLog = CreateMockImportLog(); + var tempFile = CreateTempImportFile(); + + _mockDataImporter.Setup(x => x.Import(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockImportLog); + + // Act + importer.Import(tempFile); + + // Assert + _mockLogger.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + + // Cleanup + File.Delete(tempFile); + } + + [Fact] + public void Import_Should_Throw_FileNotFoundException_When_File_Does_Not_Exist() + { + // Arrange + var importer = new OptimizelyDataImporter(_mockLogger.Object, _mockDataImporter.Object, _mockContentEvents.Object); + var nonExistentFile = Path.Combine(Path.GetTempPath(), $"nonexistent_{Guid.NewGuid()}.episerverdata"); + + // Act & Assert + Assert.Throws(() => importer.Import(nonExistentFile)); + } + + [Fact] + public void Import_Should_Complete_Successfully_With_No_Errors_Or_Warnings() + { + // Arrange + var importer = new OptimizelyDataImporter(_mockLogger.Object, _mockDataImporter.Object, _mockContentEvents.Object); + var mockImportLog = CreateMockImportLog(); + var tempFile = CreateTempImportFile(); + + _mockDataImporter.Setup(x => x.Import(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockImportLog); + + // Act + var exception = Record.Exception(() => importer.Import(tempFile)); + + // Assert + Assert.Null(exception); + + // Cleanup + File.Delete(tempFile); + } + + private string CreateTempImportFile() + { + var tempFile = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.episerverdata"); + File.WriteAllText(tempFile, "test content"); + return tempFile; + } + + private ITransferLog CreateMockImportLog(List? errors = null, List? warnings = null) + { + var mockLog = new Mock(); + mockLog.SetupGet(x => x.Errors).Returns(errors ?? []); + mockLog.SetupGet(x => x.Warnings).Returns(warnings ?? []); + return mockLog.Object; + } +} \ No newline at end of file diff --git a/src/OptimizelyTestContainers.Tests/OptimizelyTestContainers.Tests.csproj b/src/OptimizelyTestContainers.Tests/OptimizelyTestContainers.Tests.csproj index 4702198..9a33a34 100644 --- a/src/OptimizelyTestContainers.Tests/OptimizelyTestContainers.Tests.csproj +++ b/src/OptimizelyTestContainers.Tests/OptimizelyTestContainers.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable false @@ -20,11 +20,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/OptimizelyTestContainers.Tests/Properties/AssemblyInfo.cs b/src/OptimizelyTestContainers.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b7bf16a --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Xunit; + +// We do not want integration tests to run in parallel at all to avoid deadlocks and database disposing itself multiple times +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/src/OptimizelyTestContainers.Tests/StartPageIntegrationTests.cs b/src/OptimizelyTestContainers.Tests/StartPageIntegrationTests.cs new file mode 100644 index 0000000..f367a72 --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/StartPageIntegrationTests.cs @@ -0,0 +1,83 @@ +using System.ComponentModel.DataAnnotations; +using EPiServer.Core; +using EPiServer.DataAbstraction; +using EPiServer.DataAnnotations; +using OptimizelyTestContainers.Tests.Models.Pages; + +namespace OptimizelyTestContainers.Tests; + +public class StartPageTests +{ + [Fact] + public void StartPage_Should_Have_ContentType_Attribute() + { + // Arrange & Act + var attribute = typeof(StartPage).GetCustomAttributes(typeof(ContentTypeAttribute), false) + .FirstOrDefault() as ContentTypeAttribute; + + // Assert + Assert.NotNull(attribute); + Assert.Equal(Guid.Parse("19671657-B684-4D95-A61F-8DD4FE60D559"), Guid.Parse(attribute.GUID)); + } + + [Fact] + public void StartPage_Should_Inherit_From_PageData() + { + // Arrange & Act + var isPageData = typeof(PageData).IsAssignableFrom(typeof(StartPage)); + + // Assert + Assert.True(isPageData); + } + + [Fact] + public void MainContentArea_Property_Should_Have_Display_Attribute() + { + // Arrange + var property = typeof(StartPage).GetProperty(nameof(StartPage.MainContentArea)); + + // Act + var displayAttribute = property?.GetCustomAttributes(typeof(DisplayAttribute), false) + .FirstOrDefault() as DisplayAttribute; + + // Assert + Assert.NotNull(displayAttribute); + Assert.Equal(SystemTabNames.Content, displayAttribute.GroupName); + Assert.Equal(320, displayAttribute.Order); + } + + [Fact] + public void MainContentArea_Property_Should_Have_CultureSpecific_Attribute() + { + // Arrange + var property = typeof(StartPage).GetProperty(nameof(StartPage.MainContentArea)); + + // Act + var attribute = property?.GetCustomAttributes(typeof(CultureSpecificAttribute), false) + .FirstOrDefault(); + + // Assert + Assert.NotNull(attribute); + } + + [Fact] + public void MainContentArea_Property_Should_Be_Virtual() + { + // Arrange + var property = typeof(StartPage).GetProperty(nameof(StartPage.MainContentArea)); + + // Act & Assert + Assert.NotNull(property); + Assert.True(property.GetMethod?.IsVirtual); + } + + [Fact] + public void StartPage_Can_Be_Instantiated() + { + // Act + var startPage = new StartPage(); + + // Assert + Assert.NotNull(startPage); + } +} \ No newline at end of file diff --git a/src/OptimizelyTestContainers.Tests/StartupTests.cs b/src/OptimizelyTestContainers.Tests/StartupTests.cs new file mode 100644 index 0000000..bf07e4b --- /dev/null +++ b/src/OptimizelyTestContainers.Tests/StartupTests.cs @@ -0,0 +1,138 @@ +using EPiServer; +using EPiServer.Scheduler; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Moq; + +namespace OptimizelyTestContainers.Tests; + +public class StartupTests +{ + [Fact] + public void ConfigureServices_Should_Add_CMS_Services() + { + // Arrange + var services = new ServiceCollection(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + // Act + startup.ConfigureServices(services); + + // Assert - Check that core CMS services are registered + Assert.Contains(services, s => s.ServiceType == typeof(IContentRepository)); + Assert.Contains(services, s => s.ServiceType == typeof(IContentLoader)); + } + + [Fact] + public void ConfigureServices_In_Development_Should_Configure_Scheduler_Options() + { + // Arrange + var services = new ServiceCollection(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Development); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + // Act + startup.ConfigureServices(services); + + // Assert - Check that scheduler options are configured + var serviceProvider = services.BuildServiceProvider(); + var schedulerOptions = serviceProvider.GetService>(); + Assert.NotNull(schedulerOptions); + Assert.False(schedulerOptions.Value.Enabled); + } + + [Fact] + public void ConfigureServices_In_Production_Should_Not_Configure_Scheduler_Options() + { + // Arrange + var services = new ServiceCollection(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + // Act + startup.ConfigureServices(services); + + // Assert - Scheduler should use default configuration (enabled) + var serviceProvider = services.BuildServiceProvider(); + var schedulerOptions = serviceProvider.GetService>(); + + // In production, scheduler is not explicitly disabled, so it should be enabled by default + Assert.NotNull(schedulerOptions); + } + + [Fact] + public void Configure_Should_Setup_Middleware_Pipeline() + { + // Arrange + var mockAppBuilder = new Mock(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + // Setup for middleware chain + mockAppBuilder.Setup(x => x.Use(It.IsAny>())) + .Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.New()).Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.ApplicationServices).Returns(new ServiceCollection().BuildServiceProvider()); + + // Act + startup.Configure(mockAppBuilder.Object, mockEnvironment.Object); + + // Assert + mockAppBuilder.Verify(x => x.Use(It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public void Configure_In_Development_Should_Use_DeveloperExceptionPage() + { + // Arrange + var mockAppBuilder = new Mock(); + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Development); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + var startup = new Startup(mockEnvironment.Object); + + mockAppBuilder.Setup(x => x.Use(It.IsAny>())) + .Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.New()).Returns(mockAppBuilder.Object); + mockAppBuilder.Setup(x => x.ApplicationServices).Returns(new ServiceCollection().BuildServiceProvider()); + + // Act + startup.Configure(mockAppBuilder.Object, mockEnvironment.Object); + + // Assert - Middleware should be added + mockAppBuilder.Verify(x => x.Use(It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public void Startup_Constructor_Should_Accept_WebHostEnvironment() + { + // Arrange + var mockEnvironment = new Mock(); + mockEnvironment.Setup(x => x.EnvironmentName).Returns(Environments.Production); + mockEnvironment.Setup(x => x.ContentRootPath).Returns(Path.GetTempPath()); + + // Act + var startup = new Startup(mockEnvironment.Object); + + // Assert + Assert.NotNull(startup); + } +}