diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
deleted file mode 100644
index f0b3b29ea33..00000000000
--- a/.github/workflows/integration_tests.yml
+++ /dev/null
@@ -1,90 +0,0 @@
-name: Integration Tests
-
-on:
- # Manual trigger.
- workflow_dispatch:
- pull_request_review:
- types: [submitted]
- # Run it on main and release pushes too, in case we merge from a branch that's not up-to-date with the target branch
- # and breaks something after merge (or if we push to main).
- push:
- paths-ignore:
- - '**/*.md'
- - 'mkdocs.yml'
- - 'src/docs/**/*'
- branches: [ main, release/** ]
-
-env:
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
- DOTNET_CLI_TELEMETRY_OPTOUT: true
-
-permissions:
- contents: read
-
-jobs:
- test_s3:
- name: Integration Tests - S3
- if: github.event_name == 'workflow_dispatch' ||
- github.event_name == 'push' ||
- github.event.review.state == 'APPROVED' ||
- github.event.review.state == 'CHANGES_REQUESTED'
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: ./.github/actions/setup-dotnet
- - name: Build
- run: |
- dotnet build ./test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj -c Release /p:NuGetAudit=false
- - name: Cache S3Mock Docker image
- id: cache-s3mock
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
- with:
- path: /tmp/s3mock-image.tar
- key: docker-s3mock-latest
- - name: Load or pull S3Mock image
- run: |
- if [ -f /tmp/s3mock-image.tar ]; then
- docker load -i /tmp/s3mock-image.tar
- else
- docker pull adobe/s3mock
- docker save adobe/s3mock -o /tmp/s3mock-image.tar
- fi
- - name: Start S3Mock
- run: |
- docker run -d --name s3mock -p 9090:9090 adobe/s3mock
- echo "Waiting for S3Mock to be ready..."
- for i in $(seq 1 30); do
- if curl -sf http://127.0.0.1:9090/ > /dev/null 2>&1; then
- echo "S3Mock is ready."
- break
- fi
- sleep 1
- done
- - name: S3 Integration Tests
- env:
- S3_EMULATOR_URL: http://127.0.0.1:9090
- run: |
- dotnet test --project ./test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj -c Release --no-build
- test_azure_blob:
- name: Integration Tests - Azure Blob Storage (Azurite)
- if: github.event_name == 'workflow_dispatch' ||
- github.event_name == 'push' ||
- github.event.review.state == 'APPROVED' ||
- github.event.review.state == 'CHANGES_REQUESTED'
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: ./.github/actions/setup-dotnet
- - name: Build
- run: |
- dotnet build -c Release ./test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj /p:NuGetAudit=false
- - name: Start Azurite
- run: |
- docker run -d --name azurite -p 10000:10000 -p 10004:10004 ghcr.io/skrypt/azurite-adls-gen2:latest azurite --blobHost 0.0.0.0 --dfsHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck
- - name: Azure Blob Integration Tests
- env:
- AZURITE_CONNECTION_STRING: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
- AZURITE_DFS_ENDPOINT: "http://127.0.0.1:10004/devstoreaccount1"
- run: |
- dotnet test ./test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj -c Release --no-build
-
diff --git a/.github/workflows/main_ci.yml b/.github/workflows/main_ci.yml
index 1f8a01d74cb..16a224235fe 100644
--- a/.github/workflows/main_ci.yml
+++ b/.github/workflows/main_ci.yml
@@ -60,3 +60,7 @@ jobs:
src/OrchardCore.Mvc.Web/App_Data_Tests/logs
test/OrchardCore.Tests.Functional/traces
retention-days: 3
+ - name: Integration Tests
+ if: matrix.os == 'ubuntu-24.04'
+ run: |
+ dotnet test --project ./test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj -c Release --no-build
diff --git a/.github/workflows/pr_ci.yml b/.github/workflows/pr_ci.yml
index a81206a5af5..7d6e6180b67 100644
--- a/.github/workflows/pr_ci.yml
+++ b/.github/workflows/pr_ci.yml
@@ -57,3 +57,7 @@ jobs:
src/OrchardCore.Mvc.Web/App_Data_Tests/logs
test/OrchardCore.Tests.Functional/traces
retention-days: 3
+ - name: Integration Tests
+ if: matrix.os == 'ubuntu-24.04'
+ run: |
+ dotnet test --project ./test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj -c Release --no-build
diff --git a/.github/workflows/preview_ci.yml b/.github/workflows/preview_ci.yml
index 63e09f26b38..7b4d6a07490 100644
--- a/.github/workflows/preview_ci.yml
+++ b/.github/workflows/preview_ci.yml
@@ -64,6 +64,10 @@ jobs:
src/OrchardCore.Mvc.Web/App_Data_Tests/logs
test/OrchardCore.Tests.Functional/traces
retention-days: 3
+ - name: Integration Tests
+ if: matrix.os == 'ubuntu-24.04'
+ run: |
+ dotnet test --project ./test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj -c Release --no-build
- name: Deploy preview NuGet packages
if: steps.check-publish.outputs.should-publish == 'true'
run: |
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f45066c4d47..5192fb50fbd 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -65,6 +65,7 @@
+
@@ -111,4 +112,4 @@
-
+
\ No newline at end of file
diff --git a/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen1Tests.cs b/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen1Tests.cs
index f24903d88cf..f14de759527 100644
--- a/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen1Tests.cs
+++ b/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen1Tests.cs
@@ -3,7 +3,4 @@ namespace OrchardCore.Tests.Integration.AzureBlob;
///
/// Runs all tests with flat-namespace (Gen1) behavior.
///
-public sealed class BlobFileStoreGen1Tests : BlobFileStoreTestsBase
-{
- protected override bool IsHnsEnabled => false;
-}
+public sealed class BlobFileStoreGen1Tests : BlobFileStoreTestsBase;
diff --git a/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen2Tests.cs b/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen2Tests.cs
index 801b5b41565..68791d971d3 100644
--- a/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen2Tests.cs
+++ b/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreGen2Tests.cs
@@ -11,7 +11,7 @@ public sealed class BlobFileStoreGen2Tests : BlobFileStoreTestsBase
{
protected override bool IsHnsEnabled => true;
- [AzuriteFact]
+ [Fact]
public async Task CreateDirectory_Nested_CreatesIntermediateDirectories()
{
await TryCreateDirectoryAsync("a/b/c");
@@ -21,7 +21,7 @@ public async Task CreateDirectory_Nested_CreatesIntermediateDirectories()
Assert.NotNull(await GetDirectoryInfoAsync("a"));
}
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryContent_Flat_IncludesGen2Directories()
{
await TryCreateDirectoryAsync("flat-gen2");
@@ -41,7 +41,7 @@ public async Task GetDirectoryContent_Flat_IncludesGen2Directories()
Assert.Contains(entries, e => !e.IsDirectory && e.Name == "nested.txt");
}
- [AzuriteFact]
+ [Fact]
public async Task MoveFile_IsAtomic()
{
// Gen2 move uses DataLake RenameAsync which is an atomic server-side operation.
@@ -56,7 +56,7 @@ public async Task MoveFile_IsAtomic()
Assert.Equal("atomic", await ReadFileContentAsync("atomic-dst.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryInfo_AfterDeletingDirectory_ReturnsNull()
{
await TryCreateDirectoryAsync("temp-dir");
@@ -67,7 +67,7 @@ public async Task GetDirectoryInfo_AfterDeletingDirectory_ReturnsNull()
Assert.Null(await GetDirectoryInfoAsync("temp-dir"));
}
- [AzuriteFact]
+ [Fact]
public async Task CreateDirectory_EmptyDirectory_ExistsWithNoContent()
{
await TryCreateDirectoryAsync("empty-gen2-dir");
diff --git a/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreTestsBase.cs b/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreTestsBase.cs
index 06fa974fcea..4d3fb28b789 100644
--- a/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreTestsBase.cs
+++ b/test/OrchardCore.Tests.Integration/AzureBlob/BlobFileStoreTestsBase.cs
@@ -4,6 +4,7 @@
using OrchardCore.FileStorage;
using OrchardCore.FileStorage.AzureBlob;
using OrchardCore.Modules;
+using Testcontainers.Azurite;
using Xunit;
namespace OrchardCore.Tests.Integration.AzureBlob;
@@ -14,39 +15,40 @@ namespace OrchardCore.Tests.Integration.AzureBlob;
///
public abstract class BlobFileStoreTestsBase : IAsyncLifetime
{
- private const string EnvVar = "AZURITE_CONNECTION_STRING";
- private const string DfsEnvVar = "AZURITE_DFS_ENDPOINT";
+ private readonly AzuriteContainer _azuriteContainer = new AzuriteBuilder("mcr.microsoft.com/azure-storage/azurite:3.30.0")
+ .WithCommand("--skipApiVersionCheck")
+ .Build();
- protected abstract bool IsHnsEnabled { get; }
+ protected virtual bool IsHnsEnabled { get; }
private BlobFileStore _store;
private BlobContainerClient _containerClient;
private string _containerName;
- protected static string GetConnectionString()
- => System.Environment.GetEnvironmentVariable(EnvVar);
-
public async ValueTask InitializeAsync()
{
- var connectionString = GetConnectionString();
+ await _azuriteContainer.StartAsync()
+ .ConfigureAwait(false);
+
_containerName = $"test-{Guid.NewGuid():N}";
- var options = new TestBlobStorageOptions
+ _containerClient = new BlobContainerClient(_azuriteContainer.GetConnectionString(), _containerName);
+
+ await _containerClient.CreateIfNotExistsAsync();
+
+ var clock = Mock.Of(c => c.UtcNow == DateTime.UtcNow);
+ var contentTypeProvider = new FileExtensionContentTypeProvider();
+
+ var blobStorageOptions = new TestBlobStorageOptions
{
- ConnectionString = connectionString,
+ ConnectionString = _azuriteContainer.GetConnectionString(),
ContainerName = _containerName,
BasePath = "",
UseHierarchicalNamespace = IsHnsEnabled,
- DfsEndpoint = System.Environment.GetEnvironmentVariable(DfsEnvVar),
};
- _containerClient = new BlobContainerClient(connectionString, _containerName);
- await _containerClient.CreateIfNotExistsAsync();
-
- var clock = Mock.Of(c => c.UtcNow == DateTime.UtcNow);
- var contentTypeProvider = new FileExtensionContentTypeProvider();
+ _store = new BlobFileStore(blobStorageOptions, clock, contentTypeProvider);
- _store = new BlobFileStore(options, clock, contentTypeProvider);
await _store.EnsureCapabilitiesAsync();
}
@@ -93,13 +95,13 @@ protected Task MoveFileAsync(string oldPath, string newPath)
// -- Capabilities --
- [AzuriteFact]
+ [Fact]
public void EnsureCapabilities_SetsHnsFlag()
{
Assert.Equal(IsHnsEnabled, _store.Capabilities.HasHierarchicalNamespace);
}
- [AzuriteFact]
+ [Fact]
public void EnsureCapabilities_SetsAtomicMoveFlag()
{
Assert.Equal(IsHnsEnabled, _store.Capabilities.SupportsAtomicMove);
@@ -107,14 +109,14 @@ public void EnsureCapabilities_SetsAtomicMoveFlag()
// -- File operations --
- [AzuriteFact]
+ [Fact]
public async Task CreateFile_ReturnsPath()
{
var result = await CreateTestFileAsync("folder/file.txt");
Assert.Equal("folder/file.txt", result);
}
- [AzuriteFact]
+ [Fact]
public async Task GetFileInfo_ReturnsCorrectMetadata()
{
var content = "hello world";
@@ -129,14 +131,14 @@ public async Task GetFileInfo_ReturnsCorrectMetadata()
Assert.False(info.IsDirectory);
}
- [AzuriteFact]
+ [Fact]
public async Task GetFileInfo_NonExistent_ReturnsNull()
{
var info = await _store.GetFileInfoAsync("does-not-exist.txt");
Assert.Null(info);
}
- [AzuriteFact]
+ [Fact]
public async Task GetFileStream_ReturnsContent()
{
var expected = "stream content test";
@@ -147,14 +149,14 @@ public async Task GetFileStream_ReturnsContent()
Assert.Equal(expected, actual);
}
- [AzuriteFact]
+ [Fact]
public async Task GetFileStream_NonExistent_Throws()
{
await Assert.ThrowsAsync(
() => _store.GetFileStreamAsync("no-such-file.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task DeleteFile_ReturnsTrue()
{
await CreateTestFileAsync("delete-me.txt");
@@ -165,14 +167,14 @@ public async Task DeleteFile_ReturnsTrue()
Assert.Null(await _store.GetFileInfoAsync("delete-me.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task DeleteFile_NonExistent_ReturnsFalse()
{
var result = await _store.TryDeleteFileAsync("ghost.txt");
Assert.False(result);
}
- [AzuriteFact]
+ [Fact]
public async Task CopyFile_CreatesNewFile()
{
var content = "copy me";
@@ -184,7 +186,7 @@ public async Task CopyFile_CreatesNewFile()
Assert.Equal(content, await ReadFileContentAsync("copied.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task CopyFile_SamePath_Throws()
{
await CreateTestFileAsync("same.txt");
@@ -193,7 +195,7 @@ await Assert.ThrowsAsync(
() => _store.CopyFileAsync("same.txt", "same.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task CreateFile_OverwriteTrue_Succeeds()
{
await CreateTestFileAsync("overwrite.txt", "v1");
@@ -205,7 +207,7 @@ public async Task CreateFile_OverwriteTrue_Succeeds()
Assert.Equal("v2", content);
}
- [AzuriteFact]
+ [Fact]
public async Task CreateFile_OverwriteFalse_Throws()
{
await CreateTestFileAsync("no-overwrite.txt");
@@ -217,7 +219,7 @@ await Assert.ThrowsAsync(
// -- Directory operations --
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryInfo_Root_ReturnsEntry()
{
var info = await _store.GetDirectoryInfoAsync(string.Empty);
@@ -226,7 +228,7 @@ public async Task GetDirectoryInfo_Root_ReturnsEntry()
Assert.True(info.IsDirectory);
}
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryInfo_Existing_ReturnsEntry()
{
await _store.TryCreateDirectoryAsync("my-folder");
@@ -238,7 +240,7 @@ public async Task GetDirectoryInfo_Existing_ReturnsEntry()
Assert.Equal("my-folder", info.Path);
}
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryInfo_NonExistent_ReturnsNull()
{
var info = await _store.GetDirectoryInfoAsync("no-such-folder");
@@ -249,7 +251,7 @@ public async Task GetDirectoryInfo_NonExistent_ReturnsNull()
Assert.Null(info);
}
- [AzuriteFact]
+ [Fact]
public async Task CreateDirectory_NewDirectory_Succeeds()
{
var result = await _store.TryCreateDirectoryAsync("new-dir");
@@ -260,7 +262,7 @@ public async Task CreateDirectory_NewDirectory_Succeeds()
Assert.NotNull(info);
}
- [AzuriteFact]
+ [Fact]
public async Task DeleteDirectory_WithContents_DeletesAll()
{
await _store.TryCreateDirectoryAsync("dir-to-delete");
@@ -274,14 +276,14 @@ public async Task DeleteDirectory_WithContents_DeletesAll()
Assert.Null(await _store.GetFileInfoAsync("dir-to-delete/file2.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task DeleteDirectory_NonExistent_ReturnsFalse()
{
var result = await _store.TryDeleteDirectoryAsync("phantom-dir");
Assert.False(result);
}
- [AzuriteFact]
+ [Fact]
public async Task DeleteDirectory_Root_Throws()
{
await Assert.ThrowsAsync(
@@ -290,7 +292,7 @@ await Assert.ThrowsAsync(
// -- Move --
- [AzuriteFact]
+ [Fact]
public async Task MoveFile_MovesToNewPath()
{
var content = "move me";
@@ -302,7 +304,7 @@ public async Task MoveFile_MovesToNewPath()
Assert.Equal(content, await ReadFileContentAsync("dst.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task MoveFile_AcrossDirectories()
{
await _store.TryCreateDirectoryAsync("dir-a");
@@ -317,7 +319,7 @@ public async Task MoveFile_AcrossDirectories()
// -- Directory content listing --
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryContent_ListsFilesAndDirs()
{
await CreateTestFileAsync("root-file.txt");
@@ -334,7 +336,7 @@ public async Task GetDirectoryContent_ListsFilesAndDirs()
Assert.Contains(entries, e => e.Name == "sub-dir" && e.IsDirectory);
}
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryContent_ExcludesMarkerFiles()
{
await _store.TryCreateDirectoryAsync("marker-test");
@@ -349,7 +351,7 @@ public async Task GetDirectoryContent_ExcludesMarkerFiles()
Assert.DoesNotContain(entries, e => e.Name == "OrchardCore.Media.txt");
}
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryContent_Flat_ListsNestedContent()
{
await CreateTestFileAsync("flat/a.txt");
@@ -365,7 +367,7 @@ public async Task GetDirectoryContent_Flat_ListsNestedContent()
Assert.Contains(entries, e => !e.IsDirectory && e.Name == "b.txt");
}
- [AzuriteFact]
+ [Fact]
public async Task CreateDirectory_AlreadyExists_ReturnsFalse()
{
await _store.TryCreateDirectoryAsync("existing-dir");
@@ -385,14 +387,14 @@ public async Task CreateDirectory_AlreadyExists_ReturnsFalse()
}
}
- [AzuriteFact]
+ [Fact]
public async Task MoveFile_NonExistent_Throws()
{
await Assert.ThrowsAsync(
() => _store.MoveFileAsync("no-such-file.txt", "destination.txt"));
}
- [AzuriteFact]
+ [Fact]
public async Task DeleteDirectory_WithNestedSubdirectories_DeletesAll()
{
await _store.TryCreateDirectoryAsync("parent");
@@ -408,7 +410,7 @@ public async Task DeleteDirectory_WithNestedSubdirectories_DeletesAll()
Assert.Null(await _store.GetDirectoryInfoAsync("parent"));
}
- [AzuriteFact]
+ [Fact]
public async Task GetDirectoryContent_Subdirectory_ListsOnlyDirectChildren()
{
await _store.TryCreateDirectoryAsync("listing");
@@ -428,7 +430,7 @@ public async Task GetDirectoryContent_Subdirectory_ListsOnlyDirectChildren()
Assert.DoesNotContain(entries, e => e.Name == "file-b.txt");
}
- [AzuriteFact]
+ [Fact]
public async Task MoveFile_PreservesContent()
{
var content = "preserve this content across move";
@@ -440,27 +442,3 @@ public async Task MoveFile_PreservesContent()
Assert.Equal(content, actual);
}
}
-
-///
-/// Concrete for testing (the base class is abstract).
-///
-internal sealed class TestBlobStorageOptions : BlobStorageOptions;
-
-///
-/// Skips the test when the Azurite connection string is not configured.
-/// Set the AZURITE_CONNECTION_STRING environment variable to run these tests.
-///
-[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
-internal sealed class AzuriteFactAttribute : FactAttribute
-{
- public AzuriteFactAttribute(
- [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = null,
- [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = -1)
- : base(sourceFilePath, sourceLineNumber)
- {
- if (string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("AZURITE_CONNECTION_STRING")))
- {
- Skip = "Azurite is not configured. Set AZURITE_CONNECTION_STRING to run this test.";
- }
- }
-}
diff --git a/test/OrchardCore.Tests.Integration/AzureBlob/TestBlobStorageOptions.cs b/test/OrchardCore.Tests.Integration/AzureBlob/TestBlobStorageOptions.cs
new file mode 100644
index 00000000000..1d5bfed27bf
--- /dev/null
+++ b/test/OrchardCore.Tests.Integration/AzureBlob/TestBlobStorageOptions.cs
@@ -0,0 +1,5 @@
+using OrchardCore.FileStorage.AzureBlob;
+
+namespace OrchardCore.Tests.Integration.AzureBlob;
+
+internal sealed class TestBlobStorageOptions : BlobStorageOptions;
diff --git a/test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj b/test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj
index 03be9dffba4..218c1afd423 100644
--- a/test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj
+++ b/test/OrchardCore.Tests.Integration/OrchardCore.Tests.Integration.csproj
@@ -14,6 +14,7 @@
+