diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml deleted file mode 100644 index bef1750..0000000 --- a/.github/workflows/build-macos.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: build-macos - -on: - push: - paths-ignore: - - "**.md" - - LICENSE - branches: - - "master" - pull_request: - paths-ignore: - - "**.md" - - LICENSE - branches: - - master - -jobs: - build-and-test: - runs-on: macos-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Init - run: chmod +x ./build.sh - - - name: Install NuGet - uses: NuGet/setup-nuget@v1.0.5 - - - name: Setup Testspace - uses: testspace-com/setup-testspace@v1 - with: - domain: ${{github.repository_owner}} - - - name: Install .NET 8 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "8.0.x" - - - name: Install .NET 9 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "9.0.x" - - - name: Build - run: ./build.sh --target build - - - name: Run Tests - run: ./build.sh --target tests --exclusive - - - name: Push result to Testspace server - run: | - testspace [macos]**/*.trx - if: always() \ No newline at end of file diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml deleted file mode 100644 index f82fb3e..0000000 --- a/.github/workflows/build-ubuntu.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: build-ubuntu - -on: - push: - paths-ignore: - - "**.md" - - LICENSE - branches: - - "master" - pull_request: - paths-ignore: - - "**.md" - - LICENSE - branches: - - master - -jobs: - build-and-test: - runs-on: ubuntu-20.04 - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Init - run: chmod +x ./build.sh - - - name: Install NuGet - uses: NuGet/setup-nuget@v1.0.5 - - - name: Setup Testspace - uses: testspace-com/setup-testspace@v1 - with: - domain: ${{github.repository_owner}} - - - name: Install .NET 8 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "8.0.x" - - - name: Install .NET 9 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "9.0.x" - - - name: Build - run: ./build.sh --target build - - - name: Run Tests - run: ./build.sh --target tests --skipFunctionalTest false --exclusive - - - name: Push result to Testspace server - run: | - testspace [linux]**/*.trx - if: always() \ No newline at end of file diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml deleted file mode 100644 index f41f8a9..0000000 --- a/.github/workflows/build-windows.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: build-windows - -on: - push: - paths-ignore: - - "**.md" - - LICENSE - branches: - - "master" - pull_request: - paths-ignore: - - "**.md" - - LICENSE - branches: - - master - -jobs: - build-and-test: - runs-on: windows-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Testspace - uses: testspace-com/setup-testspace@v1 - with: - domain: ${{github.repository_owner}} - - - name: Install .NET 8 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "8.0.x" - - - name: Install .NET 9 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "9.0.x" - - - name: Build - run: .\build.ps1 --target build - - - name: Run Tests - run: .\build.ps1 --target tests --exclusive - - - name: Push result to Testspace server - run: | - testspace [windows]**/*.trx - if: always() \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a494c1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: "CI/CD Pipeline" + +on: + push: + paths-ignore: + - "**.md" + - LICENSE + branches: + - "master" + pull_request: + paths-ignore: + - "**.md" + - LICENSE + branches: + - master + - "feature/*" + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_NOLOGO: true + +jobs: + build-and-test: + name: "Build & Test (${{ matrix.name }})" + runs-on: ${{ matrix.os }} + env: + NUGET_PACKAGES: ${{ contains(matrix.os, 'windows') && format('{0}\.nuget\packages', github.workspace) || format('{0}/.nuget/packages', github.workspace) }} + + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + name: "Windows" + script: "./build.ps1" + + - os: ubuntu-22.04 + name: "Linux" + script: "./build.sh" + + - os: macos-latest + name: "macOS" + script: "./build.sh" + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for better caching + + - name: "Setup .NET SDK" + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + + - name: "Make build script executable" + if: runner.os != 'Windows' + run: chmod +x ./build.sh + + - name: "Cache NuGet packages" + uses: actions/cache@v4 + with: + path: ${{ runner.os == 'Windows' && format('{0}\.nuget\packages', github.workspace) || format('{0}/.nuget/packages', github.workspace) }} + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj', '**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: "Build" + run: ${{ matrix.script }} --target build + + - name: "Run Tests" + run: ${{ matrix.script }} --target tests --skipFunctionalTest ${{ runner.os == 'Linux' && 'false' || 'true' }} --exclusive + + - name: "Publish Test Results" + uses: dorny/test-reporter@v1 + if: success() || failure() + with: + name: 'Test Results (${{ matrix.name }})' + path: '**/TestResults/*.trx' + reporter: 'dotnet-trx' + fail-on-error: true + max-annotations: 50 + + - name: "Upload Test Artifacts" + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results-${{ matrix.name }} + path: | + **/*.trx + **/TestResults/**/* + retention-days: 7 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..2f7ccaf --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +name: Dependency Review + +on: + pull_request: + branches: + - master + - "feature/*" + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + name: "Dependency Review" + runs-on: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Dependency Review" + uses: actions/dependency-review-action@v4 + with: + # Fail the check if a vulnerability with 'moderate' severity or higher is found. + fail-on-severity: moderate + # Always post a summary of the check as a comment on the PR. + comment-summary-in-pr: always \ No newline at end of file diff --git a/.github/workflows/publish-dev-github.yml b/.github/workflows/publish-dev-github.yml new file mode 100644 index 0000000..141bea0 --- /dev/null +++ b/.github/workflows/publish-dev-github.yml @@ -0,0 +1,99 @@ +name: "Auto Publish to GitHub Packages" + +on: + push: + branches: + - master + paths-ignore: + - "**.md" + - LICENSE + - ".github/**" + - "docs/**" + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_NOLOGO: true + +jobs: + auto-publish: + name: "Auto Publish Development Build" + runs-on: ubuntu-22.04 + if: github.repository == 'localstack-dotnet/localstack-dotnet-client' + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + + permissions: + contents: read + packages: write + + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: "Setup .NET SDK" + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + + - name: "Cache NuGet packages" + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj', '**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: "Make build script executable" + run: chmod +x ./build.sh + + - name: "Build & Test" + run: ./build.sh --target tests --skipFunctionalTest true + + - name: "Setup GitHub Packages Authentication" + run: | + dotnet nuget add source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json \ + --name github-packages \ + --username ${{ github.actor }} \ + --password ${{ secrets.GITHUB_TOKEN }} \ + --store-password-in-clear-text + + - name: "Pack & Publish LocalStack.Client" + run: | + echo "🔨 Building and publishing LocalStack.Client package..." + ./build.sh --target nuget-pack-and-publish \ + --package-source github \ + --package-id LocalStack.Client \ + --use-directory-props-version true \ + --branch-name ${{ github.ref_name }} \ + --package-secret ${{ secrets.GITHUB_TOKEN }} + + - name: "Pack & Publish LocalStack.Client.Extensions" + run: | + echo "🔨 Building and publishing LocalStack.Client.Extensions package..." + ./build.sh --target nuget-pack-and-publish \ + --package-source github \ + --package-id LocalStack.Client.Extensions \ + --use-directory-props-version true \ + --branch-name ${{ github.ref_name }} \ + --package-secret ${{ secrets.GITHUB_TOKEN }} + + - name: "Upload Package Artifacts" + uses: actions/upload-artifact@v4 + with: + name: "dev-packages-${{ github.run_number }}" + path: | + artifacts/*.nupkg + artifacts/*.snupkg + retention-days: 7 + + - name: "Generate Build Summary" + run: | + echo "📦 Generating build summary..." + ./build.sh --target workflow-summary \ + --use-directory-props-version true \ + --branch-name ${{ github.ref_name }} diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 17d60ba..afb009e 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -1,4 +1,4 @@ -name: "publish-nuget" +name: "Manual Package Publishing" on: workflow_dispatch: @@ -10,10 +10,10 @@ on: type: choice description: Package Source required: true - default: "myget" + default: "nuget" options: - - myget - nuget + - github package-id: type: choice description: Package Id @@ -23,48 +23,111 @@ on: - LocalStack.Client - LocalStack.Client.Extensions +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_NOLOGO: true + jobs: - publish-nuget: - runs-on: ubuntu-20.04 + publish-manual: + name: "Publish to ${{ github.event.inputs.package-source }}" + runs-on: ubuntu-22.04 + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + + permissions: + contents: read + packages: write steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Init - run: chmod +x ./build.sh - - - name: Install NuGet - uses: NuGet/setup-nuget@v1.0.5 + - name: "Checkout" + uses: actions/checkout@v4 - - name: Install .NET 8 - uses: actions/setup-dotnet@v1 + - name: "Setup .NET SDK" + uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: | + 8.0.x + 9.0.x - - name: Install .NET 9 - uses: actions/setup-dotnet@v1 + - name: "Cache NuGet packages" + uses: actions/cache@v4 with: - dotnet-version: "9.0.x" + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj', '**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: "Make build script executable" + run: chmod +x ./build.sh + + - name: "Build & Test" + run: ./build.sh --target tests --skipFunctionalTest true - - name: Build & Test - run: ./build.sh + - name: "Print Package Information" + run: | + echo "📦 Package: ${{ github.event.inputs.package-id }}" + echo "🏷️ Version: ${{ github.event.inputs.package-version }}" + echo "🎯 Target: ${{ github.event.inputs.package-source }}" + echo "🔗 Repository: ${{ github.repository }}" - - name: "Print Version" + - name: "Setup GitHub Packages Authentication" + if: ${{ github.event.inputs.package-source == 'github' }} run: | - echo "Package Version: ${{ github.event.inputs.package-version }}" + dotnet nuget add source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json \ + --name github-packages \ + --username ${{ github.actor }} \ + --password ${{ secrets.GITHUB_TOKEN }} \ + --store-password-in-clear-text - - name: Remove Project Ref & Add latest pack + - name: "Remove Project Reference & Add Package Reference" if: ${{ github.event.inputs.package-id == 'LocalStack.Client.Extensions' }} - run: cd src/LocalStack.Client.Extensions/ && dotnet remove reference ../LocalStack.Client/LocalStack.Client.csproj && dotnet add package LocalStack.Client + run: | + cd src/LocalStack.Client.Extensions/ + + # Remove project reference + dotnet remove reference ../LocalStack.Client/LocalStack.Client.csproj + + # Add package reference based on target source + if [ "${{ github.event.inputs.package-source }}" == "github" ]; then + dotnet add package LocalStack.Client \ + --version ${{ github.event.inputs.package-version }} \ + --source github-packages + else + dotnet add package LocalStack.Client \ + --version ${{ github.event.inputs.package-version }} + fi - - name: Nuget Pack - run: ./build.sh --target nuget-pack --package-source ${{ github.event.inputs.package-source }} --package-id ${{ github.event.inputs.package-id }} --package-version ${{ github.event.inputs.package-version }} + - name: "Pack NuGet Package" + run: | + ./build.sh --target nuget-pack \ + --package-source ${{ github.event.inputs.package-source }} \ + --package-id ${{ github.event.inputs.package-id }} \ + --package-version ${{ github.event.inputs.package-version }} - - name: MyGet Push - if: ${{ github.event.inputs.package-source == 'myget' }} - run: ./build.sh --target nuget-push --package-source ${{ github.event.inputs.package-source }} --package-id ${{ github.event.inputs.package-id }} --package-version ${{ github.event.inputs.package-version }} --package-secret ${{secrets.MYGET_API_KEY}} + - name: "Publish to GitHub Packages" + if: ${{ github.event.inputs.package-source == 'github' }} + run: | + ./build.sh --target nuget-push \ + --package-source github \ + --package-id ${{ github.event.inputs.package-id }} \ + --package-version ${{ github.event.inputs.package-version }} \ + --package-secret ${{ secrets.GITHUB_TOKEN }} - - name: NuGet Push + - name: "Publish to NuGet.org" if: ${{ github.event.inputs.package-source == 'nuget' }} - run: ./build.sh --target nuget-push --package-source ${{ github.event.inputs.package-source }} --package-id ${{ github.event.inputs.package-id }} --package-version ${{ github.event.inputs.package-version }} --package-secret ${{secrets.NUGET_API_KEY}} \ No newline at end of file + run: | + ./build.sh --target nuget-push \ + --package-source nuget \ + --package-id ${{ github.event.inputs.package-id }} \ + --package-version ${{ github.event.inputs.package-version }} \ + --package-secret ${{ secrets.NUGET_API_KEY }} + + - name: "Upload Package Artifacts" + uses: actions/upload-artifact@v4 + with: + name: "packages-${{ github.event.inputs.package-id }}-${{ github.event.inputs.package-version }}" + path: | + artifacts/*.nupkg + artifacts/*.snupkg + retention-days: 30 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6d1c8..896aa33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # LocalStack .NET Client Change Log +### [v2.0.0-preview1](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v2.0.0-preview1) + +#### 1. Breaking Changes + +- **Framework Support Updates:** + - **Deprecated** support for **.NET Framework 4.6.2**. + - **Added** support for **.NET Framework 4.7.2** (required for AWS SDK v4 compatibility). + +#### 2. General + +- **AWS SDK v4 Migration:** + - **Complete migration** from AWS SDK for .NET v3 to v4. + - **AWSSDK.Core** minimum version set to **4.0.0.15**. + - **AWSSDK.Extensions.NETCore.Setup** updated to **4.0.2**. + - All 70+ AWS SDK service packages updated to v4.x series. + +- **Framework Support:** + - **.NET 9** + - **.NET 8** + - **.NET Standard 2.0** + - **.NET Framework 4.7.2** + +- **Testing Validation:** + - **1,099 total tests** passing across all target frameworks. + - Successfully tested with AWS SDK v4 across all supported .NET versions. + - Tested against following LocalStack versions: + - **v3.7.1** + - **v4.3.0** + +#### 3. Important Notes + +- **Preview Release**: This is a preview release for early adopters and testing. See the [v2.0.0 Roadmap & Migration Guide](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45) for the complete migration plan. +- **No API Changes**: LocalStack.NET public APIs remain unchanged. All changes are internal to support AWS SDK v4 compatibility. +- **Feedback Welcome**: Please report issues or feedback on [GitHub Issues](https://github.com/localstack-dotnet/localstack-dotnet-client/issues). +- **v2.x series requires AWS SDK v4**: This version is only compatible with AWS SDK for .NET v4.x packages. +- **Migration from v1.x**: Users upgrading from v1.x should ensure their projects reference AWS SDK v4 packages. +- **Framework Requirement**: .NET Framework 4.7.2 or higher is now required (upgrade from 4.6.2). + +--- + ### [v1.6.0](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.6.0) #### 1. General diff --git a/Directory.Build.props b/Directory.Build.props index 532556c..9ed3370 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,8 +5,8 @@ LocalStack.NET https://github.com/localstack-dotnet/localstack-dotnet-client localstack-dotnet-square.png - 1.6.0 - 1.4.0 + 2.0.0-preview1 + 2.0.0-preview1 true snupkg 13.0 diff --git a/Directory.Packages.props b/Directory.Packages.props index 99f4395..6863198 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,125 +1,125 @@ - - + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -146,8 +146,8 @@ - - + + @@ -159,7 +159,7 @@ runtime; build; native; contentfiles; analyzers - + \ No newline at end of file diff --git a/README.md b/README.md index 95224e5..51c4faa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LocalStack .NET Client ![Nuget](https://img.shields.io/nuget/dt/LocalStack.Client) [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) [![Space Metric](https://localstack-dotnet.testspace.com/spaces/232580/badge?token=bc6aa170f4388c662b791244948f6d2b14f16983)](https://localstack-dotnet.testspace.com/spaces/232580?utm_campaign=metric&utm_medium=referral&utm_source=badge "Test Cases") +# LocalStack .NET Client ![Nuget](https://img.shields.io/nuget/dt/LocalStack.Client) [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml) [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml) > ## ⚠️ AWS SDK v4 Transition Notice > @@ -27,16 +27,16 @@ Localstack.NET is an easy-to-use .NET client for [LocalStack](https://github.com | Package | v1.x (AWS SDK v3) | v2.x (AWS SDK v4) - Development | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| LocalStack.Client | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) | [![MyGet](https://img.shields.io/myget/localstack-dotnet-client/v/LocalStack.Client.svg?label=myget)](https://www.myget.org/feed/localstack-dotnet-client/package/nuget/LocalStack.Client) | -| LocalStack.Client.Extensions | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.Extensions.svg)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![MyGet](https://img.shields.io/myget/localstack-dotnet-client/v/LocalStack.Client.Extensions.svg?label=myget)](https://www.myget.org/feed/localstack-dotnet-client/package/nuget/LocalStack.Client.Extensions) | +| LocalStack.Client | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) | [![GitHub Packages](https://img.shields.io/github/v/release/localstack-dotnet/localstack-dotnet-client?include_prereleases&label=github%20packages)](https://github.com/localstack-dotnet/localstack-dotnet-client/packages) | +| LocalStack.Client.Extensions | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.Extensions.svg)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![GitHub Packages](https://img.shields.io/github/v/release/localstack-dotnet/localstack-dotnet-client?include_prereleases&label=github%20packages)](https://github.com/localstack-dotnet/localstack-dotnet-client/packages) | ## Continuous Integration -| Build server | Platform | Build status | -| -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Github Actions | Ubuntu | [![build-ubuntu](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-ubuntu.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-ubuntu.yml) | -| Github Actions | Windows | [![build-windows](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-windows.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-windows.yml) | -| Github Actions | macOS | [![build-macos](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-macos.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-macos.yml) | +| Build Platform | Status | Description | +|----------------|--------|-------------| +| **Cross-Platform CI** | [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml) | Matrix testing: Windows, Linux, macOS | +| **Security Analysis** | [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml) | CodeQL analysis & dependency review | +| **Automated Publishing** | [![Auto Publish](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/publish-dev-github.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/publish-dev-github.yml) | Daily GitHub Packages builds | ## Table of Contents @@ -81,13 +81,37 @@ For detailed installation and setup instructions, please refer to the [official ## Getting Started -LocalStack.NET is installed from NuGet. To work with LocalStack in your .NET applications, you'll need the main library and its extensions. Here's how you can install them: +LocalStack.NET is available through multiple package sources to support different development workflows. + +### 📦 Package Installation + +#### Stable Releases (NuGet.org) + +For production use and stable releases: ```bash dotnet add package LocalStack.Client dotnet add package LocalStack.Client.Extensions ``` +#### Development Builds (GitHub Packages) + +For testing latest features and bug fixes: + +```bash +# Add GitHub Packages source +dotnet nuget add source https://nuget.pkg.github.com/localstack-dotnet/index.json \ + --name github-localstack \ + --username YOUR_GITHUB_USERNAME \ + --password YOUR_GITHUB_TOKEN + +# Install development packages +dotnet add package LocalStack.Client --prerelease --source github-localstack +dotnet add package LocalStack.Client.Extensions --prerelease --source github-localstack +``` + +> **🔑 GitHub Packages Authentication**: You'll need a GitHub account and [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with `read:packages` permission. + Refer to [documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Getting-Started#installation) for more information on how to install LocalStack.NET. `LocalStack.NET` is a library that provides a wrapper around the [aws-sdk-net](https://github.com/aws/aws-sdk-net). This means you can use it in a similar way to the `AWS SDK for .NET` and to [AWSSDK.Extensions.NETCore.Setup](https://docs.aws.amazon.com/sdk-for-net/latest/developer-guide/net-dg-config-netcore.html) with a few differences. For more on how to use the AWS SDK for .NET, see [Getting Started with the AWS SDK for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-setup.html). diff --git a/build/LocalStack.Build/BuildContext.cs b/build/LocalStack.Build/BuildContext.cs index 4000e4b..b8ae64f 100644 --- a/build/LocalStack.Build/BuildContext.cs +++ b/build/LocalStack.Build/BuildContext.cs @@ -4,23 +4,34 @@ namespace LocalStack.Build; public sealed class BuildContext : FrostingContext { + public const string LocalStackClientProjName = "LocalStack.Client"; + public const string LocalStackClientExtensionsProjName = "LocalStack.Client.Extensions"; + + public const string GitHubPackageSource = "github"; + public const string NuGetPackageSource = "nuget"; + public const string MyGetPackageSource = "myget"; + public BuildContext(ICakeContext context) : base(context) { BuildConfiguration = context.Argument("config", "Release"); - ForceBuild = context.Argument("force-build", false); - ForceRestore = context.Argument("force-restore", false); + ForceBuild = context.Argument("force-build", defaultValue: false); + ForceRestore = context.Argument("force-restore", defaultValue: false); PackageVersion = context.Argument("package-version", "x.x.x"); PackageId = context.Argument("package-id", default(string)); PackageSecret = context.Argument("package-secret", default(string)); - PackageSource = context.Argument("package-source", default(string)); - SkipFunctionalTest = context.Argument("skipFunctionalTest", true); + PackageSource = context.Argument("package-source", GitHubPackageSource); + SkipFunctionalTest = context.Argument("skipFunctionalTest", defaultValue: true); + + // New version generation arguments + UseDirectoryPropsVersion = context.Argument("use-directory-props-version", defaultValue: false); + BranchName = context.Argument("branch-name", "master"); var sourceBuilder = ImmutableDictionary.CreateBuilder(); - sourceBuilder.AddRange(new[] - { - new KeyValuePair("myget", "https://www.myget.org/F/localstack-dotnet-client/api/v3/index.json"), - new KeyValuePair("nuget", "https://api.nuget.org/v3/index.json") - }); + sourceBuilder.AddRange([ + new KeyValuePair(MyGetPackageSource, "https://www.myget.org/F/localstack-dotnet-client/api/v3/index.json"), + new KeyValuePair(NuGetPackageSource, "https://api.nuget.org/v3/index.json"), + new KeyValuePair(GitHubPackageSource, "https://nuget.pkg.github.com/localstack-dotnet/index.json"), + ]); PackageSourceMap = sourceBuilder.ToImmutable(); SolutionRoot = context.Directory("../../"); @@ -28,18 +39,18 @@ public BuildContext(ICakeContext context) : base(context) TestsPath = SolutionRoot + context.Directory("tests"); BuildPath = SolutionRoot + context.Directory("build"); ArtifactOutput = SolutionRoot + context.Directory("artifacts"); - LocalStackClientFolder = SrcPath + context.Directory("LocalStack.Client"); - LocalStackClientExtFolder = SrcPath + context.Directory("LocalStack.Client.Extensions"); + LocalStackClientFolder = SrcPath + context.Directory(LocalStackClientProjName); + LocalStackClientExtFolder = SrcPath + context.Directory(LocalStackClientExtensionsProjName); SlnFilePath = SolutionRoot + context.File("LocalStack.sln"); - LocalStackClientProjFile = LocalStackClientFolder + context.File("LocalStack.Client.csproj"); - LocalStackClientExtProjFile = LocalStackClientExtFolder + context.File("LocalStack.Client.Extensions.csproj"); + LocalStackClientProjFile = LocalStackClientFolder + context.File($"{LocalStackClientProjName}.csproj"); + LocalStackClientExtProjFile = LocalStackClientExtFolder + context.File($"{LocalStackClientExtensionsProjName}.csproj"); var packIdBuilder = ImmutableDictionary.CreateBuilder(); - packIdBuilder.AddRange(new[] - { - new KeyValuePair("LocalStack.Client", LocalStackClientProjFile), - new KeyValuePair("LocalStack.Client.Extensions", LocalStackClientExtProjFile) - }); + packIdBuilder.AddRange( + [ + new KeyValuePair(LocalStackClientProjName, LocalStackClientProjFile), + new KeyValuePair(LocalStackClientExtensionsProjName, LocalStackClientExtProjFile), + ]); PackageIdProjMap = packIdBuilder.ToImmutable(); } @@ -59,6 +70,10 @@ public BuildContext(ICakeContext context) : base(context) public string PackageSource { get; } + public bool UseDirectoryPropsVersion { get; } + + public string BranchName { get; } + public ImmutableDictionary PackageSourceMap { get; } public ImmutableDictionary PackageIdProjMap { get; } @@ -91,27 +106,10 @@ public static void ValidateArgument(string argumentName, string argument) } } - public void InstallXUnitNugetPackage() - { - if (!Directory.Exists("testrunner")) - { - Directory.CreateDirectory("testrunner"); - } - - var nugetInstallSettings = new NuGetInstallSettings - { - Version = "2.8.1", Verbosity = NuGetVerbosity.Normal, OutputDirectory = "testrunner", WorkingDirectory = "." - }; - - this.NuGetInstall("xunit.runner.console", nugetInstallSettings); - } - public IEnumerable GetProjMetadata() { DirectoryPath testsRoot = this.Directory(TestsPath); - List csProjFile = this.GetFiles($"{testsRoot}/**/*.csproj") - .Where(fp => fp.FullPath.EndsWith("Tests.csproj", StringComparison.InvariantCulture)) - .ToList(); + List csProjFile = [.. this.GetFiles($"{testsRoot}/**/*.csproj").Where(fp => fp.FullPath.EndsWith("Tests.csproj", StringComparison.InvariantCulture))]; var projMetadata = new List(); @@ -130,28 +128,85 @@ public IEnumerable GetProjMetadata() return projMetadata; } - public void RunXUnitUsingMono(string targetFramework, string assemblyPath) + public void InstallMonoOnLinux() { - int exitCode = this.StartProcess( - "mono", new ProcessSettings { Arguments = $"./testrunner/xunit.runner.console.2.8.1/tools/{targetFramework}/xunit.console.exe {assemblyPath}" }); + int result = this.StartProcess("mono", new ProcessSettings + { + Arguments = "--version", + RedirectStandardOutput = true, + NoWorkingDirectory = true, + }); + + if (result == 0) + { + this.Information("✅ Mono is already installed. Skipping installation."); + return; + } + + this.Information("Mono not found. Starting installation on Linux for .NET Framework test platform support..."); + + // Add Mono repository key + int exitCode1 = this.StartProcess("sudo", new ProcessSettings + { + Arguments = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF", + }); + + if (exitCode1 != 0) + { + this.Warning($"⚠️ Failed to add Mono repository key (exit code: {exitCode1})"); + return; + } + + // Add Mono repository + int exitCode2 = this.StartProcess("bash", new ProcessSettings + { + Arguments = "-c \"echo 'deb https://download.mono-project.com/repo/ubuntu focal main' | sudo tee /etc/apt/sources.list.d/mono-official-stable.list\"", + }); + + if (exitCode2 != 0) + { + this.Warning($"⚠️ Failed to add Mono repository (exit code: {exitCode2})"); + return; + } + + // Update package list + int exitCode3 = this.StartProcess("sudo", new ProcessSettings { Arguments = "apt update" }); + + if (exitCode3 != 0) + { + this.Warning($"⚠️ Failed to update package list (exit code: {exitCode3})"); + return; + } + + // Install Mono + int exitCode4 = this.StartProcess("sudo", new ProcessSettings { Arguments = "apt install -y mono-complete" }); - if (exitCode != 0) + if (exitCode4 != 0) { - throw new InvalidOperationException($"Exit code: {exitCode}"); + this.Warning($"⚠️ Failed to install Mono (exit code: {exitCode4})"); + this.Warning("This may cause .NET Framework tests to fail on Linux"); + return; } + + this.Information("✅ Mono installation completed successfully"); } public string GetProjectVersion() { - FilePath file = this.File("./src/Directory.Build.props"); + if (UseDirectoryPropsVersion) + { + return GetDynamicVersionFromProps("PackageMainVersion"); + } + // Original logic for backward compatibility + FilePath file = this.File("./src/Directory.Build.props"); this.Information(file.FullPath); string project = File.ReadAllText(file.FullPath, Encoding.UTF8); int startIndex = project.IndexOf("", StringComparison.Ordinal) + "".Length; int endIndex = project.IndexOf("", startIndex, StringComparison.Ordinal); - string version = project.Substring(startIndex, endIndex - startIndex); + string version = project[startIndex..endIndex]; version = $"{version}.{PackageVersion}"; return version; @@ -159,20 +214,119 @@ public string GetProjectVersion() public string GetExtensionProjectVersion() { - FilePath file = this.File(LocalStackClientExtProjFile); + if (UseDirectoryPropsVersion) + { + return GetDynamicVersionFromProps("PackageExtensionVersion"); + } + // Original logic for backward compatibility + FilePath file = this.File(LocalStackClientExtProjFile); this.Information(file.FullPath); string project = File.ReadAllText(file.FullPath, Encoding.UTF8); int startIndex = project.IndexOf("", StringComparison.Ordinal) + "".Length; int endIndex = project.IndexOf("", startIndex, StringComparison.Ordinal); - string version = project.Substring(startIndex, endIndex - startIndex); + string version = project[startIndex..endIndex]; version = $"{version}.{PackageVersion}"; return version; } + /// + /// Gets the target frameworks for a specific package using the existing proven method + /// + /// The package identifier + /// Comma-separated target frameworks + public string GetPackageTargetFrameworks(string packageId) + { + if (!PackageIdProjMap.TryGetValue(packageId, out FilePath? projectFile) || projectFile == null) + { + throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId)); + } + + string[] frameworks = GetProjectTargetFrameworks(projectFile.FullPath); + return string.Join(", ", frameworks); + } + + /// + /// Generates dynamic version from Directory.Build.props with build metadata + /// + /// The property name to extract (PackageMainVersion or PackageExtensionVersion) + /// Version with build metadata (e.g., 2.0.0-preview1.20240715.a1b2c3d) + private string GetDynamicVersionFromProps(string versionPropertyName) + { + // Extract base version from Directory.Build.props + FilePath propsFile = this.File("../../Directory.Build.props"); + string content = File.ReadAllText(propsFile.FullPath, Encoding.UTF8); + + string startElement = $"<{versionPropertyName}>"; + string endElement = $""; + + int startIndex = content.IndexOf(startElement, StringComparison.Ordinal) + startElement.Length; + int endIndex = content.IndexOf(endElement, startIndex, StringComparison.Ordinal); + + if (startIndex < startElement.Length || endIndex < 0) + { + throw new InvalidOperationException($"Could not find {versionPropertyName} in Directory.Build.props"); + } + + string baseVersion = content[startIndex..endIndex]; + + // Generate build metadata + string buildDate = DateTime.UtcNow.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture); + string commitSha = GetGitCommitSha(); + string safeBranchName = BranchName.Replace('/', '-').Replace('_', '-'); + + // Simplified NuGet-compliant version format + if (BranchName == "master") + { + // For master: 2.0.0-preview1-20240715-a1b2c3d + return $"{baseVersion}-{buildDate}-{commitSha}"; + } + else + { + // For feature branches: 2.0.0-preview1-feature-branch-20240715-a1b2c3d + return $"{baseVersion}-{safeBranchName}-{buildDate}-{commitSha}"; + } + } + + /// + /// Gets the short git commit SHA for version metadata + /// + /// Short commit SHA or timestamp fallback + private string GetGitCommitSha() + { + try + { + var processSettings = new ProcessSettings + { + Arguments = "rev-parse --short HEAD", + RedirectStandardOutput = true, + RedirectStandardError = true, + Silent = true, + }; + + var exitCode = this.StartProcess("git", processSettings, out IEnumerable output); + + if (exitCode == 0 && output?.Any() == true) + { + string? commitSha = output.FirstOrDefault()?.Trim(); + if (!string.IsNullOrEmpty(commitSha)) + { + return commitSha; + } + } + } + catch (Exception ex) + { + this.Warning($"Failed to get git commit SHA: {ex.Message}"); + } + + // Fallback to timestamp-based identifier + return DateTime.UtcNow.ToString("HHmmss", System.Globalization.CultureInfo.InvariantCulture); + } + private string[] GetProjectTargetFrameworks(string csprojPath) { FilePath file = this.File(csprojPath); @@ -185,7 +339,7 @@ private string[] GetProjectTargetFrameworks(string csprojPath) int startIndex = project.IndexOf(startElement, StringComparison.Ordinal) + startElement.Length; int endIndex = project.IndexOf(endElement, startIndex, StringComparison.Ordinal); - string targetFrameworks = project.Substring(startIndex, endIndex - startIndex); + string targetFrameworks = project[startIndex..endIndex]; return targetFrameworks.Split(';'); } @@ -204,14 +358,14 @@ private string GetAssemblyName(string csprojPath) int startIndex = project.IndexOf("", StringComparison.Ordinal) + "".Length; int endIndex = project.IndexOf("", startIndex, StringComparison.Ordinal); - assemblyName = project.Substring(startIndex, endIndex - startIndex); + assemblyName = project[startIndex..endIndex]; } else { int startIndex = csprojPath.LastIndexOf('/') + 1; int endIndex = csprojPath.IndexOf(".csproj", startIndex, StringComparison.Ordinal); - assemblyName = csprojPath.Substring(startIndex, endIndex - startIndex); + assemblyName = csprojPath[startIndex..endIndex]; } return assemblyName; diff --git a/build/LocalStack.Build/ConsoleHelper.cs b/build/LocalStack.Build/ConsoleHelper.cs new file mode 100644 index 0000000..b226587 --- /dev/null +++ b/build/LocalStack.Build/ConsoleHelper.cs @@ -0,0 +1,198 @@ +#pragma warning disable CA1515 // Consider making public types internal +#pragma warning disable CA1055 // Change the return type of method 'ConsoleHelper.GetDownloadUrl(string, string, string, [string])' from 'string' to 'System.Uri' + +namespace LocalStack.Build; + +/// +/// Helper class for rich console output using Spectre.Console +/// +public static class ConsoleHelper +{ + /// + /// Displays a large LocalStack.NET header with FigletText + /// + public static void WriteHeader() + { + AnsiConsole.Write(new FigletText("LocalStack.NET").LeftJustified().Color(Color.Blue)); + } + + /// + /// Displays a success message with green checkmark + /// + /// The success message to display + public static void WriteSuccess(string message) + { + AnsiConsole.MarkupLine($"[green]✅ {message.EscapeMarkup()}[/]"); + } + + /// + /// Displays a warning message with yellow warning symbol + /// + /// The warning message to display + public static void WriteWarning(string message) + { + AnsiConsole.MarkupLine($"[yellow]⚠️ {message.EscapeMarkup()}[/]"); + } + + /// + /// Displays an error message with red X symbol + /// + /// The error message to display + public static void WriteError(string message) + { + AnsiConsole.MarkupLine($"[red]❌ {message.EscapeMarkup()}[/]"); + } + + /// + /// Displays an informational message with blue info symbol + /// + /// The info message to display + public static void WriteInfo(string message) + { + AnsiConsole.MarkupLine($"[cyan]ℹ️ {message.EscapeMarkup()}[/]"); + } + + /// + /// Displays a processing message with gear symbol + /// + /// The processing message to display + public static void WriteProcessing(string message) + { + AnsiConsole.MarkupLine($"[yellow]🔧 {message.EscapeMarkup()}[/]"); + } + + /// + /// Displays a package-related message with package symbol + /// + /// The package message to display + public static void WritePackage(string message) + { + AnsiConsole.MarkupLine($"[cyan]📦 {message.EscapeMarkup()}[/]"); + } + + /// + /// Displays an upload/publish message with rocket symbol + /// + /// The upload message to display + public static void WriteUpload(string message) + { + AnsiConsole.MarkupLine($"[green]📤 {message.EscapeMarkup()}[/]"); + } + + /// + /// Creates and displays a package information table + /// + /// The package identifier + /// The package version + /// The target frameworks + /// The build configuration + /// The package source + public static void WritePackageInfoTable(string packageId, string version, string targetFrameworks, string buildConfig, string packageSource) + { + var table = new Table().Border(TableBorder.Rounded) + .BorderColor(Color.Grey) + .AddColumn(new TableColumn("[yellow]Property[/]").Centered()) + .AddColumn(new TableColumn("[cyan]Value[/]").LeftAligned()) + .AddRow("Package ID", packageId.EscapeMarkup()) + .AddRow("Version", version.EscapeMarkup()) + .AddRow("Target Frameworks", targetFrameworks.EscapeMarkup()) + .AddRow("Build Configuration", buildConfig.EscapeMarkup()) + .AddRow("Package Source", packageSource.EscapeMarkup()); + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } + + /// + /// Creates and displays a publication summary panel + /// + /// The package identifier + /// The package version + /// The package source + /// The download URL +#pragma warning disable MA0006 // Use String.Create instead of string concatenation + public static void WritePublicationSummary(string packageId, string version, string packageSource, string downloadUrl) + { + var panel = new Panel(new Markup($""" + [bold]📦 Package:[/] {packageId.EscapeMarkup()} + [bold]🏷️ Version:[/] {version.EscapeMarkup()} + [bold]🎯 Published to:[/] {packageSource.EscapeMarkup()} + [bold]🔗 Download URL:[/] [link]{downloadUrl.EscapeMarkup()}[/] + """)).Header(new PanelHeader("[bold green]✅ Publication Complete[/]").Centered()) + .BorderColor(Color.Green) + .Padding(1, 1); + + AnsiConsole.Write(panel); + AnsiConsole.WriteLine(); + } + + /// + /// Executes a function with a progress bar + /// + /// Description of the operation + /// The action to execute with progress context + public static void WithProgress(string description, Action action) + { + AnsiConsole.Progress() + .Start(ctx => + { + var task = ctx.AddTask($"[green]{description.EscapeMarkup()}[/]"); + action(ctx); + task.Increment(100); + }); + } + + /// + /// Displays a rule separator with optional text + /// + /// Optional title for the rule + public static void WriteRule(string title = "") + { + var rule = string.IsNullOrEmpty(title) ? new Rule() : new Rule($"[bold blue]{title.EscapeMarkup()}[/]"); + + AnsiConsole.Write(rule); + } + + /// + /// Displays version generation information + /// + /// The base version from Directory.Build.props + /// The final generated version with metadata + /// The build date + /// The git commit SHA + /// The git branch name + public static void WriteVersionInfo(string baseVersion, string finalVersion, string buildDate, string commitSha, string branchName) + { + var table = new Table().Border(TableBorder.Simple) + .BorderColor(Color.Grey) + .AddColumn(new TableColumn("[yellow]Version Component[/]").Centered()) + .AddColumn(new TableColumn("[cyan]Value[/]").LeftAligned()) + .AddRow("Base Version", baseVersion.EscapeMarkup()) + .AddRow("Build Date", buildDate.EscapeMarkup()) + .AddRow("Commit SHA", commitSha.EscapeMarkup()) + .AddRow("Branch", branchName.EscapeMarkup()) + .AddRow("[bold]Final Version[/]", $"[bold green]{finalVersion.EscapeMarkup()}[/]"); + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } + + /// + /// Generates a download URL based on package source + /// + /// The package source (github, nuget, myget) + /// The package identifier + /// The package version + /// The repository owner (for GitHub packages) + /// The download URL + public static string GetDownloadUrl(string packageSource, string packageId, string version, string repositoryOwner = "localstack-dotnet") + { + return packageSource?.ToUpperInvariant() switch + { + "GITHUB" => $"https://github.com/{repositoryOwner}/localstack-dotnet-client/packages", + "NUGET" => $"https://www.nuget.org/packages/{packageId}/{version}", + "MYGET" => $"https://www.myget.org/packages/{packageId}", + _ => "Package published successfully", + }; + } +} \ No newline at end of file diff --git a/build/LocalStack.Build/GlobalUsings.cs b/build/LocalStack.Build/GlobalUsings.cs index e0ccdec..14ccd72 100644 --- a/build/LocalStack.Build/GlobalUsings.cs +++ b/build/LocalStack.Build/GlobalUsings.cs @@ -2,15 +2,17 @@ global using Cake.Common.Diagnostics; global using Cake.Common.IO; global using Cake.Common.IO.Paths; +global using Cake.Common.Tools.DotNet; global using Cake.Common.Tools.DotNet.MSBuild; global using Cake.Common.Tools.NuGet; -global using Cake.Common.Tools.NuGet.Install; global using Cake.Common.Tools.NuGet.List; global using Cake.Core; global using Cake.Core.IO; global using Cake.Docker; global using Cake.Frosting; +global using Spectre.Console; + global using LocalStack.Build; global using LocalStack.Build.Models; @@ -22,7 +24,6 @@ global using System.Text; global using System.Text.RegularExpressions; -global using Cake.Common.Tools.DotNet; global using Cake.Common.Tools.DotNet.Build; global using Cake.Common.Tools.DotNet.NuGet.Push; global using Cake.Common.Tools.DotNet.Pack; diff --git a/build/LocalStack.Build/LocalStack.Build.csproj b/build/LocalStack.Build/LocalStack.Build.csproj index 0840af6..4b263e5 100644 --- a/build/LocalStack.Build/LocalStack.Build.csproj +++ b/build/LocalStack.Build/LocalStack.Build.csproj @@ -5,7 +5,7 @@ $(MSBuildProjectDirectory) latest - $(NoWarn);CA1303;CA1707;CS8601;CS8618;MA0047;MA0048;CA1050;S3903;MA0006;CA1031;CA1062;MA0051;S112;CA2201;CA1307;MA0074;MA0023;MA0009;CA1307;CA1310;CA1515 + $(NoWarn);CA1303;CA1707;CS8601;CS8618;MA0047;MA0048;CA1050;S3903;MA0006;CA1031;CA1062;MA0051;S112;CA2201;CA1307;MA0074;MA0023;MA0009;CA1307;CA1310;CA1515;CA1054;CA1055 diff --git a/build/LocalStack.Build/Program.cs b/build/LocalStack.Build/Program.cs index c8a5855..dfe0c52 100644 --- a/build/LocalStack.Build/Program.cs +++ b/build/LocalStack.Build/Program.cs @@ -12,6 +12,9 @@ public sealed class InitTask : FrostingTask { public override void Run(BuildContext context) { + ConsoleHelper.WriteHeader(); + ConsoleHelper.WriteRule("Initialization"); + context.StartProcess("dotnet", new ProcessSettings { Arguments = "--info" }); if (!context.IsRunningOnUnix()) @@ -20,14 +23,10 @@ public override void Run(BuildContext context) } context.StartProcess("git", new ProcessSettings { Arguments = "config --global core.autocrlf true" }); - - context.StartProcess("mono", new ProcessSettings { Arguments = "--version" }); - - context.InstallXUnitNugetPackage(); } } -[TaskName("build"), IsDependentOn(typeof(InitTask)),] +[TaskName("build"), IsDependentOn(typeof(InitTask))] public sealed class BuildTask : FrostingTask { public override void Run(BuildContext context) @@ -45,7 +44,7 @@ public override void Run(BuildContext context) var settings = new DotNetTestSettings { - NoRestore = !context.ForceRestore, NoBuild = !context.ForceBuild, Configuration = context.BuildConfiguration, Blame = true + NoRestore = !context.ForceRestore, NoBuild = !context.ForceBuild, Configuration = context.BuildConfiguration, Blame = true, }; IEnumerable projMetadata = context.GetProjMetadata(); @@ -53,7 +52,7 @@ public override void Run(BuildContext context) foreach (ProjMetadata testProj in projMetadata) { string testProjectPath = testProj.CsProjPath; - string targetFrameworks = string.Join(",", testProj.TargetFrameworks); + string targetFrameworks = string.Join(',', testProj.TargetFrameworks); context.Warning($"Target Frameworks {targetFrameworks}"); @@ -82,7 +81,7 @@ public override void Run(BuildContext context) { context.Warning(psOutput); - string[] containers = psOutput.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + string[] containers = psOutput.Split([Environment.NewLine], StringSplitOptions.None); context.DockerRm(containers); } } @@ -92,21 +91,19 @@ public override void Run(BuildContext context) } } - if (context.IsRunningOnLinux() && targetFramework == "net462") - { - context.Warning("Temporarily disabled running net462 tests on Linux because of a problem in mono runtime"); - } - else if (context.IsRunningOnMacOs() && targetFramework == "net462") - { - context.RunXUnitUsingMono(targetFramework, $"{testProj.DirectoryPath}/bin/{context.BuildConfiguration}/{targetFramework}/{testProj.AssemblyName}.dll"); - } - else + // .NET Framework testing on non-Windows platforms + // - Modern .NET includes built-in Mono runtime + // - Test platform still requires external Mono installation on Linux + if (targetFramework == "net472" && !context.IsRunningOnWindows()) { - string testFilePrefix = targetFramework.Replace(".", "-"); - settings.ArgumentCustomization = args => args.Append($" --logger \"trx;LogFileName={testFilePrefix}_{testResults}\""); - context.DotNetTest(testProjectPath, settings); + string platform = context.IsRunningOnLinux() ? "Linux (with external Mono)" : "macOS (built-in Mono)"; + context.Information($"Running .NET Framework tests on {platform}"); } + string testFilePrefix = targetFramework.Replace('.', '-'); + settings.ArgumentCustomization = args => args.Append($" --logger \"trx;LogFileName={testFilePrefix}_{testResults}\""); + context.DotNetTest(testProjectPath, settings); + context.Warning("=============================================================="); } } @@ -118,50 +115,192 @@ public sealed class NugetPackTask : FrostingTask { public override void Run(BuildContext context) { - ValidatePackageVersion(context); + // Display header + ConsoleHelper.WriteRule("Package Creation"); + + // If no specific package ID is provided, pack all packages + if (string.IsNullOrEmpty(context.PackageId)) + { + PackAllPackages(context); + } + else + { + PackSinglePackage(context, context.PackageId); + } + + ConsoleHelper.WriteRule(); + } + + private static void PackAllPackages(BuildContext context) + { + foreach (string packageId in context.PackageIdProjMap.Keys) + { + ConsoleHelper.WriteInfo($"Creating package: {packageId}"); + PackSinglePackage(context, packageId); + } + } + + private static void PackSinglePackage(BuildContext context, string packageId) + { + // Get effective version using enhanced methods + string effectiveVersion = GetEffectiveVersion(context, packageId); + + // Display package information + ConsoleHelper.WritePackageInfoTable(packageId, effectiveVersion, GetTargetFrameworks(context, packageId), context.BuildConfiguration, context.PackageSource); + + // Validate inputs + ValidatePackageInputs(context, packageId, effectiveVersion); + + // Handle Extensions project dependency switching if needed + if (packageId == BuildContext.LocalStackClientExtensionsProjName && + context is + { + PackageSource: BuildContext.GitHubPackageSource, + UseDirectoryPropsVersion: false, + }) + { + PrepareExtensionsProject(context, effectiveVersion); + } + // Create packages with progress indication + ConsoleHelper.WithProgress($"Creating {packageId} package", _ => CreatePackage(context, packageId, effectiveVersion)); + + // Success message + ConsoleHelper.WriteSuccess($"Successfully created {packageId} v{effectiveVersion}"); + ConsoleHelper.WriteInfo($"Package location: {context.ArtifactOutput}"); + } + + private static string GetEffectiveVersion(BuildContext context, string packageId) + { + return packageId switch + { + BuildContext.LocalStackClientProjName => context.GetProjectVersion(), + BuildContext.LocalStackClientExtensionsProjName => context.GetExtensionProjectVersion(), + _ => throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId)), + }; + } + + private static string GetTargetFrameworks(BuildContext context, string packageId) + { + // Use the existing proven method to get actual target frameworks + return context.GetPackageTargetFrameworks(packageId); + } + + private static void PrepareExtensionsProject(BuildContext context, string version) + { + ConsoleHelper.WriteProcessing("Updating Extensions project dependencies..."); + + try + { + // Set working directory to Extensions project + var originalWorkingDir = context.Environment.WorkingDirectory; + context.Environment.WorkingDirectory = context.LocalStackClientExtFolder; + + try + { + // Remove project reference using Cake built-in method + var projectRef = context.File("../LocalStack.Client/LocalStack.Client.csproj"); + context.DotNetRemoveReference([projectRef]); + ConsoleHelper.WriteInfo("Removed project reference to LocalStack.Client"); + + // Add package reference using Cake built-in method + context.DotNetAddPackage(BuildContext.LocalStackClientProjName, version); + ConsoleHelper.WriteSuccess($"Added package reference for {BuildContext.LocalStackClientProjName} v{version}"); + } + finally + { + // Restore original working directory + context.Environment.WorkingDirectory = originalWorkingDir; + } + } + catch (Exception ex) + { + ConsoleHelper.WriteError($"Failed to prepare Extensions project: {ex.Message}"); + + throw; + } + } + + private static void CreatePackage(BuildContext context, string packageId, string effectiveVersion) + { if (!Directory.Exists(context.ArtifactOutput)) { Directory.CreateDirectory(context.ArtifactOutput); } - FilePath packageCsProj = context.PackageIdProjMap[context.PackageId]; + if (!context.PackageIdProjMap.TryGetValue(packageId, out FilePath? packageCsProj) || packageCsProj == null) + { + throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId)); + } var settings = new DotNetPackSettings { - Configuration = context.BuildConfiguration, OutputDirectory = context.ArtifactOutput, MSBuildSettings = new DotNetMSBuildSettings() + Configuration = context.BuildConfiguration, + OutputDirectory = context.ArtifactOutput, + NoBuild = false, + NoRestore = false, + MSBuildSettings = new DotNetMSBuildSettings(), }; - settings.MSBuildSettings.SetVersion(context.PackageVersion); + settings.MSBuildSettings.SetVersion(effectiveVersion); context.DotNetPack(packageCsProj.FullPath, settings); } - private static void ValidatePackageVersion(BuildContext context) + private static void ValidatePackageInputs(BuildContext context, string packageId, string effectiveVersion) { - BuildContext.ValidateArgument("package-id", context.PackageId); - BuildContext.ValidateArgument("package-version", context.PackageVersion); + BuildContext.ValidateArgument("package-id", packageId); BuildContext.ValidateArgument("package-source", context.PackageSource); - Match match = Regex.Match(context.PackageVersion, @"^(\d+)\.(\d+)\.(\d+)(\.(\d+))*$", RegexOptions.IgnoreCase); + // Skip detailed version validation when using directory props version + if (context.UseDirectoryPropsVersion) + { + ConsoleHelper.WriteInfo("Using dynamic version generation from Directory.Build.props"); + + return; + } + + // Original validation for manual version input + ValidatePackageVersion(context, effectiveVersion); + } + + private static void ValidatePackageVersion(BuildContext context, string version) + { + Match match = Regex.Match(version, @"^(\d+)\.(\d+)\.(\d+)([\.\-].*)*$", RegexOptions.IgnoreCase); if (!match.Success) { - throw new Exception($"Invalid version: {context.PackageVersion}"); + throw new Exception($"Invalid version: {version}"); } - string packageSource = context.PackageSourceMap[context.PackageSource]; + // Skip version validation for GitHub Packages - allows overwriting dev builds + if (context.PackageSource == BuildContext.GitHubPackageSource) + { + ConsoleHelper.WriteInfo("Skipping version validation for GitHub Packages source"); + + return; + } - var nuGetListSettings = new NuGetListSettings { AllVersions = false, Source = new List() { packageSource } }; - NuGetListItem nuGetListItem = context.NuGetList(context.PackageId, nuGetListSettings).Single(item => item.Name == context.PackageId); - string latestPackVersionStr = nuGetListItem.Version; + try + { + string packageSource = context.PackageSourceMap[context.PackageSource]; + var nuGetListSettings = new NuGetListSettings { AllVersions = false, Source = [packageSource] }; + NuGetListItem nuGetListItem = context.NuGetList(context.PackageId, nuGetListSettings).Single(item => item.Name == context.PackageId); + string latestPackVersionStr = nuGetListItem.Version; - Version packageVersion = Version.Parse(context.PackageVersion); - Version latestPackVersion = Version.Parse(latestPackVersionStr); + Version packageVersion = Version.Parse(version); + Version latestPackVersion = Version.Parse(latestPackVersionStr); - if (packageVersion <= latestPackVersion) + if (packageVersion <= latestPackVersion) + { + throw new Exception($"The new package version {version} should be greater than the latest package version {latestPackVersionStr}"); + } + + ConsoleHelper.WriteSuccess($"Version validation passed: {version} > {latestPackVersionStr}"); + } + catch (Exception ex) when (ex is not InvalidOperationException) { - throw new Exception($"The new package version {context.PackageVersion} should be greater than the latest package version {latestPackVersionStr}"); + ConsoleHelper.WriteWarning($"Could not validate version against existing packages: {ex.Message}"); } } } @@ -170,25 +309,104 @@ private static void ValidatePackageVersion(BuildContext context) public sealed class NugetPushTask : FrostingTask { public override void Run(BuildContext context) + { + // Display header + ConsoleHelper.WriteRule("Package Publishing"); + + // Get effective version using enhanced methods + string effectiveVersion = GetEffectiveVersion(context); + + // Validate inputs + ValidatePublishInputs(context, effectiveVersion); + + // Display package information + ConsoleHelper.WritePackageInfoTable(context.PackageId, effectiveVersion, GetTargetFrameworks(context), context.BuildConfiguration, context.PackageSource); + + // Perform publishing with progress indication + ConsoleHelper.WithProgress("Publishing package", progressCtx => + { + PublishPackage(context, effectiveVersion); + }); + + // Success message with download URL + var downloadUrl = ConsoleHelper.GetDownloadUrl(context.PackageSource, context.PackageId, effectiveVersion); + ConsoleHelper.WritePublicationSummary(context.PackageId, effectiveVersion, context.PackageSource, downloadUrl); + ConsoleHelper.WriteRule(); + } + + private static string GetEffectiveVersion(BuildContext context) + { + return context.PackageId switch + { + BuildContext.LocalStackClientProjName => context.GetProjectVersion(), + BuildContext.LocalStackClientExtensionsProjName => context.GetExtensionProjectVersion(), + _ => throw new ArgumentException($"Unknown package ID: {context.PackageId}", nameof(context)), + }; + } + + private static string GetTargetFrameworks(BuildContext context) + { + return context.GetPackageTargetFrameworks(context.PackageId); + } + + private static void ValidatePublishInputs(BuildContext context, string effectiveVersion) { BuildContext.ValidateArgument("package-id", context.PackageId); - BuildContext.ValidateArgument("package-version", context.PackageVersion); BuildContext.ValidateArgument("package-secret", context.PackageSecret); BuildContext.ValidateArgument("package-source", context.PackageSource); - string packageId = context.PackageId; - string packageVersion = context.PackageVersion; + // For dynamic version generation, validate the effective version instead of PackageVersion + if (context.UseDirectoryPropsVersion) + { + ConsoleHelper.WriteInfo($"Using dynamic version: {effectiveVersion}"); + } + else + { + BuildContext.ValidateArgument("package-version", context.PackageVersion); + } + } - ConvertableFilePath packageFile = context.ArtifactOutput + context.File($"{packageId}.{packageVersion}.nupkg"); + private static void PublishPackage(BuildContext context, string effectiveVersion) + { + // Use the effective version for both dynamic and manual version scenarios + string packageVersion = context.UseDirectoryPropsVersion ? effectiveVersion : context.PackageVersion; + + ConvertableFilePath packageFile = context.ArtifactOutput + context.File($"{context.PackageId}.{packageVersion}.nupkg"); if (!context.FileExists(packageFile)) { - throw new Exception($"The specified {packageFile.Path} package file does not exists"); + throw new Exception($"The specified {packageFile.Path} package file does not exist"); } string packageSecret = context.PackageSecret; string packageSource = context.PackageSourceMap[context.PackageSource]; + ConsoleHelper.WriteUpload($"Publishing {context.PackageId} to {context.PackageSource}..."); + context.DotNetNuGetPush(packageFile.Path.FullPath, new DotNetNuGetPushSettings() { ApiKey = packageSecret, Source = packageSource, }); + + ConsoleHelper.WriteSuccess($"Successfully published {context.PackageId} v{packageVersion}"); + } +} + +[TaskName("nuget-pack-and-publish")] +public sealed class NugetPackAndPublishTask : FrostingTask +{ + public override void Run(BuildContext context) + { + ConsoleHelper.WriteRule("Pack & Publish Pipeline"); + + // First pack the package + ConsoleHelper.WriteProcessing("Step 1: Creating package..."); + var packTask = new NugetPackTask(); + packTask.Run(context); + + // Then publish the package + ConsoleHelper.WriteProcessing("Step 2: Publishing package..."); + var pushTask = new NugetPushTask(); + pushTask.Run(context); + + ConsoleHelper.WriteSuccess("Pack & Publish pipeline completed successfully!"); + ConsoleHelper.WriteRule(); } } \ No newline at end of file diff --git a/build/LocalStack.Build/SummaryTask.cs b/build/LocalStack.Build/SummaryTask.cs new file mode 100644 index 0000000..15a3a72 --- /dev/null +++ b/build/LocalStack.Build/SummaryTask.cs @@ -0,0 +1,168 @@ +using System.Globalization; + +[TaskName("workflow-summary")] +public sealed class SummaryTask : FrostingTask +{ + private const string GitHubOwner = "localstack-dotnet"; + + public override void Run(BuildContext context) + { + ConsoleHelper.WriteRule("Build Summary"); + + GenerateBuildSummary(context); + GenerateInstallationInstructions(context); + GenerateMetadataTable(context); + + ConsoleHelper.WriteRule(); + } + + private static void GenerateBuildSummary(BuildContext context) + { + var panel = new Panel(GetSummaryContent(context)) + .Border(BoxBorder.Rounded) + .BorderColor(Color.Green) + .Header("[bold green]✅ Build Complete[/]") + .HeaderAlignment(Justify.Center); + + AnsiConsole.Write(panel); + AnsiConsole.WriteLine(); + } + + private static string GetSummaryContent(BuildContext context) + { + var content = new StringBuilder(); + + if (string.IsNullOrEmpty(context.PackageId)) + { + // Summary for all packages + content.AppendLine(CultureInfo.InvariantCulture, $"[bold]📦 Packages Built:[/]"); + + foreach (string packageId in context.PackageIdProjMap.Keys) + { + string version = GetPackageVersion(context, packageId); + content.AppendLine(CultureInfo.InvariantCulture, $" • [cyan]{packageId}[/] [yellow]v{version}[/]"); + } + } + else + { + // Summary for specific package + string version = GetPackageVersion(context, context.PackageId); + content.AppendLine(CultureInfo.InvariantCulture, $"[bold]📦 Package:[/] [cyan]{context.PackageId}[/]"); + content.AppendLine(CultureInfo.InvariantCulture, $"[bold]🏷️ Version:[/] [yellow]{version}[/]"); + content.AppendLine(CultureInfo.InvariantCulture, $"[bold]🎯 Target:[/] [blue]{context.PackageSource}[/]"); + content.AppendLine(CultureInfo.InvariantCulture, $"[bold]⚙️ Config:[/] [green]{context.BuildConfiguration}[/]"); + } + + return content.ToString().TrimEnd(); + } + + private static void GenerateInstallationInstructions(BuildContext context) + { + var panel = new Panel(GetInstallationContent(context)) + .Border(BoxBorder.Rounded) + .BorderColor(Color.Blue) + .Header("[bold blue]🚀 Installation Instructions[/]") + .HeaderAlignment(Justify.Center); + + AnsiConsole.Write(panel); + AnsiConsole.WriteLine(); + } + + private static string GetInstallationContent(BuildContext context) + { + var content = new StringBuilder(); + + if (context.PackageSource == BuildContext.GitHubPackageSource) + { + content.AppendLine("[bold]1. Add GitHub Packages source:[/]"); + content.AppendLine(CultureInfo.InvariantCulture, $"[grey]dotnet nuget add source https://nuget.pkg.github.com/{GitHubOwner}/index.json \\[/]"); + content.AppendLine("[grey] --name github-localstack \\[/]"); + content.AppendLine("[grey] --username YOUR_USERNAME \\[/]"); + content.AppendLine("[grey] --password YOUR_GITHUB_TOKEN[/]"); + content.AppendLine(); + } + + content.AppendLine("[bold]2. Install package(s):[/]"); + + if (string.IsNullOrEmpty(context.PackageId)) + { + // Installation for all packages + foreach (string packageId in context.PackageIdProjMap.Keys) + { + string version = GetPackageVersion(context, packageId); + content.AppendLine(GetInstallCommand(packageId, version, context.PackageSource)); + } + } + else + { + // Installation for specific package + string version = GetPackageVersion(context, context.PackageId); + content.AppendLine(GetInstallCommand(context.PackageId, version, context.PackageSource)); + } + + return content.ToString().TrimEnd(); + } + + private static string GetInstallCommand(string packageId, string version, string packageSource) + { + string sourceFlag = packageSource == BuildContext.GitHubPackageSource ? " --source github-localstack" : ""; + return $"[grey]dotnet add package {packageId} --version {version}{sourceFlag}[/]"; + } + + private static void GenerateMetadataTable(BuildContext context) + { + var table = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.Grey) + .Title("[bold]📊 Build Metadata[/]") + .AddColumn("[yellow]Property[/]") + .AddColumn("[cyan]Value[/]"); + + // Add build information + table.AddRow("Build Date", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC", CultureInfo.InvariantCulture)); + table.AddRow("Build Configuration", context.BuildConfiguration); + + if (context.UseDirectoryPropsVersion) + { + table.AddRow("Version Source", "Directory.Build.props (Dynamic)"); + table.AddRow("Branch Name", context.BranchName); + + try + { + // Simply skip git commit info since the method is private + table.AddRow("Git Commit", "See build output"); + } + catch + { + table.AddRow("Git Commit", "Not available"); + } + } + else + { + table.AddRow("Version Source", "Manual"); + } + + // Add package information + if (!string.IsNullOrEmpty(context.PackageId)) + { + string targetFrameworks = context.GetPackageTargetFrameworks(context.PackageId); + table.AddRow("Target Frameworks", targetFrameworks); + + string downloadUrl = ConsoleHelper.GetDownloadUrl(context.PackageSource, context.PackageId, GetPackageVersion(context, context.PackageId)); + table.AddRow("Download URL", downloadUrl); + } + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } + + private static string GetPackageVersion(BuildContext context, string packageId) + { + return packageId switch + { + BuildContext.LocalStackClientProjName => context.GetProjectVersion(), + BuildContext.LocalStackClientExtensionsProjName => context.GetExtensionProjectVersion(), + _ => "Unknown", + }; + } +} diff --git a/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs b/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs index 19bb4bb..a707578 100644 --- a/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs +++ b/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs @@ -5,39 +5,36 @@ namespace LocalStack.Client.Extensions; public sealed class AwsClientFactoryWrapper : IAwsClientFactoryWrapper { - private static readonly string ClientFactoryFullName = "Amazon.Extensions.NETCore.Setup.ClientFactory"; + private static readonly string ClientFactoryGenericTypeName = "Amazon.Extensions.NETCore.Setup.ClientFactory`1"; private static readonly string CreateServiceClientMethodName = "CreateServiceClient"; public AmazonServiceClient CreateServiceClient(IServiceProvider provider, AWSOptions? awsOptions) where TClient : IAmazonService { - Type? clientFactoryType = typeof(ConfigurationException).Assembly.GetType(ClientFactoryFullName); + Type? genericFactoryType = typeof(ConfigurationException).Assembly.GetType(ClientFactoryGenericTypeName); - if (clientFactoryType == null) + if (genericFactoryType == null) { - throw new LocalStackClientConfigurationException($"Failed to find internal ClientFactory in {ClientFactoryFullName}"); + throw new LocalStackClientConfigurationException($"Failed to find internal ClientFactory in {ClientFactoryGenericTypeName}"); } - ConstructorInfo? constructorInfo = - clientFactoryType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(Type), typeof(AWSOptions) }, null); + // Create ClientFactory + Type concreteFactoryType = genericFactoryType.MakeGenericType(typeof(TClient)); + ConstructorInfo? constructor = concreteFactoryType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(AWSOptions) }, null); - if (constructorInfo == null) + if (constructor == null) { - throw new LocalStackClientConfigurationException("ClientFactory missing a constructor with parameters Type and AWSOptions."); + throw new LocalStackClientConfigurationException("ClientFactory missing constructor with AWSOptions parameter."); } - Type clientType = typeof(TClient); + object factory = constructor.Invoke(new object[] { awsOptions! }); + MethodInfo? createMethod = factory.GetType().GetMethod(CreateServiceClientMethodName, BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(IServiceProvider) }, null); - object clientFactory = constructorInfo.Invoke(new object[] { clientType, awsOptions! }); - - MethodInfo? methodInfo = clientFactory.GetType().GetMethod(CreateServiceClientMethodName, BindingFlags.NonPublic | BindingFlags.Instance); - - if (methodInfo == null) + if (createMethod == null) { - throw new LocalStackClientConfigurationException($"Failed to find internal method {CreateServiceClientMethodName} in {ClientFactoryFullName}"); + throw new LocalStackClientConfigurationException($"ClientFactory missing {CreateServiceClientMethodName}(IServiceProvider) method."); } - object serviceInstance = methodInfo.Invoke(clientFactory, new object[] { provider }); - + object serviceInstance = createMethod.Invoke(factory, new object[] { provider }); return (AmazonServiceClient)serviceInstance; } } \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj b/src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj index 32eaa4d..e0aec55 100644 --- a/src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj +++ b/src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj @@ -1,64 +1,75 @@  - - netstandard2.0;net8.0;net9.0 - LocalStack.Client.Extensions - LocalStack.Client.Extensions - $(PackageExtensionVersion) + + netstandard2.0;net8.0;net9.0 + LocalStack.Client.Extensions + LocalStack.Client.Extensions + $(PackageExtensionVersion) - LocalStack.NET Client - - Extensions for the LocalStack.NET Client to integrate with .NET Core configuration and dependency injection frameworks. The extensions also provides wrapper around AWSSDK.Extensions.NETCore.Setup to use both LocalStack and AWS side-by-side - - aws-sdk, localstack, client-library, dotnet, dotnet-core - LICENSE.txt - README.md - true - 1.2.2 - true - true - $(NoWarn);CA1510 - + LocalStack.NET Client + + Extensions for the LocalStack.NET Client to integrate with .NET Core configuration and dependency injection frameworks. The extensions also provides wrapper around AWSSDK.Extensions.NETCore.Setup to use both LocalStack and AWS side-by-side + + aws-sdk, localstack, client-library, dotnet, dotnet-core + LICENSE.txt + README.md + true + 1.2.2 + true + true + $(NoWarn);CA1510 + - - true - + + true + - - + + - - - - - + + + + + - - + + - - - - - - - - - - + + + + - - - Always - - - Always - - - + + + + - - - + + + + + + + + + + + + + + + Always + + + Always + + + + + + + \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/README.md b/src/LocalStack.Client.Extensions/README.md index 4bec638..51c4faa 100644 --- a/src/LocalStack.Client.Extensions/README.md +++ b/src/LocalStack.Client.Extensions/README.md @@ -1,21 +1,42 @@ -# LocalStack .NET Client ![Nuget](https://img.shields.io/nuget/dt/LocalStack.Client) [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) [![Space Metric](https://localstack-dotnet.testspace.com/spaces/232580/badge?token=bc6aa170f4388c662b791244948f6d2b14f16983)](https://localstack-dotnet.testspace.com/spaces/232580?utm_campaign=metric&utm_medium=referral&utm_source=badge "Test Cases") +# LocalStack .NET Client ![Nuget](https://img.shields.io/nuget/dt/LocalStack.Client) [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml) [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml) + +> ## ⚠️ AWS SDK v4 Transition Notice +> +> **Current Status**: This main branch is under active development for **AWS SDK v4 support (v2.0)** +> +> **Version Strategy**: +> - **v1.x** (AWS SDK v3): Maintenance mode → Available on [sdkv3-lts branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/sdkv3-lts) (EOL: July 31, 2026) +> - **v2.x** (AWS SDK v4): Active development → Native AOT support in subsequent v2.x releases +> +> **Migration Timeline**: Q3 2025 for v2.0.0 GA +> +> 📖 **[Read Full Roadmap & Migration Guide →](../../discussions)** ![LocalStack](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/master/assets/localstack-dotnet.png?raw=true) Localstack.NET is an easy-to-use .NET client for [LocalStack](https://github.com/localstack/localstack), a fully functional local AWS cloud stack. The client library provides a thin wrapper around [aws-sdk-net](https://github.com/aws/aws-sdk-net) which automatically configures the target endpoints to use LocalStack for your local cloud application development. -| Package | Stable | Nightly | +## Version Compatibility + +| LocalStack.NET Version | AWS SDK Version | .NET Support | Status | Branch | +|------------------------|-----------------|--------------|---------|---------| +| v1.x | AWS SDK v3 | .NET 8, 9, Standard 2.0, Framework 4.6.2 | Maintenance (until July 2026) | [sdkv3-lts](../../tree/sdkv3-lts) | +| v2.x | AWS SDK v4 | .NET 8, 9, Standard 2.0, Framework 4.7.2 | Active Development | [master](../../tree/main) | + +## Package Status + +| Package | v1.x (AWS SDK v3) | v2.x (AWS SDK v4) - Development | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| LocalStack.Client | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) | [![MyGet](https://img.shields.io/myget/localstack-dotnet-client/v/LocalStack.Client.svg?label=myget)](https://www.myget.org/feed/localstack-dotnet-client/package/nuget/LocalStack.Client) | -| LocalStack.Client.Extensions | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.Extensions.svg)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![MyGet](https://img.shields.io/myget/localstack-dotnet-client/v/LocalStack.Client.Extensions.svg?label=myget)](https://www.myget.org/feed/localstack-dotnet-client/package/nuget/LocalStack.Client.Extensions) | +| LocalStack.Client | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) | [![GitHub Packages](https://img.shields.io/github/v/release/localstack-dotnet/localstack-dotnet-client?include_prereleases&label=github%20packages)](https://github.com/localstack-dotnet/localstack-dotnet-client/packages) | +| LocalStack.Client.Extensions | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.Extensions.svg)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![GitHub Packages](https://img.shields.io/github/v/release/localstack-dotnet/localstack-dotnet-client?include_prereleases&label=github%20packages)](https://github.com/localstack-dotnet/localstack-dotnet-client/packages) | ## Continuous Integration -| Build server | Platform | Build status | -| -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Github Actions | Ubuntu | [![build-ubuntu](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-ubuntu.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-ubuntu.yml) | -| Github Actions | Windows | [![build-windows](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-windows.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-windows.yml) | -| Github Actions | macOS | [![build-macos](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-macos.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-macos.yml) | +| Build Platform | Status | Description | +|----------------|--------|-------------| +| **Cross-Platform CI** | [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml) | Matrix testing: Windows, Linux, macOS | +| **Security Analysis** | [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml) | CodeQL analysis & dependency review | +| **Automated Publishing** | [![Auto Publish](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/publish-dev-github.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/publish-dev-github.yml) | Daily GitHub Packages builds | ## Table of Contents @@ -60,13 +81,37 @@ For detailed installation and setup instructions, please refer to the [official ## Getting Started -LocalStack.NET is installed from NuGet. To work with LocalStack in your .NET applications, you'll need the main library and its extensions. Here's how you can install them: +LocalStack.NET is available through multiple package sources to support different development workflows. + +### 📦 Package Installation + +#### Stable Releases (NuGet.org) + +For production use and stable releases: ```bash dotnet add package LocalStack.Client dotnet add package LocalStack.Client.Extensions ``` +#### Development Builds (GitHub Packages) + +For testing latest features and bug fixes: + +```bash +# Add GitHub Packages source +dotnet nuget add source https://nuget.pkg.github.com/localstack-dotnet/index.json \ + --name github-localstack \ + --username YOUR_GITHUB_USERNAME \ + --password YOUR_GITHUB_TOKEN + +# Install development packages +dotnet add package LocalStack.Client --prerelease --source github-localstack +dotnet add package LocalStack.Client.Extensions --prerelease --source github-localstack +``` + +> **🔑 GitHub Packages Authentication**: You'll need a GitHub account and [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with `read:packages` permission. + Refer to [documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Getting-Started#installation) for more information on how to install LocalStack.NET. `LocalStack.NET` is a library that provides a wrapper around the [aws-sdk-net](https://github.com/aws/aws-sdk-net). This means you can use it in a similar way to the `AWS SDK for .NET` and to [AWSSDK.Extensions.NETCore.Setup](https://docs.aws.amazon.com/sdk-for-net/latest/developer-guide/net-dg-config-netcore.html) with a few differences. For more on how to use the AWS SDK for .NET, see [Getting Started with the AWS SDK for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-setup.html). @@ -189,4 +234,4 @@ Please refer to [`CHANGELOG.md`](CHANGELOG.md) to see the complete list of chang ## License -Licensed under MIT, see [LICENSE](LICENSE) for the full text. \ No newline at end of file +Licensed under MIT, see [LICENSE](LICENSE) for the full text. diff --git a/src/LocalStack.Client/GlobalUsings.cs b/src/LocalStack.Client/GlobalUsings.cs index e7aff8f..df142da 100644 --- a/src/LocalStack.Client/GlobalUsings.cs +++ b/src/LocalStack.Client/GlobalUsings.cs @@ -18,7 +18,7 @@ global using LocalStack.Client.Utils; #pragma warning disable MA0048 // File name must match type name -#if NETSTANDARD || NET462 +#if NETSTANDARD || NET472 namespace System.Runtime.CompilerServices { using System.ComponentModel; diff --git a/src/LocalStack.Client/LocalStack.Client.csproj b/src/LocalStack.Client/LocalStack.Client.csproj index 6812224..3a7e54c 100644 --- a/src/LocalStack.Client/LocalStack.Client.csproj +++ b/src/LocalStack.Client/LocalStack.Client.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net462;net8.0;net9.0 + netstandard2.0;net472;net8.0;net9.0 LocalStack.Client LocalStack.Client @@ -51,8 +51,8 @@ - - NET462 + + NET472 \ No newline at end of file diff --git a/src/LocalStack.Client/README.md b/src/LocalStack.Client/README.md index 4bec638..51c4faa 100644 --- a/src/LocalStack.Client/README.md +++ b/src/LocalStack.Client/README.md @@ -1,21 +1,42 @@ -# LocalStack .NET Client ![Nuget](https://img.shields.io/nuget/dt/LocalStack.Client) [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) [![Space Metric](https://localstack-dotnet.testspace.com/spaces/232580/badge?token=bc6aa170f4388c662b791244948f6d2b14f16983)](https://localstack-dotnet.testspace.com/spaces/232580?utm_campaign=metric&utm_medium=referral&utm_source=badge "Test Cases") +# LocalStack .NET Client ![Nuget](https://img.shields.io/nuget/dt/LocalStack.Client) [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml) [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml) + +> ## ⚠️ AWS SDK v4 Transition Notice +> +> **Current Status**: This main branch is under active development for **AWS SDK v4 support (v2.0)** +> +> **Version Strategy**: +> - **v1.x** (AWS SDK v3): Maintenance mode → Available on [sdkv3-lts branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/sdkv3-lts) (EOL: July 31, 2026) +> - **v2.x** (AWS SDK v4): Active development → Native AOT support in subsequent v2.x releases +> +> **Migration Timeline**: Q3 2025 for v2.0.0 GA +> +> 📖 **[Read Full Roadmap & Migration Guide →](../../discussions)** ![LocalStack](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/master/assets/localstack-dotnet.png?raw=true) Localstack.NET is an easy-to-use .NET client for [LocalStack](https://github.com/localstack/localstack), a fully functional local AWS cloud stack. The client library provides a thin wrapper around [aws-sdk-net](https://github.com/aws/aws-sdk-net) which automatically configures the target endpoints to use LocalStack for your local cloud application development. -| Package | Stable | Nightly | +## Version Compatibility + +| LocalStack.NET Version | AWS SDK Version | .NET Support | Status | Branch | +|------------------------|-----------------|--------------|---------|---------| +| v1.x | AWS SDK v3 | .NET 8, 9, Standard 2.0, Framework 4.6.2 | Maintenance (until July 2026) | [sdkv3-lts](../../tree/sdkv3-lts) | +| v2.x | AWS SDK v4 | .NET 8, 9, Standard 2.0, Framework 4.7.2 | Active Development | [master](../../tree/main) | + +## Package Status + +| Package | v1.x (AWS SDK v3) | v2.x (AWS SDK v4) - Development | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| LocalStack.Client | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) | [![MyGet](https://img.shields.io/myget/localstack-dotnet-client/v/LocalStack.Client.svg?label=myget)](https://www.myget.org/feed/localstack-dotnet-client/package/nuget/LocalStack.Client) | -| LocalStack.Client.Extensions | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.Extensions.svg)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![MyGet](https://img.shields.io/myget/localstack-dotnet-client/v/LocalStack.Client.Extensions.svg?label=myget)](https://www.myget.org/feed/localstack-dotnet-client/package/nuget/LocalStack.Client.Extensions) | +| LocalStack.Client | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.svg)](https://www.nuget.org/packages/LocalStack.Client/) | [![GitHub Packages](https://img.shields.io/github/v/release/localstack-dotnet/localstack-dotnet-client?include_prereleases&label=github%20packages)](https://github.com/localstack-dotnet/localstack-dotnet-client/packages) | +| LocalStack.Client.Extensions | [![NuGet](https://img.shields.io/nuget/v/LocalStack.Client.Extensions.svg)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![GitHub Packages](https://img.shields.io/github/v/release/localstack-dotnet/localstack-dotnet-client?include_prereleases&label=github%20packages)](https://github.com/localstack-dotnet/localstack-dotnet-client/packages) | ## Continuous Integration -| Build server | Platform | Build status | -| -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Github Actions | Ubuntu | [![build-ubuntu](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-ubuntu.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-ubuntu.yml) | -| Github Actions | Windows | [![build-windows](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-windows.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-windows.yml) | -| Github Actions | macOS | [![build-macos](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-macos.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/build-macos.yml) | +| Build Platform | Status | Description | +|----------------|--------|-------------| +| **Cross-Platform CI** | [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci.yml) | Matrix testing: Windows, Linux, macOS | +| **Security Analysis** | [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/security.yml) | CodeQL analysis & dependency review | +| **Automated Publishing** | [![Auto Publish](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/publish-dev-github.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/publish-dev-github.yml) | Daily GitHub Packages builds | ## Table of Contents @@ -60,13 +81,37 @@ For detailed installation and setup instructions, please refer to the [official ## Getting Started -LocalStack.NET is installed from NuGet. To work with LocalStack in your .NET applications, you'll need the main library and its extensions. Here's how you can install them: +LocalStack.NET is available through multiple package sources to support different development workflows. + +### 📦 Package Installation + +#### Stable Releases (NuGet.org) + +For production use and stable releases: ```bash dotnet add package LocalStack.Client dotnet add package LocalStack.Client.Extensions ``` +#### Development Builds (GitHub Packages) + +For testing latest features and bug fixes: + +```bash +# Add GitHub Packages source +dotnet nuget add source https://nuget.pkg.github.com/localstack-dotnet/index.json \ + --name github-localstack \ + --username YOUR_GITHUB_USERNAME \ + --password YOUR_GITHUB_TOKEN + +# Install development packages +dotnet add package LocalStack.Client --prerelease --source github-localstack +dotnet add package LocalStack.Client.Extensions --prerelease --source github-localstack +``` + +> **🔑 GitHub Packages Authentication**: You'll need a GitHub account and [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with `read:packages` permission. + Refer to [documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Getting-Started#installation) for more information on how to install LocalStack.NET. `LocalStack.NET` is a library that provides a wrapper around the [aws-sdk-net](https://github.com/aws/aws-sdk-net). This means you can use it in a similar way to the `AWS SDK for .NET` and to [AWSSDK.Extensions.NETCore.Setup](https://docs.aws.amazon.com/sdk-for-net/latest/developer-guide/net-dg-config-netcore.html) with a few differences. For more on how to use the AWS SDK for .NET, see [Getting Started with the AWS SDK for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-setup.html). @@ -189,4 +234,4 @@ Please refer to [`CHANGELOG.md`](CHANGELOG.md) to see the complete list of chang ## License -Licensed under MIT, see [LICENSE](LICENSE) for the full text. \ No newline at end of file +Licensed under MIT, see [LICENSE](LICENSE) for the full text. diff --git a/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs b/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs index dba4981..936e416 100644 --- a/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs +++ b/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs @@ -19,19 +19,40 @@ public void CreateServiceClient_Should_Throw_LocalStackClientConfigurationExcept Type type = _awsClientFactoryWrapper.GetType(); const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Static; - FieldInfo? clientFactoryFullNameField = type.GetField("ClientFactoryFullName", bindingFlags); + FieldInfo? clientFactoryGenericTypeNameField = type.GetField("ClientFactoryGenericTypeName", bindingFlags); FieldInfo? createServiceClientMethodNameFieldInfo = type.GetField("CreateServiceClientMethodName", bindingFlags); - Assert.NotNull(clientFactoryFullNameField); + Assert.NotNull(clientFactoryGenericTypeNameField); Assert.NotNull(createServiceClientMethodNameFieldInfo); - SetPrivateReadonlyField(clientFactoryFullNameField, "NonExistingType"); + SetPrivateReadonlyField(clientFactoryGenericTypeNameField, "NonExistingType"); SetPrivateReadonlyField(createServiceClientMethodNameFieldInfo, "NonExistingMethod"); Assert.Throws( () => _awsClientFactoryWrapper.CreateServiceClient(_mockServiceProvider.Object, _awsOptions)); } + [Fact] + public void CreateServiceClient_Should_Throw_LocalStackClientConfigurationException_When_ClientFactory_Type_Not_Found() + { + Type type = _awsClientFactoryWrapper.GetType(); + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Static; + + FieldInfo? clientFactoryGenericTypeNameField = type.GetField("ClientFactoryGenericTypeName", bindingFlags); + FieldInfo? createServiceClientMethodNameFieldInfo = type.GetField("CreateServiceClientMethodName", bindingFlags); + + Assert.NotNull(clientFactoryGenericTypeNameField); + Assert.NotNull(createServiceClientMethodNameFieldInfo); + + SetPrivateReadonlyField(clientFactoryGenericTypeNameField, "NonExistingType"); + SetPrivateReadonlyField(createServiceClientMethodNameFieldInfo, "NonExistingMethod"); + + var exception = Assert.Throws( + () => _awsClientFactoryWrapper.CreateServiceClient(_mockServiceProvider.Object, _awsOptions)); + + Assert.Contains("Failed to find internal ClientFactory", exception.Message, StringComparison.Ordinal); + } + [Fact] public void CreateServiceClient_Should_Create_Client_When_UseLocalStack_False() { diff --git a/tests/LocalStack.Client.Extensions.Tests/ServiceCollectionExtensionsTests.cs b/tests/LocalStack.Client.Extensions.Tests/ServiceCollectionExtensionsTests.cs index ec73040..23a9934 100644 --- a/tests/LocalStack.Client.Extensions.Tests/ServiceCollectionExtensionsTests.cs +++ b/tests/LocalStack.Client.Extensions.Tests/ServiceCollectionExtensionsTests.cs @@ -324,9 +324,9 @@ public void GetRequiredService_Should_Use_Suitable_ClientFactory_To_Create_AwsSe ServiceProvider provider = serviceCollection.BuildServiceProvider(); mockSession.Setup(session => session.CreateClientByInterface(It.IsAny())) - .Returns(() => new MockAmazonServiceClient("tsada", "sadasdas", "sadasda", new MockClientConfig())); + .Returns(() => new MockAmazonServiceClient("tsada", "sadasdas", "sadasda", MockClientConfig.CreateDefaultMockClientConfig())); mockClientFactory.Setup(wrapper => wrapper.CreateServiceClient(It.IsAny(), It.IsAny())) - .Returns(() => new MockAmazonServiceClient("tsada", "sadasdas", "sadasda", new MockClientConfig())); + .Returns(() => new MockAmazonServiceClient("tsada", "sadasdas", "sadasda", MockClientConfig.CreateDefaultMockClientConfig())); var mockAmazonService = provider.GetRequiredService(); diff --git a/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationStackExecutor.cs b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationStackExecutor.cs index 679dc50..d17323e 100644 --- a/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationStackExecutor.cs +++ b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationStackExecutor.cs @@ -1,4 +1,6 @@ -using Tag = Amazon.CloudFormation.Model.Tag; +#pragma warning disable CA2254 + +using Tag = Amazon.CloudFormation.Model.Tag; namespace LocalStack.Client.Functional.Tests.CloudFormation; @@ -190,11 +192,11 @@ private async Task ExecuteChangeSetAsync(string changeSetId, ChangeSetTyp if (changeSetType == ChangeSetType.CREATE) { - logger.LogInformation($"Initiated CloudFormation stack creation for {cloudFormationResource.Name}"); + logger.LogInformation("Initiated CloudFormation stack creation for {Name}", cloudFormationResource.Name); } else { - logger.LogInformation($"Initiated CloudFormation stack update on {cloudFormationResource.Name}"); + logger.LogInformation("Initiated CloudFormation stack update on {Name}", cloudFormationResource.Name); } } catch (Exception e) @@ -244,7 +246,7 @@ private async Task DetermineChangeSetTypeAsync(Stack? stack, Canc changeSetType = ChangeSetType.CREATE; } - // If the status was DELETE_IN_PROGRESS then just wait for delete to complete + // If the status was DELETE_IN_PROGRESS then just wait for delete to complete else if (stack.StackStatus == StackStatus.DELETE_IN_PROGRESS) { await WaitForNoLongerInProgressAsync(cancellationToken).ConfigureAwait(false); @@ -327,7 +329,7 @@ private async Task DeleteRollbackCompleteStackAsync(Stack stack, CancellationTok if (currentStack != null) { logger.LogInformation( - $"... Waiting for stack's state to change from {currentStack.StackStatus}: {TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start).TotalSeconds.ToString("0", CultureInfo.InvariantCulture).PadLeft(3)} secs"); + "... Waiting for stack's state to change from {CurrentStackStackStatus}: {PadLeft} secs", currentStack.StackStatus, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start).TotalSeconds.ToString("0", CultureInfo.InvariantCulture).PadLeft(3)); } await Task.Delay(StackPollingDelay, cancellation).ConfigureAwait(false); @@ -422,7 +424,7 @@ private async Task WaitStackToCompleteAsync(DateTimeOffset minTimeStampFo for (int i = events.Count - 1; i >= 0; i--) { var line = new StringBuilder(); - line.Append(events[i].Timestamp.ToString("g", CultureInfo.InvariantCulture).PadRight(TIMESTAMP_WIDTH)); + line.Append(events[i].Timestamp?.ToString("g", CultureInfo.InvariantCulture).PadRight(TIMESTAMP_WIDTH)); line.Append(' '); line.Append(events[i].LogicalResourceId.PadRight(LOGICAL_RESOURCE_WIDTH)); line.Append(' '); @@ -435,9 +437,9 @@ private async Task WaitStackToCompleteAsync(DateTimeOffset minTimeStampFo line.Append(events[i].ResourceStatusReason); } - if (minTimeStampForEvents < events[i].Timestamp) + if (minTimeStampForEvents < events[i].Timestamp && events[i].Timestamp != null) { - minTimeStampForEvents = events[i].Timestamp; + minTimeStampForEvents = (DateTimeOffset)events[i].Timestamp!; } logger.LogInformation(line.ToString()); diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/BaseDynamoDbScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/BaseDynamoDbScenario.cs index a97e3b3..79261b5 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/BaseDynamoDbScenario.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/BaseDynamoDbScenario.cs @@ -10,7 +10,7 @@ protected BaseDynamoDbScenario(TestFixture testFixture, ILocalStackFixture local bool useServiceUrl = false) : base(testFixture, localStackFixture, configFile, useServiceUrl) { DynamoDb = ServiceProvider.GetRequiredService(); - DynamoDbContext = new DynamoDBContext(DynamoDb); + DynamoDbContext = new DynamoDBContextBuilder().WithDynamoDBClient(() => DynamoDb).Build(); } protected IAmazonDynamoDB DynamoDb { get; private set; } @@ -39,19 +39,36 @@ public virtual async Task DynamoDbService_Should_Delete_A_DynamoDb_Table_Async() public virtual async Task DynamoDbService_Should_Add_A_Record_To_A_DynamoDb_Table_Async() { var tableName = Guid.NewGuid().ToString(); - var dynamoDbOperationConfig = new DynamoDBOperationConfig() { OverrideTableName = tableName }; + + // Fix: Use GetTargetTableConfig instead of DynamoDBOperationConfig + var getTargetTableConfig = new GetTargetTableConfig() { OverrideTableName = tableName }; + await CreateTestTableAsync(tableName); - Table targetTable = DynamoDbContext.GetTargetTable(dynamoDbOperationConfig); + var describeResponse = await DynamoDb.DescribeTableAsync(new DescribeTableRequest(tableName)); + var gsiExists = describeResponse.Table.GlobalSecondaryIndexes?.Exists(gsi => gsi.IndexName == TestConstants.MovieTableMovieIdGsi) == true; + + if (!gsiExists) + { + var availableGsis = describeResponse.Table.GlobalSecondaryIndexes?.Select(g => g.IndexName).ToArray() ?? ["none"]; + + throw new System.InvalidOperationException($"GSI '{TestConstants.MovieTableMovieIdGsi}' was not found on table '{tableName}'. " + + $"Available GSIs: {string.Join(", ", availableGsis)}"); + } + + // Fix: Cast to Table and use GetTargetTableConfig + var targetTable = (Table)DynamoDbContext.GetTargetTable(getTargetTableConfig); var movieEntity = new Fixture().Create(); string modelJson = JsonSerializer.Serialize(movieEntity); Document item = Document.FromJson(modelJson); await targetTable.PutItemAsync(item); - dynamoDbOperationConfig.IndexName = TestConstants.MovieTableMovieIdGsi; - List movieEntities = - await DynamoDbContext.QueryAsync(movieEntity.MovieId, dynamoDbOperationConfig).GetRemainingAsync(); + + // Fix: Use QueryConfig instead of DynamoDBOperationConfig + var queryConfig = new QueryConfig() { OverrideTableName = tableName, IndexName = TestConstants.MovieTableMovieIdGsi }; + + List movieEntities = await DynamoDbContext.QueryAsync(movieEntity.MovieId, queryConfig).GetRemainingAsync(); Assert.True(movieEntity.DeepEquals(movieEntities[0])); } @@ -62,28 +79,30 @@ public virtual async Task DynamoDbService_Should_List_Records_In_A_DynamoDb_Tabl var tableName = Guid.NewGuid().ToString(); const int recordCount = 5; - var dynamoDbOperationConfig = new DynamoDBOperationConfig() { OverrideTableName = tableName }; + // Fix: Use GetTargetTableConfig instead of DynamoDBOperationConfig + var getTargetTableConfig = new GetTargetTableConfig() { OverrideTableName = tableName }; await CreateTestTableAsync(tableName); - Table targetTable = DynamoDbContext.GetTargetTable(dynamoDbOperationConfig); - List movieEntities = new Fixture().CreateMany(recordCount).ToList(); - List documents = movieEntities.Select(entity => + // Fix: Cast to Table and use GetTargetTableConfig + var targetTable = (Table)DynamoDbContext.GetTargetTable(getTargetTableConfig); + List movieEntities = [.. new Fixture().CreateMany(recordCount)]; + List documents = [.. movieEntities.Select(entity => { string serialize = JsonSerializer.Serialize(entity); Document item = Document.FromJson(serialize); return item; - }) - .ToList(); + }),]; foreach (Document document in documents) { await targetTable.PutItemAsync(document); } - dynamoDbOperationConfig.IndexName = TestConstants.MovieTableMovieIdGsi; - List returnedMovieEntities = - await DynamoDbContext.ScanAsync(new List(), dynamoDbOperationConfig).GetRemainingAsync(); + // Fix: Use ScanConfig instead of DynamoDBOperationConfig + var scanConfig = new ScanConfig() { OverrideTableName = tableName, IndexName = TestConstants.MovieTableMovieIdGsi }; + + List returnedMovieEntities = await DynamoDbContext.ScanAsync(new List(), scanConfig).GetRemainingAsync(); Assert.NotNull(movieEntities); Assert.NotEmpty(movieEntities); @@ -101,29 +120,28 @@ protected Task CreateTestTableAsync(string? tableName = nul var postTableCreateRequest = new CreateTableRequest { AttributeDefinitions = - new List - { - new() { AttributeName = nameof(MovieEntity.DirectorId), AttributeType = ScalarAttributeType.S }, - new() { AttributeName = nameof(MovieEntity.CreateDate), AttributeType = ScalarAttributeType.S }, - new() { AttributeName = nameof(MovieEntity.MovieId), AttributeType = ScalarAttributeType.S }, - }, + [ + new AttributeDefinition { AttributeName = nameof(MovieEntity.DirectorId), AttributeType = ScalarAttributeType.S }, + new AttributeDefinition { AttributeName = nameof(MovieEntity.CreateDate), AttributeType = ScalarAttributeType.S }, + new AttributeDefinition { AttributeName = nameof(MovieEntity.MovieId), AttributeType = ScalarAttributeType.S }, + ], TableName = tableName ?? TestTableName, KeySchema = - new List() - { - new() { AttributeName = nameof(MovieEntity.DirectorId), KeyType = KeyType.HASH }, - new() { AttributeName = nameof(MovieEntity.CreateDate), KeyType = KeyType.RANGE }, - }, - GlobalSecondaryIndexes = new List - { - new() + [ + new KeySchemaElement { AttributeName = nameof(MovieEntity.DirectorId), KeyType = KeyType.HASH }, + new KeySchemaElement { AttributeName = nameof(MovieEntity.CreateDate), KeyType = KeyType.RANGE }, + ], + GlobalSecondaryIndexes = + [ + new GlobalSecondaryIndex { Projection = new Projection { ProjectionType = ProjectionType.ALL }, IndexName = TestConstants.MovieTableMovieIdGsi, - KeySchema = new List { new() { AttributeName = nameof(MovieEntity.MovieId), KeyType = KeyType.HASH } }, - ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 5, WriteCapacityUnits = 5 } - } - }, + KeySchema = [new KeySchemaElement { AttributeName = nameof(MovieEntity.MovieId), KeyType = KeyType.HASH }], + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 5, WriteCapacityUnits = 5 }, + }, + + ], ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 5, WriteCapacityUnits = 6 }, }; diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/Entities/MovieEntity.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/Entities/MovieEntity.cs index 3ae67ca..1e5d4be 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/Entities/MovieEntity.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/Entities/MovieEntity.cs @@ -8,10 +8,10 @@ public class MovieEntity public Guid DirectorId { get; set; } [DynamoDBRangeKey] - public string CreateDate { get; set; } + [DynamoDBGlobalSecondaryIndexHashKey(TestConstants.MovieTableMovieIdGsi)] public Guid MovieId { get; set; } public string MovieName { get; set; } -} +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/BaseRealLife.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/BaseRealLife.cs index 9a18454..11af890 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/BaseRealLife.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/BaseRealLife.cs @@ -66,7 +66,7 @@ public virtual async Task Assert.Equal(HttpStatusCode.OK, receiveMessageResponse.HttpStatusCode); - if (receiveMessageResponse.Messages.Count == 0) + if ((receiveMessageResponse.Messages?.Count ?? 0) == 0) { await Task.Delay(2000); receiveMessageResponse = await AmazonSqs.ReceiveMessageAsync(receiveMessageRequest); @@ -75,7 +75,7 @@ public virtual async Task } Assert.NotNull(receiveMessageResponse.Messages); - Assert.NotEmpty(receiveMessageResponse.Messages); + Assert.NotEmpty(receiveMessageResponse.Messages!); Assert.Single(receiveMessageResponse.Messages); dynamic? deserializedMessage = JsonConvert.DeserializeObject(receiveMessageResponse.Messages[0].Body, new ExpandoObjectConverter()); diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs index 45bcaef..97f2912 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs @@ -22,7 +22,7 @@ public async Task SnsService_Should_Create_A_Sns_Topic_Async() Assert.Equal(HttpStatusCode.OK, createTopicResponse.HttpStatusCode); ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync(); - Topic? snsTopic = listTopicsResponse.Topics.SingleOrDefault(topic => topic.TopicArn == createTopicResponse.TopicArn); + Topic? snsTopic = listTopicsResponse.Topics?.SingleOrDefault(topic => topic.TopicArn == createTopicResponse.TopicArn); Assert.NotNull(snsTopic); Assert.EndsWith(topicName, snsTopic.TopicArn, StringComparison.Ordinal); @@ -41,7 +41,7 @@ public async Task SnsService_Should_Delete_A_Sns_Topic_Async() Assert.Equal(HttpStatusCode.OK, deleteTopicResponse.HttpStatusCode); ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync(); - bool hasAny = listTopicsResponse.Topics.Exists(topic => topic.TopicArn == createTopicResponse.TopicArn); + bool hasAny = listTopicsResponse.Topics?.Exists(topic => topic.TopicArn == createTopicResponse.TopicArn) ?? false; Assert.False(hasAny); } @@ -91,9 +91,10 @@ public virtual async Task Multi_Region_Tests_Async(string systemName) var topicArn = $"arn:aws:sns:{systemName}:000000000000:{topicName}"; ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync(); - Topic? snsTopic = listTopicsResponse.Topics.SingleOrDefault(topic => topic.TopicArn == topicArn); + Topic? snsTopic = listTopicsResponse.Topics?.SingleOrDefault(topic => topic.TopicArn == topicArn); Assert.NotNull(snsTopic); + Assert.NotNull(listTopicsResponse.Topics); Assert.Single(listTopicsResponse.Topics); await DeleteSnsTopicAsync(topicArn); //Cleanup diff --git a/tests/LocalStack.Client.Functional.Tests/TestConstants.cs b/tests/LocalStack.Client.Functional.Tests/TestConstants.cs index 3fa982e..8f34b0e 100644 --- a/tests/LocalStack.Client.Functional.Tests/TestConstants.cs +++ b/tests/LocalStack.Client.Functional.Tests/TestConstants.cs @@ -7,5 +7,5 @@ public static class TestConstants public const string LocalStackV37 = "3.7.1"; public const string LocalStackV43 = "4.3.0"; - public const string MovieTableMovieIdGsi = "MoiveTableMovie-Index"; + public const string MovieTableMovieIdGsi = "MovieTableMovie-Index"; } \ No newline at end of file diff --git a/tests/LocalStack.Client.Integration.Tests/LocalStack.Client.Integration.Tests.csproj b/tests/LocalStack.Client.Integration.Tests/LocalStack.Client.Integration.Tests.csproj index a2c62b6..b0fafab 100644 --- a/tests/LocalStack.Client.Integration.Tests/LocalStack.Client.Integration.Tests.csproj +++ b/tests/LocalStack.Client.Integration.Tests/LocalStack.Client.Integration.Tests.csproj @@ -1,7 +1,7 @@  - net462;net8.0;net9.0 + net472;net8.0;net9.0 $(NoWarn);CA1707;MA0006;CA1510 @@ -128,7 +128,7 @@ - + diff --git a/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj b/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj index 77b86ff..e59c51e 100644 --- a/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj +++ b/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj @@ -1,7 +1,7 @@  - net462;net8.0;net9.0 + net472;net8.0;net9.0 $(NoWarn);CA1707;MA0006 @@ -21,7 +21,7 @@ - + diff --git a/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs b/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs index 682c0d1..b28ba87 100644 --- a/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs +++ b/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs @@ -37,7 +37,7 @@ public void CreateClientByImplementation_Should_Create_SessionAWSCredentials_Wit var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (string awsAccessKeyId, string awsAccessKey, string awsSessionToken, _) = mockSession.SessionOptionsMock.SetupDefault(); @@ -71,7 +71,7 @@ public void CreateClientByImplementation_Should_Create_ClientConfig_With_UseHttp var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -98,7 +98,7 @@ public void CreateClientByImplementation_Should_Create_ClientConfig_With_UseHttp var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); var configOptions = new ConfigOptions(); mockSession.SessionOptionsMock.SetupDefault(); @@ -134,7 +134,7 @@ public void CreateClientByImplementation_Should_Set_RegionEndpoint_By_RegionName var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); @@ -161,7 +161,7 @@ public void CreateClientByImplementation_Should_Set_ServiceUrl_By_ServiceEndpoin var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); #pragma warning disable CS8604 // Possible null reference argument. mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); @@ -187,7 +187,7 @@ public void CreateClientByImplementation_Should_Pass_The_ClientConfig_To_SetForc var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -211,7 +211,7 @@ public void CreateClientByImplementation_Should_Create_AmazonServiceClient_By_Gi var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(); @@ -288,7 +288,7 @@ public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_Set_ var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -315,7 +315,7 @@ public void CreateClientByInterface_Should_Create_SessionAWSCredentials_With_Aws var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (string awsAccessKeyId, string awsAccessKey, string awsSessionToken, _) = mockSession.SessionOptionsMock.SetupDefault(); @@ -347,7 +347,7 @@ public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_And_ var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); var configOptions = new ConfigOptions(); mockSession.SessionOptionsMock.SetupDefault(); @@ -385,7 +385,7 @@ public void CreateClientByInterface_Should_Set_RegionEndpoint_By_RegionName_Prop var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); @@ -413,7 +413,7 @@ public void CreateClientByInterface_Should_Set_ServiceUrl_By_ServiceEndpoint_Con var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); #pragma warning disable CS8604 // Possible null reference argument. mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); @@ -439,7 +439,7 @@ public void CreateClientByInterface_Should_Pass_The_ClientConfig_To_SetForcePath var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -464,7 +464,7 @@ public void CreateClientByInterface_Should_Create_AmazonServiceClient_By_Given_G var mockSession = MockSession.Create(); var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); var configOptions = new ConfigOptions(); (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(); diff --git a/tests/LocalStack.Client.Tests/SessionTests/SessionReflectionTests.cs b/tests/LocalStack.Client.Tests/SessionTests/SessionReflectionTests.cs index 0b3e9e7..90778e7 100644 --- a/tests/LocalStack.Client.Tests/SessionTests/SessionReflectionTests.cs +++ b/tests/LocalStack.Client.Tests/SessionTests/SessionReflectionTests.cs @@ -36,9 +36,9 @@ public void CreateClientConfig_Should_Create_ClientConfig_By_Given_Generic_Servi public void SetForcePathStyle_Should_Return_False_If_Given_ClientConfig_Does_Not_Have_ForcePathStyle() { var sessionReflection = new SessionReflection(); - var clientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); - bool set = sessionReflection.SetForcePathStyle(clientConfig, true); + bool set = sessionReflection.SetForcePathStyle(mockClientConfig, true); Assert.False(set); } @@ -47,7 +47,7 @@ public void SetForcePathStyle_Should_Return_False_If_Given_ClientConfig_Does_Not public void SetForcePathStyle_Should_Set_ForcePathStyle_Of_ClientConfig_If_It_Exists() { var sessionReflection = new SessionReflection(); - var clientConfig = new MockClientConfigWithForcePathStyle(); + var clientConfig = MockClientConfigWithForcePathStyle.CreateDefaultMockClientConfigWithForcePathStyle(); Assert.False(clientConfig.ForcePathStyle); @@ -57,11 +57,11 @@ public void SetForcePathStyle_Should_Set_ForcePathStyle_Of_ClientConfig_If_It_Ex Assert.True(clientConfig.ForcePathStyle); } - [Theory, - InlineData("eu-central-1"), - InlineData("us-west-1"), + [Theory, + InlineData("eu-central-1"), + InlineData("us-west-1"), InlineData("af-south-1"), - InlineData("ap-southeast-1"), + InlineData("ap-southeast-1"), InlineData("ca-central-1"), InlineData("eu-west-2"), InlineData("sa-east-1")] @@ -77,4 +77,4 @@ public void SetClientRegion_Should_Set_RegionEndpoint_Of_The_Given_Client_By_Sys Assert.NotNull(mockAmazonServiceClient.Config.RegionEndpoint); Assert.Equal(RegionEndpoint.GetBySystemName(systemName), mockAmazonServiceClient.Config.RegionEndpoint); } -} +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/LocalStack.Tests.Common.csproj b/tests/common/LocalStack.Tests.Common/LocalStack.Tests.Common.csproj index 27948d3..d7d39e1 100644 --- a/tests/common/LocalStack.Tests.Common/LocalStack.Tests.Common.csproj +++ b/tests/common/LocalStack.Tests.Common/LocalStack.Tests.Common.csproj @@ -1,7 +1,7 @@ - net462;net8.0;net9.0 + net472;net8.0;net9.0 $(NoWarn);CA1707;MA0006;CA1510 @@ -10,7 +10,7 @@ - + diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs index 956c53f..d5c2a9c 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs @@ -2,5 +2,14 @@ public interface IMockAmazonService : IDisposable, IAmazonService { +#if NET8_0_OR_GREATER +#pragma warning disable CA1033 + static ClientConfig IAmazonService.CreateDefaultClientConfig() => MockClientConfig.CreateDefaultMockClientConfig(); -} + static IAmazonService IAmazonService.CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) + { + return new MockAmazonServiceClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); + } +#pragma warning restore CA1033 +#endif +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs index 6ec4cb9..c1dbf29 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs @@ -2,5 +2,14 @@ public interface IMockAmazonServiceWithServiceMetadata : IDisposable, IAmazonService { +#if NET8_0_OR_GREATER +#pragma warning disable CA1033 + static ClientConfig IAmazonService.CreateDefaultClientConfig() => MockClientConfig.CreateDefaultMockClientConfig(); -} + static IAmazonService IAmazonService.CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) + { + return new MockAmazonServiceWithServiceMetadataClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); + } +#pragma warning restore CA1033 +#endif +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs index fbf19bb..01b2e73 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs @@ -1,8 +1,10 @@ -namespace LocalStack.Tests.Common.Mocks.MockServiceClients; +#pragma warning disable S2325,CA1822 + +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; public class MockAmazonServiceClient : AmazonServiceClient, IMockAmazonService { - public MockAmazonServiceClient() : base(new MockCredentials(), new MockClientConfig()) + public MockAmazonServiceClient() : base(new MockCredentials(), new MockClientConfig(new MockConfigurationProvider())) { } @@ -20,10 +22,17 @@ public MockAmazonServiceClient(string awsAccessKeyId, string awsSecretAccessKey, { } - public AWSCredentials AwsCredentials => Credentials; + public AWSCredentials AwsCredentials => Config.DefaultAWSCredentials; + +#if NET8_0_OR_GREATER + public static ClientConfig CreateDefaultClientConfig() + { + return MockClientConfig.CreateDefaultMockClientConfig(); + } - protected override AbstractAWSSigner CreateSigner() + public static IAmazonService CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) { - return new NullSigner(); + return new MockAmazonServiceClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); } -} +#endif +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs index 4a2343c..05d93b0 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs @@ -1,11 +1,13 @@ -#pragma warning disable S1144, CA1823 +using Amazon.Runtime.Credentials; + +#pragma warning disable S1144, CA1823 namespace LocalStack.Tests.Common.Mocks.MockServiceClients; public class MockAmazonServiceWithServiceMetadataClient : AmazonServiceClient, IMockAmazonServiceWithServiceMetadata { private static IServiceMetadata serviceMetadata = new MockServiceMetadata(); - public MockAmazonServiceWithServiceMetadataClient() : base(FallbackCredentialsFactory.GetCredentials(), new MockClientConfig()) + public MockAmazonServiceWithServiceMetadataClient() : base(DefaultAWSCredentialsIdentityResolver.GetCredentials(), MockClientConfig.CreateDefaultMockClientConfig()) { } @@ -23,10 +25,15 @@ public MockAmazonServiceWithServiceMetadataClient(string awsAccessKeyId, string { } - public AWSCredentials AwsCredentials => Credentials; +#if NET8_0_OR_GREATER + public static ClientConfig CreateDefaultClientConfig() + { + return MockClientConfig.CreateDefaultMockClientConfig(); + } - protected override AbstractAWSSigner CreateSigner() + public static IAmazonService CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) { - return new NullSigner(); + return new MockAmazonServiceWithServiceMetadataClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); } -} +#endif +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs index 09a9618..d19c23e 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs @@ -1,8 +1,14 @@ -namespace LocalStack.Tests.Common.Mocks.MockServiceClients; +using Amazon.Runtime.Endpoints; + +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; public class MockClientConfig : ClientConfig, IClientConfig { - public MockClientConfig() + public MockClientConfig() : this(new MockConfigurationProvider()) + { + } + + public MockClientConfig(IDefaultConfigurationProvider configurationProvider) : base(configurationProvider) { ServiceURL = "http://localhost"; } @@ -11,5 +17,12 @@ public MockClientConfig() public override string UserAgent => InternalSDKUtils.BuildUserAgentString(ServiceVersion); + public override Endpoint DetermineServiceOperationEndpoint(ServiceOperationEndpointParameters parameters) + { + return new Endpoint(ServiceURL); + } + public override string RegionEndpointServiceName => "mock-service"; -} + + public static MockClientConfig CreateDefaultMockClientConfig() => new(new MockConfigurationProvider()); +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs index e6c7346..688b506 100644 --- a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs @@ -2,5 +2,12 @@ public class MockClientConfigWithForcePathStyle : MockClientConfig { + public MockClientConfigWithForcePathStyle(IDefaultConfigurationProvider configurationProvider, bool forcePathStyle) : base(configurationProvider) + { + ForcePathStyle = forcePathStyle; + } + public bool ForcePathStyle { get; set; } -} + + public static MockClientConfigWithForcePathStyle CreateDefaultMockClientConfigWithForcePathStyle() => new(new MockConfigurationProvider(), forcePathStyle: false); +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockConfiguration.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockConfiguration.cs new file mode 100644 index 0000000..36b1e30 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockConfiguration.cs @@ -0,0 +1,18 @@ +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public class MockConfiguration : IDefaultConfiguration +{ + public DefaultConfigurationMode Name { get; } + + public RequestRetryMode RetryMode { get; } + + public S3UsEast1RegionalEndpointValue S3UsEast1RegionalEndpoint { get; } + + public TimeSpan? ConnectTimeout { get; } + + public TimeSpan? TlsNegotiationTimeout { get; } + + public TimeSpan? TimeToFirstByteTimeout { get; } + + public TimeSpan? HttpRequestTimeout { get; } +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockConfigurationProvider.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockConfigurationProvider.cs new file mode 100644 index 0000000..f4e2a39 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockConfigurationProvider.cs @@ -0,0 +1,11 @@ +using Amazon; + +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public class MockConfigurationProvider : IDefaultConfigurationProvider +{ + public IDefaultConfiguration GetDefaultConfiguration(RegionEndpoint clientRegion, DefaultConfigurationMode? requestedConfigurationMode = null) + { + return new MockConfiguration(); + } +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj b/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj index 129fa3a..a35f787 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj +++ b/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj @@ -2,7 +2,7 @@ Exe - net462;net8.0;net9.0 + net472;net8.0;net9.0 $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007