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 @@ +