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