diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..53764f5 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,25 @@ +# Code of Conduct + +This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. + +For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/about/policies/code-of-conduct). + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [localstack.dotnet@gmail.com](mailto:localstack.dotnet@gmail.com) or by contacting the [.NET Foundation](mailto:conduct@dotnetfoundation.org). + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2cabcea..f580012 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,113 +1,206 @@ -# Contributing to [LocalStack.NET Client](https://github.com/localstack-dotnet/localstack-dotnet-client) +# Contributing to LocalStack .NET Client -All kind of pull requests even for things like typos, documentation, test cases, etc are always welcome. By submitting a pull request for this project, you agree to license your contribution under the MIT license to this project. +πŸŽ‰ **Thank you for your interest in contributing to LocalStack .NET Client!** -Please read these guidelines before contributing to LocalStack.NET Client: +We welcome contributions of all kinds - from bug reports and feature requests to code improvements and documentation updates. This guide will help you get started and ensure your contributions have the best chance of being accepted. - - [Question or Problem?](#question) - - [Issues and Bugs](#issue) - - [Feature Requests](#feature) - - [Submitting a Pull Request](#submit-pull-request) - - [Getting Started](#getting-started) - - [Pull Requests](#pull-requests) +## πŸ“‹ Quick Reference -## Got a Question or Problem? +- πŸ› **Found a bug?** β†’ [Create an Issue](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/new) +- πŸ’‘ **Have an idea?** β†’ [Start a Discussion](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions) +- ❓ **Need help?** β†’ [Q&A Discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/categories/q-a) +- 🚨 **Security issue?** β†’ See our [Security Policy](.github/SECURITY.md) +- πŸ”§ **Ready to code?** β†’ [Submit a Pull Request](https://github.com/localstack-dotnet/localstack-dotnet-client/compare) -If you have questions about how to use LocalStack.NET Client, you can ask by submitting an issue to the [GitHub Repository][github] +## 🀝 Code of Conduct -## Found an Issue? +This project follows the [.NET Foundation Code of Conduct](.github/CODE_OF_CONDUCT.md). By participating, you're expected to uphold this code. Please report unacceptable behavior to [localstack.dotnet@gmail.com](mailto:localstack.dotnet@gmail.com). -If you find a bug in the source code or a mistake in the documentation, you can help by -submitting an issue to the [GitHub Repository][github]. Even better you can submit a Pull Request -with a fix. +## πŸ“ Contributor License Agreement (CLA) -When submitting an issue please include the following information: +**Important**: As this project is pursuing .NET Foundation membership, contributors may be required to sign a Contributor License Agreement (CLA) as part of the contribution process. This helps ensure that contributions can be used by the project and the community. By submitting a pull request, you agree to license your contribution under the MIT license. -- A description of the issue -- The exception message and stacktrace if an error was thrown -- If possible, please include code that reproduces the issue. [DropBox][dropbox] or GitHub's -[Gist][gist] can be used to share large code samples, or you could -[submit a pull request](#pullrequest) with the issue reproduced in a new test. +## 🎯 Version Strategy -The more information you include about the issue, the more likely it is to be fixed! +We maintain a **dual-track versioning strategy**: +- **v2.x (AWS SDK v4)** - Active development on [`master`](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master) branch +- **v1.x (AWS SDK v3)** - Long-term support on [`sdkv3-lts`](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/sdkv3-lts) branch (maintained until July 2026) -## Want a Feature? +When contributing, please specify which version track your contribution targets. -You can request a new feature by submitting an issue to the [GitHub Repository][github] +## πŸš€ Getting Started -## Submitting a Pull Request +### Prerequisites -Good pull requests, patches, improvements and new features are a fantastic -help. They should remain focused in scope and avoid containing unrelatedcahe -commits. When submitting a pull request to the [GitHub Repository][github] make sure to do the following: +- [.NET SDK 8.0+](https://dotnet.microsoft.com/download) (for development) +- [Docker](https://docs.docker.com/get-docker/) (for LocalStack testing) +- [Git](https://git-scm.com/downloads) +- IDE: [Visual Studio](https://visualstudio.microsoft.com/), [Rider](https://www.jetbrains.com/rider/), or [VS Code](https://code.visualstudio.com/) -- Check that new and updated code follows LocalStack.NET Client's existing code formatting and naming standard -- Run LocalStack.NET Client's unit tests to ensure no existing functionality has been affected -- Write new unit tests to test your changes. All features and fixed bugs must have tests to verify -they work +### Development Environment Setup -Read [GitHub Help][pullrequesthelp] for more details about creating pull requests. +1. **Fork and Clone** -### Getting Started - -- Make sure you have a [GitHub account](https://github.com/signup/free) -- Submit a ticket for your issue, assuming one does not already exist. - - Clearly describe the issue including steps to reproduce the bug. -- Fork the repository on GitHub - -### Pull requests - -Adhering to the following process is the best way to get your work -included in the project: + ```bash + # Fork the repository on GitHub, then clone your fork + git clone https://github.com/YOUR-USERNAME/localstack-dotnet-client.git + cd localstack-dotnet-client + + # Add upstream remote + git remote add upstream https://github.com/localstack-dotnet/localstack-dotnet-client.git + ``` -1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, - and configure the remotes: +2. **Build the Project** ```bash - # Clone your fork of the repo into the current directory - git clone git@github.com:/localstack-dotnet-client.git - # Navigate to the newly cloned directory - cd - # Assign the original repo to a remote called "upstream" - git remote add upstream git@github.com:localstack-dotnet/localstack-dotnet-client.git + # Windows + .\build.ps1 + + # Linux/macOS + ./build.sh ``` -2. If you cloned a while ago, get the latest changes from upstream: +3. **Run Tests** ```bash - git checkout master - git pull upstream master + # All tests (requires Docker for functional tests) + .\build.ps1 --target tests + + # Unit/Integration tests only + .\build.ps1 --target tests --skipFunctionalTest true ``` -3. Create a new topic branch (off the main project development branch) to - contain your feature, change, or fix: +## πŸ› Reporting Issues + +### Before Creating an Issue + +1. **Search existing issues** to avoid duplicates +2. **Check [Discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions)** - your question might already be answered +3. **Verify the issue** occurs with LocalStack (not real AWS services) +4. **Test with latest version** when possible + +### Creating a Bug Report + +Use our [Issue Template](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/new) which will guide you through providing: + +- **Environment details** (LocalStack version, .NET version, OS) +- **Minimal reproduction** case +- **Expected vs actual** behavior +- **Configuration** and error messages + +## πŸ’‘ Suggesting Features + +We love new ideas! Here's how to suggest features: + +1. **Check [existing discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/categories/ideas)** for similar requests +2. **Start a [Discussion](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/new?category=ideas)** to gauge community interest +3. **Create an issue** if there's positive feedback and clear requirements + +## πŸ”§ Contributing Code + +### Before You Start + +1. **Discuss significant changes** in [Discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions) first +2. **Check for existing work** - someone might already be working on it +3. **Create an issue** if one doesn't exist (for tracking) + +### Pull Request Process + +1. **Create a feature branch** ```bash - git checkout -b + git checkout -b feature/your-feature-name + # or + git checkout -b fix/issue-number-description ``` -4. Commit your changes in logical chunks. Use Git's - [interactive rebase](https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history) - feature to tidy up your commits before making them public. +2. **Make your changes** + - Follow existing code style and conventions + - Add tests for new functionality + - Update documentation as needed + - Ensure all analyzers pass without warnings -5. Locally merge (or rebase) the upstream development branch into your topic branch: +3. **Test thoroughly** ```bash - git pull [--rebase] upstream master + # Run all tests + ./build.sh --target tests + + # Test specific scenarios with LocalStack + # (see sandbox projects for examples) ``` -6. Push your topic branch up to your fork: +4. **Commit with conventional commits** ```bash - git push origin + git commit -m "feat: add support for XYZ service" + git commit -m "fix: resolve timeout issue in DynamoDB client" + git commit -m "docs: update installation guide" ``` -7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) - with a clear title and description against the `master` branch. +5. **Submit the Pull Request** + - Use our [PR Template](https://github.com/localstack-dotnet/localstack-dotnet-client/compare) + - Provide clear description of changes + - Link related issues + - Specify target version track (v1.x or v2.x) + +### Code Quality Standards + +- βœ… **Follow existing patterns** and architectural decisions +- βœ… **Write comprehensive tests** (unit, integration, functional where applicable) +- βœ… **Add XML documentation** for public APIs +- βœ… **No analyzer warnings** - we treat warnings as errors +- βœ… **Maintain backward compatibility** (unless it's a breaking change PR) +- βœ… **Performance considerations** - avoid introducing regressions + +### Testing Guidelines + +We have multiple test types: + +- **Unit Tests** - Fast, isolated, no external dependencies +- **Integration Tests** - Test AWS SDK integration and client creation +- **Functional Tests** - Full end-to-end with LocalStack containers + +When adding tests: + +- Place them in the appropriate test project +- Follow existing naming conventions +- Test both success and error scenarios +- Include tests for edge cases + +## πŸ“š Documentation + +- **Code comments** - Explain the "why", not the "what" +- **XML documentation** - Required for all public APIs +- **README updates** - For feature additions or breaking changes +- **CHANGELOG** - Add entries for user-facing changes + +## πŸ” Review Process + +1. **Automated checks** must pass (build, tests, code analysis) +2. **Maintainer review** - we aim to review within 48 hours +3. **Community feedback** - other contributors may provide input +4. **Iterative improvements** - address feedback promptly +5. **Final approval** and merge + +## ❓ Getting Help + +- **Questions about usage** β†’ [Q&A Discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/categories/q-a) +- **Ideas for features** β†’ [Ideas Discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/categories/ideas) +- **General discussion** β†’ [General Discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/categories/general) +- **Show your work** β†’ [Show and Tell](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/categories/show-and-tell) + +## πŸŽ‰ Recognition + +Contributors are recognized in: + +- Our [Contributors](https://github.com/localstack-dotnet/localstack-dotnet-client/graphs/contributors) page +- Release notes for significant contributions +- Project documentation for major features + +--- +**By contributing to this project, you agree to abide by our [Code of Conduct](.github/CODE_OF_CONDUCT.md) and understand that your contributions will be licensed under the MIT License.** -[github]: https://github.com/localstack-dotnet/localstack-dotnet-client -[dropbox]: https://www.dropbox.com -[gist]: https://gist.github.com -[pullrequesthelp]: https://help.github.com/articles/using-pull-requests +Thank you for making LocalStack .NET Client better! πŸš€ diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index f8b8ed9..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,17 +0,0 @@ -## Expected Behavior - - -## Actual Behavior - - -## Steps to Reproduce the Problem - - 1. - 1. - 1. - -## Specifications - - - Version: - - Platform: - - Subsystem: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b1d4ca7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,86 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +## πŸ› Bug Description + +**What happened?** +A clear and concise description of the bug. + +**What did you expect to happen?** +A clear and concise description of what you expected to happen. + +## πŸ”„ Steps to Reproduce + +1. +2. +3. +4. + +**Minimal code example:** + +```csharp +// Please provide a minimal code example that reproduces the issue +``` + +## πŸ“‹ Environment Information + +**LocalStack.Client Version:** + +- [ ] v1.x (AWS SDK v3) - LTS Branch +- [ ] v2.x (AWS SDK v4) - Current Branch +- Version: + +**LocalStack Information:** + +- LocalStack Version: +- LocalStack Image: (e.g., `localstack/localstack:3.0`) +- LocalStack Services Used: (e.g., S3, DynamoDB, SNS) + +**.NET Information:** + +- .NET Version: (e.g., .NET 8, .NET Framework 4.7.2) +- Operating System: (e.g., Windows 11, Ubuntu 22.04, macOS 14) +- IDE/Editor: (e.g., Visual Studio 2022, Rider, VS Code) + +**AWS SDK Information:** + +- AWS SDK Version: (e.g., AWSSDK.S3 4.0.4.2) +- Services: (e.g., S3, DynamoDB, SNS) + +## πŸ” Additional Context + +**Configuration (Optional):** + +```json +// Your LocalStack configuration (appsettings.json snippet) +{ + "LocalStack": { + // ... + } +} +``` + +**Error Messages/Stack Traces:** + +``` +Paste any error messages or stack traces here +``` + +**Screenshots:** +If applicable, add screenshots to help explain your problem. + +**Additional Information:** +Add any other context about the problem here. + +## βœ… Checklist + +- [ ] I have searched existing issues to ensure this is not a duplicate +- [ ] I have provided all the requested information above +- [ ] I have tested this with the latest version of LocalStack.Client +- [ ] I have verified this issue occurs with LocalStack (not with real AWS services) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..000290f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,103 @@ + + +## πŸ“ Description + +**What does this PR do?** +Provide a clear and concise description of the changes. + +**Related Issue(s):** + +- Fixes #(issue number) +- Closes #(issue number) +- Related to #(issue number) + +## πŸ”„ Type of Change + +- [ ] πŸ› Bug fix (non-breaking change that fixes an issue) +- [ ] ✨ New feature (non-breaking change that adds functionality) +- [ ] πŸ’₯ Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] πŸ“š Documentation update +- [ ] 🧹 Code cleanup/refactoring +- [ ] ⚑ Performance improvement +- [ ] πŸ§ͺ Test improvements + +## 🎯 Target Version Track + +- [ ] v1.x (AWS SDK v3) - LTS Branch (`sdkv3-lts`) +- [ ] v2.x (AWS SDK v4) - Current Branch (`master`) +- [ ] Both versions (requires separate PRs) + +## πŸ§ͺ Testing + +**How has this been tested?** + +- [ ] Unit tests added/updated +- [ ] Integration tests added/updated +- [ ] Functional tests added/updated +- [ ] Manual testing performed +- [ ] Tested with LocalStack container +- [ ] Tested across multiple .NET versions + +**Test Environment:** + +- LocalStack Version: +- .NET Versions Tested: +- Operating Systems: + +## πŸ“š Documentation + +- [ ] Code is self-documenting with clear naming +- [ ] XML documentation comments added/updated +- [ ] README.md updated (if needed) +- [ ] CHANGELOG.md entry added +- [ ] Breaking changes documented + +## βœ… Code Quality Checklist + +- [ ] Code follows project coding standards +- [ ] No new analyzer warnings introduced +- [ ] All tests pass locally +- [ ] No merge conflicts +- [ ] Branch is up to date with target branch +- [ ] Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) + +## πŸ” Additional Notes + +**Breaking Changes:** +If this is a breaking change, describe the impact and migration path for users. + +**Performance Impact:** +Describe any performance implications of these changes. + +**Dependencies:** +List any new dependencies or version changes. + +## 🎯 Reviewer Focus Areas + +**Please pay special attention to:** + +- [ ] Security implications +- [ ] Performance impact +- [ ] Breaking changes +- [ ] Test coverage +- [ ] Documentation completeness +- [ ] Backward compatibility + +## πŸ“Έ Screenshots/Examples + +If applicable, add screenshots or code examples showing the changes in action. + +```csharp +// Example usage of new feature +``` + +--- + +By submitting this pull request, I confirm that: + +- [ ] I have read and agree to the project's [Code of Conduct](.github/CODE_OF_CONDUCT.md) +- [ ] I understand that this contribution may be subject to the [.NET Foundation CLA](.github/CONTRIBUTING.md) +- [ ] My contribution is licensed under the same terms as the project (MIT License) diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..75221be --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,62 @@ +# Security Policy + +## Supported Versions + +We maintain a dual-track versioning strategy with different support levels for each track. Security patches are released based on the CVSS v3.0 Rating and version track: + +### Version Tracks + +- **v2.x (AWS SDK v4)**: Active development on [master branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master) +- **v1.x (AWS SDK v3)**: Long-term support (LTS) on [sdkv3-lts branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/sdkv3-lts), maintained until **July 2026** + +### Security Patch Policy + +| CVSS v3.0 | v2.x (Current) | v1.x (LTS) | +| --------- | -------------- | ---------- | +| 9.0-10.0 | βœ… All releases within previous 3 months | βœ… Latest LTS release | +| 4.0-8.9 | βœ… Most recent release | βœ… Latest LTS release | +| < 4.0 | ⚠️ Best effort | ❌ No patches (upgrade recommended) | + +## Reporting a Vulnerability + +The LocalStack .NET Client team takes security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. + +**Security Infrastructure**: This repository has GitHub Advanced Security enabled with automated vulnerability detection, dependency scanning, code scanning, and secret detection to help maintain security standards. + +To report a security vulnerability, please use one of the following methods: + +### Preferred Method: GitHub Security Advisories + +1. Go to the [Security tab](https://github.com/localstack-dotnet/localstack-dotnet-client/security) of this repository +2. Click "Report a vulnerability" +3. Fill out the security advisory form with details about the vulnerability + +### Alternative Method: Email + +Send an email to [localstack.dotnet@gmail.com](mailto:localstack.dotnet@gmail.com) with: + +- A clear description of the vulnerability +- Steps to reproduce the issue +- Potential impact of the vulnerability +- Any suggested fixes (if available) + +### Public Issues + +For non-security related bugs, please use our [GitHub Issues](https://github.com/localstack-dotnet/localstack-dotnet-client/issues) tracker. + +## Response Timeline + +We will respond to security vulnerability reports within **48 hours** and will keep you informed throughout the process of fixing the vulnerability. + +## Security Updates + +Security updates will be released as soon as possible after a vulnerability is confirmed and a fix is available. We will: + +1. Confirm the problem and determine the affected versions +2. Audit code to find any potential similar problems +3. Prepare fixes for all supported versions +4. Release new versions as quickly as possible + +## Comments on this Policy + +If you have suggestions on how this process could be improved, please submit a pull request or open an issue to discuss. \ No newline at end of file diff --git a/.github/actions/update-test-badge/README.md b/.github/actions/update-test-badge/README.md new file mode 100644 index 0000000..596bc61 --- /dev/null +++ b/.github/actions/update-test-badge/README.md @@ -0,0 +1,183 @@ +# Update Test Results Badge Action + +A reusable GitHub Action that updates test result badges by uploading test data to GitHub Gist files and displaying badge URLs for README files. + +## Purpose + +This action simplifies the process of maintaining dynamic test result badges by: + +- Creating structured JSON data from test results +- Uploading the data to platform-specific files in a single GitHub Gist +- Providing ready-to-use badge URLs for documentation + +## Usage + +```yaml +- name: "Update Test Results Badge" + uses: ./.github/actions/update-test-badge + with: + platform: "Linux" + gist_id: "472c59b7c2a1898c48a29f3c88897c5a" + filename: "test-results-linux.json" + gist_token: ${{ secrets.GIST_SECRET }} + test_passed: 1099 + test_failed: 0 + test_skipped: 0 + test_url_html: "https://github.com/owner/repo/runs/12345" + commit_sha: ${{ github.sha }} + run_id: ${{ github.run_id }} + repository: ${{ github.repository }} + server_url: ${{ github.server_url }} +``` + +## Gist Structure + +This action uses a **single Gist** with **multiple files** for different platforms: + +``` +Gist ID: 472c59b7c2a1898c48a29f3c88897c5a +β”œβ”€β”€ test-results-linux.json +β”œβ”€β”€ test-results-windows.json +└── test-results-macos.json +``` + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `platform` | Platform name (Linux, Windows, macOS) | βœ… | - | +| `gist_id` | GitHub Gist ID for storing test results | βœ… | - | +| `filename` | Filename for platform-specific JSON (e.g., test-results-linux.json) | βœ… | - | +| `gist_token` | GitHub token with gist permissions | βœ… | - | +| `test_passed` | Number of passed tests | βœ… | - | +| `test_failed` | Number of failed tests | βœ… | - | +| `test_skipped` | Number of skipped tests | βœ… | - | +| `test_url_html` | URL to test results page | ❌ | `''` | +| `commit_sha` | Git commit SHA | βœ… | - | +| `run_id` | GitHub Actions run ID | βœ… | - | +| `repository` | Repository in owner/repo format | βœ… | - | +| `server_url` | GitHub server URL | βœ… | - | +| `api_domain` | Badge API domain for URLs | ❌ | `your-api-domain` | + +## Outputs + +This action produces: + +- **Gist File Update**: Updates the platform-specific file in the single Gist +- **Console Output**: Displays badge URLs ready for README usage +- **Debug Info**: Shows HTTP status and error details + +## Generated JSON Format + +The action creates JSON data in this format for each platform file: + +```json +{ + "platform": "Linux", + "passed": 1099, + "failed": 0, + "skipped": 0, + "total": 1099, + "url_html": "https://github.com/owner/repo/runs/12345", + "timestamp": "2025-01-16T10:30:00Z", + "commit": "abc123def456", + "run_id": "12345678", + "workflow_run_url": "https://github.com/owner/repo/actions/runs/12345678" +} +``` + +## Error Handling + +- **Non-essential**: Uses `continue-on-error: true` to prevent workflow failures +- **Graceful degradation**: Provides detailed error messages without stopping execution +- **HTTP status reporting**: Shows API response codes for debugging +- **File-specific updates**: Only updates the specific platform file, doesn't affect other platform data + +## Integration with Badge API + +This action is designed to work with the LocalStack .NET Client Badge API that: + +- Reads from the updated Gist files +- Generates shields.io-compatible badge JSON +- Provides redirect endpoints to test result pages + +## Matrix Integration Example + +```yaml +env: + BADGE_GIST_ID: "472c59b7c2a1898c48a29f3c88897c5a" + +strategy: + matrix: + include: + - os: ubuntu-22.04 + name: "Linux" + filename: "test-results-linux.json" + - os: windows-latest + name: "Windows" + filename: "test-results-windows.json" + - os: macos-latest + name: "macOS" + filename: "test-results-macos.json" + +steps: + - name: "Update Test Results Badge" + uses: ./.github/actions/update-test-badge + with: + platform: ${{ matrix.name }} + gist_id: ${{ env.BADGE_GIST_ID }} + filename: ${{ matrix.filename }} + gist_token: ${{ secrets.GIST_SECRET }} + # ... other inputs +``` + +## Required Setup + +1. **Create single GitHub Gist** with platform-specific files: + - `test-results-linux.json` + - `test-results-windows.json` + - `test-results-macos.json` +2. **Generate GitHub PAT** with `gist` scope +3. **Add to repository secrets** as `GIST_SECRET` +4. **Deploy Badge API** to consume the Gist data + +## Badge URLs Generated + +The action displays ready-to-use markdown for README files: + +```markdown +[![Test Results (Linux)](https://your-api-domain/badge/tests/linux)](https://your-api-domain/redirect/tests/linux) +``` + +## Advantages of Explicit Filename Configuration + +- βœ… **No String Manipulation**: Eliminates brittle string transformation logic +- βœ… **Declarative**: Filenames are explicitly declared in workflow configuration +- βœ… **Predictable**: No risk of unexpected filename generation +- βœ… **Reusable**: Action works with any filename structure +- βœ… **Debuggable**: Easy to see exactly what files will be created +- βœ… **Flexible**: Supports any naming convention without code changes + +## Advantages of Single Gist Approach + +- βœ… **Simplified Management**: One Gist to manage instead of three +- βœ… **Atomic Operations**: All platform data in one place +- βœ… **Better Organization**: Clear file structure with descriptive names +- βœ… **Easier Debugging**: Single location to check all test data +- βœ… **Cost Efficient**: Fewer API calls and resources + +## Troubleshooting + +**Common Issues:** + +- **403 Forbidden**: Check `GIST_SECRET` permissions +- **404 Not Found**: Verify `gist_id` is correct +- **JSON Errors**: Ensure `jq` is available in runner +- **File Missing**: Gist files are created automatically on first update + +**Debug Steps:** + +1. Check action output for HTTP status codes +2. Verify Gist exists and is publicly accessible +3. Confirm token has proper `gist` scope +4. Check individual file URLs: `https://gist.githubusercontent.com/{gist_id}/raw/test-results-{platform}.json` diff --git a/.github/actions/update-test-badge/action.yml b/.github/actions/update-test-badge/action.yml new file mode 100644 index 0000000..38d7c1c --- /dev/null +++ b/.github/actions/update-test-badge/action.yml @@ -0,0 +1,125 @@ +name: 'Update Test Results Badge' +description: 'Updates test results badge data in GitHub Gist and displays badge URLs' +author: 'LocalStack .NET Team' + +inputs: + platform: + description: 'Platform name (Linux, Windows, macOS)' + required: true + gist_id: + description: 'GitHub Gist ID for storing test results' + required: true + filename: + description: 'Filename for the platform-specific JSON file (e.g., test-results-linux.json)' + required: true + gist_token: + description: 'GitHub token with gist permissions' + required: true + test_passed: + description: 'Number of passed tests' + required: true + test_failed: + description: 'Number of failed tests' + required: true + test_skipped: + description: 'Number of skipped tests' + required: true + test_url_html: + description: 'URL to test results page' + required: false + default: '' + commit_sha: + description: 'Git commit SHA' + required: true + run_id: + description: 'GitHub Actions run ID' + required: true + repository: + description: 'Repository in owner/repo format' + required: true + server_url: + description: 'GitHub server URL' + required: true + api_domain: + description: 'Badge API domain for displaying URLs' + required: false + default: 'your-api-domain' + +runs: + using: 'composite' + steps: + - name: 'Update Test Results Badge Data' + shell: bash + run: | + # Use explicit filename from input + FILENAME="${{ inputs.filename }}" + + # Calculate totals + TOTAL=$((${{ inputs.test_passed }} + ${{ inputs.test_failed }} + ${{ inputs.test_skipped }})) + + # Create JSON payload for badge API + cat > test-results.json << EOF + { + "platform": "${{ inputs.platform }}", + "passed": ${{ inputs.test_passed }}, + "failed": ${{ inputs.test_failed }}, + "skipped": ${{ inputs.test_skipped }}, + "total": ${TOTAL}, + "url_html": "${{ inputs.test_url_html }}", + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "commit": "${{ inputs.commit_sha }}", + "run_id": "${{ inputs.run_id }}", + "workflow_run_url": "${{ inputs.server_url }}/${{ inputs.repository }}/actions/runs/${{ inputs.run_id }}" + } + EOF + + echo "πŸ“Š Generated test results JSON for ${{ inputs.platform }}:" + cat test-results.json | jq '.' 2>/dev/null || cat test-results.json + + # Upload to single Gist with platform-specific filename + echo "πŸ“€ Uploading to Gist: ${{ inputs.gist_id }} (file: ${FILENAME})" + + # Create gist update payload - only update the specific platform file + cat > gist-payload.json << EOF + { + "files": { + "${FILENAME}": { + "content": $(cat test-results.json | jq -R -s '.') + } + } + } + EOF + + # Update Gist using GitHub API + HTTP_STATUS=$(curl -s -X PATCH \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ inputs.gist_token }}" \ + "https://api.github.com/gists/${{ inputs.gist_id }}" \ + -d @gist-payload.json \ + -w "%{http_code}" \ + -o response.json) + + if [ "$HTTP_STATUS" -eq 200 ]; then + echo "βœ… Successfully updated Gist file ${FILENAME} (HTTP $HTTP_STATUS)" + else + echo "⚠️ Failed to update Gist file ${FILENAME} (HTTP $HTTP_STATUS)" + echo "Response:" + cat response.json 2>/dev/null || echo "No response body" + fi + + - name: 'Display Badge URLs' + shell: bash + run: | + PLATFORM_LOWER=$(echo "${{ inputs.platform }}" | tr '[:upper:]' '[:lower:]') + FILENAME="${{ inputs.filename }}" + + echo "🎯 Badge URL for ${{ inputs.platform }}:" + echo "" + echo "**${{ inputs.platform }} Badge:**" + echo "[![Test Results (${{ inputs.platform }})](https://${{ inputs.api_domain }}/badge/tests/${PLATFORM_LOWER})](https://${{ inputs.api_domain }}/redirect/tests/${PLATFORM_LOWER})" + echo "" + echo "**Raw URLs:**" + echo "- Badge: https://${{ inputs.api_domain }}/badge/tests/${PLATFORM_LOWER}" + echo "- Redirect: https://${{ inputs.api_domain }}/redirect/tests/${PLATFORM_LOWER}" + echo "- Gist: https://gist.github.com/${{ inputs.gist_id }}" + echo "- Gist File: https://gist.githubusercontent.com/Blind-Striker/${{ inputs.gist_id }}/raw/${FILENAME}" \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..9cf7ea2 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,214 @@ +# LocalStack .NET Client - AI Agent Instructions + +Hey Copilot, welcome to the team! Before we start writing some brilliant code, let's get aligned on how we'll work together. Think of this as our "prime directive." + +## **1. Our Partnership Philosophy** + +* **Be my brainstorming partner:** Be talkative, conversational, and don't be afraid to use some quick and clever humor. We're in this together, so let's make it fun. +* **Innovate, but be practical:** I love creative, outside-the-box thinking. But at the end of the day, our code needs to be robust, maintainable, and solve the problem at hand. Practicality is king. +* **Challenge me:** I'm not looking for a yes-person. If you see a flaw in my logic, a potential edge case I've missed, or a more elegant solution, please speak up! I expect you to provide constructive criticism and explain the "why" behind your suggestions. A healthy debate leads to better code. + +## **2. The "Plan-Then-Execute" Rule** + +This is the most important rule: **Do not write a full implementation without my approval.** + +* **Step 1: Propose a plan.** Before generating any significant block of code, first outline your approach. This could be pseudo-code, a list of steps, or a high-level description of the classes and methods you'll create. +* **Step 2: Wait for the green light.** I will review your plan and give you the go-ahead. This ensures we're on the same page before you invest time generating the full implementation. + +## **3. Technical Ground Rules** + +* **Centralized NuGet Management:** This solution uses centralized package management. When a new package is needed, you should: + 1. Add a PackageReference to the Directory.Packages.props file, specifying both the Include and Version attributes. + 2. Add a corresponding PackageReference with only the Include attribute to the relevant .csproj file. +* **Testing Our Code:** Our testing stack is **xUnit**, **Moq**, and **Testcontainers**. Please generate tests following the patterns and best practices for these frameworks. Use Fact and Theory attributes from xUnit, set up fakes with Mock, and help configure services for integration tests using Testcontainers. +* **Roslyn Analyzers Are King (Usually):** We adhere to our configured analyzer rules. However, if we're quickly testing an idea or prototyping, you can safely use #pragma warning disable to ignore a specific rule. Just be sure to add a comment like // TODO: Re-address this analyzer warning so we can clean it up later. +* **Modern C#:** Let's default to modern C# conventions: file-scoped namespaces, record types for DTOs, top-level statements, expression-bodied members, and async/await best practices. + +## Project Overview + +**LocalStack .NET Client** is a sophisticated .NET library that wraps the AWS SDK to work seamlessly with LocalStack (local AWS emulation). The project is undergoing a major architectural evolution with **Native AOT support** via source generators and **AWS SDK v4 migration**. + +> πŸ“– **Deep Dive**: For comprehensive project details, see [`artifacts/Project_Onboarding.md`](../artifacts/Project_Onboarding.md) - a detailed guide covering architecture, testing strategy, CI/CD pipeline, and contribution guidelines. + +### What I Learned from the Onboarding Document + +**Key Insights for AI Agents:** + +1. **Testing is Sophisticated**: The project uses a 4-tier testing pyramid (Unit β†’ Integration β†’ Functional β†’ Sandbox). Functional tests use **TestContainers** with dynamic port mapping across multiple LocalStack versions (v3.7.1, v4.3.0). + +2. **Version-Aware Development**: The project carefully manages AWS SDK compatibility. Currently migrated to **AWS SDK v4** with specific considerations: + - .NET Framework requirement bumped from 4.6.2 β†’ 4.7.2 + - Extensions package uses new `ClientFactory` pattern vs old non-generic `ClientFactory` + - Some functional tests may fail due to behavioral changes in SNS/DynamoDB operations + +3. **Enterprise Build System**: Uses **Cake Frosting** (not traditional Cake scripts) with cross-platform CI/CD across Ubuntu/Windows/macOS. The build system handles complex scenarios like .NET Framework testing on Mono. + +4. **Service Coverage**: Supports **50+ AWS services** through intelligent endpoint resolution. Services are mapped through `AwsServiceEndpointMetadata.All` with both legacy per-service ports and modern unified edge port (4566). + +5. **Reflection Strategy**: The codebase heavily uses reflection to access AWS SDK internals (private `serviceMetadata` fields, dynamic `ClientConfig` creation). This is being modernized with UnsafeAccessor for AOT. + +### Core Architecture + +The library follows a **Session-based architecture** with three main components: + +1. **Session (`ISession`)**: Core client factory that configures AWS clients for LocalStack endpoints +2. **Config (`IConfig`)**: Service endpoint resolution and LocalStack connection management +3. **SessionReflection (`ISessionReflection`)**: Abstraction layer for AWS SDK private member access + +### Key Innovation: Dual Reflection Strategy + +The project uses a sophisticated **conditional compilation pattern** for .NET compatibility: + +- **.NET 8+**: Uses **UnsafeAccessor** pattern via Roslyn source generators (`LocalStack.Client.Generators`) for Native AOT +- **Legacy (.NET Standard 2.0, .NET Framework)**: Falls back to traditional reflection APIs + +## Development Workflows + +### Build System +- **Build Scripts**: Use `build.ps1` (Windows) or `build.sh` (Linux) - these delegate to Cake build tasks +- **Build Framework**: Uses **Cake Frosting** in `build/LocalStack.Build/` - examine `CakeTasks/` folder for available targets +- **Common Commands**: + - `build.ps1` - Full build and test + - `build.ps1 --target=tests` - Run tests only + +### Project Structure + +``` +src/ +β”œβ”€β”€ LocalStack.Client/ # Core library (multi-target: netstandard2.0, net472, net8.0, net9.0) +β”œβ”€β”€ LocalStack.Client.Extensions/ # DI integration (AddLocalStack() extension) +└── LocalStack.Client.Generators/ # Source generator for AOT (netstandard2.0, Roslyn) + +tests/ +β”œβ”€β”€ LocalStack.Client.Tests/ # Unit tests (mocked) +β”œβ”€β”€ LocalStack.Client.Integration.Tests/ # Real LocalStack integration +β”œβ”€β”€ LocalStack.Client.AotCompatibility.Tests/ # Native AOT testing +└── sandboxes/ # Example console apps +``` + +## Critical Patterns & Conventions + +### 1. Multi-Framework Configuration Pattern + +For .NET 8+ conditional features, use this pattern consistently: + +```csharp +#if NET8_0_OR_GREATER + // Modern implementation (UnsafeAccessor) + var accessor = AwsAccessorRegistry.GetByInterface(); + return accessor.CreateClient(credentials, clientConfig); +#else + // Legacy implementation (reflection) + return CreateClientByInterface(typeof(TClient), useServiceUrl); +#endif +``` + +### 2. Session Client Creation Pattern + +The Session class provides multiple client creation methods - understand the distinction: + +- `CreateClientByInterface()` - Interface-based (preferred) +- `CreateClientByImplementation()` - Concrete type +- `useServiceUrl` parameter controls endpoint vs region configuration + +### 3. Source Generator Integration + +When working on AOT features: +- Generator runs only for .NET 8+ projects (`Net8OrAbove` condition) +- Discovers AWS clients from referenced assemblies at compile-time +- Generates `IAwsAccessor` implementations in `LocalStack.Client.Generated` namespace +- Auto-registers via `ModuleInitializer` in `AwsAccessorRegistry` + +### 4. Configuration Hierarchy + +LocalStack configuration follows this pattern: +```json +{ + "LocalStack": { + "UseLocalStack": true, + "Session": { + "RegionName": "eu-central-1", + "AwsAccessKeyId": "accessKey", + "AwsAccessKey": "secretKey" + }, + "Config": { + "LocalStackHost": "localhost.localstack.cloud", + "EdgePort": 4566, + "UseSsl": false + } + } +} +``` + +## Testing Patterns + +### Multi-Layered Testing Strategy +Based on the comprehensive testing guide in the onboarding document: + +- **Unit Tests**: `MockSession.Create()` β†’ Setup mocks β†’ Verify calls pattern +- **Integration Tests**: Client creation across **50+ AWS services** without external dependencies +- **Functional Tests**: **TestContainers** with multiple LocalStack versions (v3.7.1, v4.3.0) +- **Sandbox Apps**: Real-world examples in `tests/sandboxes/` demonstrating usage patterns + +### TestContainers Pattern +```csharp +// Dynamic port mapping prevents conflicts +ushort mappedPublicPort = localStackFixture.LocalStackContainer.GetMappedPublicPort(4566); +``` + +### Known Testing Challenges +- **SNS Issues**: LocalStack v3.7.2/v3.8.0 have known SNS bugs (use v3.7.1 or v3.9.0+) +- **SQS Compatibility**: AWSSDK.SQS 3.7.300+ has issues with LocalStack v1/v2 +- **AWS SDK v4 Migration**: Some functional tests may fail due to behavioral changes + +## Package Management + +**Centralized Package Management** - Always follow this two-step process: + +1. Add to `Directory.Packages.props`: +```xml + +``` + +2. Reference in project: +```xml + +``` + +### Code Quality Standards +The onboarding document emphasizes **enterprise-level quality**: +- **10+ Analyzers Active**: Roslynator, SonarAnalyzer, Meziantou.Analyzer, SecurityCodeScan +- **Warnings as Errors**: `TreatWarningsAsErrors=true` across solution +- **Nullable Reference Types**: Enabled solution-wide for safety +- **Modern C# 13**: Latest language features with strict mode enabled + +## Working with AWS SDK Integration + +### Service Discovery Pattern +The library auto-discovers AWS services using naming conventions: +- `IAmazonS3` β†’ `AmazonS3Client` β†’ `AmazonS3Config` +- Service metadata extracted from private static `serviceMetadata` fields +- Endpoint mapping in `AwsServiceEndpointMetadata.All` + +### Reflection Abstraction +When adding AWS SDK integration features: +- Always implement in both `SessionReflectionLegacy` and `SessionReflectionModern` +- Modern version uses generated `IAwsAccessor` implementations +- Legacy version uses traditional reflection with error handling + +## Key Files to Understand + +- `src/LocalStack.Client/Session.cs` - Core client factory logic +- `src/LocalStack.Client/Utils/SessionReflection.cs` - Facade that chooses implementation +- `src/LocalStack.Client.Generators/AwsAccessorGenerator.cs` - Source generator main logic +- `tests/sandboxes/` - Working examples of all usage patterns +- `Directory.Build.props` - Shared MSBuild configuration with analyzer rules + +## Plan-Then-Execute Workflow + +1. **Propose architectural approach** - especially for cross-framework features +2. **Consider AOT implications** - will this work with UnsafeAccessor pattern? +3. **Plan test strategy** - unit, integration, and AOT compatibility +4. **Wait for approval** before implementing significant changes + +The codebase prioritizes **backwards compatibility** and **AOT-first design** - keep these principles central to any contributions. \ No newline at end of file diff --git a/.github/nuget.config b/.github/nuget.config new file mode 100644 index 0000000..af21e40 --- /dev/null +++ b/.github/nuget.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml deleted file mode 100644 index ae6235a..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 6 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "6.0.x" - - - name: Install .NET 7 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "7.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() diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml deleted file mode 100644 index f2293ce..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-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 6 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "6.0.x" - - - name: Install .NET 7 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "7.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() diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml deleted file mode 100644 index c0f654d..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 6 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "6.0.x" - - - name: Install .NET 7 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "7.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() diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..c9b728a --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,229 @@ +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) }} + BADGE_GIST_ID: "472c59b7c2a1898c48a29f3c88897c5a" + + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + name: "Windows" + script: "./build.ps1" + filename: "test-results-windows.json" + + - os: ubuntu-22.04 + name: "Linux" + script: "./build.sh" + filename: "test-results-linux.json" + + - os: macos-latest + name: "macOS" + script: "./build.sh" + filename: "test-results-macos.json" + + 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" + id: 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: "Update Test Results Badge" + if: always() # Run even if tests failed or were skipped + continue-on-error: true # Don't fail workflow if badge update fails + uses: ./.github/actions/update-test-badge + with: + platform: ${{ matrix.name }} + gist_id: ${{ env.BADGE_GIST_ID }} + filename: ${{ matrix.filename }} + gist_token: ${{ secrets.GIST_SECRET }} + test_passed: ${{ steps.test-results.outputs.passed || 0 }} + test_failed: ${{ steps.test-results.outputs.failed || 0 }} + test_skipped: ${{ steps.test-results.outputs.skipped || 0 }} + test_url_html: ${{ steps.test-results.outputs.url_html || '' }} + commit_sha: ${{ github.sha }} + run_id: ${{ github.run_id }} + repository: ${{ github.repository }} + server_url: ${{ github.server_url }} + + - name: "Upload Test Artifacts" + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results-${{ matrix.name }} + path: | + **/*.trx + **/TestResults/**/* + retention-days: 7 + + continuous-deployment: + name: "Continuous Deployment" + runs-on: ubuntu-22.04 + needs: build-and-test + if: | + github.repository == 'localstack-dotnet/localstack-dotnet-client' && + ((github.event_name == 'push' && github.ref == 'refs/heads/master') || + (github.event_name == 'pull_request' && startsWith(github.head_ref, 'feature/'))) + 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: "Setup GitHub Packages Configuration" + run: | + echo "πŸ” Adding GitHub Packages authentication..." + 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 + + echo "πŸ”§ Original nuget.config..." + cat nuget.config + + echo "πŸ“ Backing up original nuget.config..." + cp nuget.config nuget.config.backup + + echo "πŸ”§ Using GitHub-optimized nuget.config..." + cp .github/nuget.config nuget.config + + echo "πŸ”§ Replaced nuget.config..." + cat nuget.config + + - name: "Pack & Publish LocalStack.Client" + id: pack-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.event_name == 'pull_request' && github.head_ref || github.ref_name }} \ + --package-secret ${{ secrets.GITHUB_TOKEN }} + + - name: "Prepare Extensions Project" + run: | + echo "πŸ”§ Preparing Extensions project to use LocalStack.Client package..." + ./build.sh --target nuget-prepare-extensions \ + --package-source github \ + --package-id LocalStack.Client.Extensions \ + --client-version ${{ steps.pack-client.outputs.client-version }} + + - 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.event_name == 'pull_request' && github.head_ref || github.ref_name }} \ + --package-secret ${{ secrets.GITHUB_TOKEN }} + + - name: "Upload Package Artifacts" + uses: actions/upload-artifact@v4 + with: + name: "packages-${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}-${{ 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.event_name == 'pull_request' && github.head_ref || github.ref_name }} + + - name: "Cleanup Configuration" + if: always() + run: | + echo "🧹 Restoring original nuget.config..." + mv nuget.config.backup nuget.config || echo "⚠️ Original config not found, skipping restore" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..f51a099 --- /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-22.04 + 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-nuget.yml b/.github/workflows/publish-nuget.yml index 15ab266..11a75aa 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: @@ -6,14 +6,17 @@ on: package-version: description: "Package Version" required: true + localstack-client-version: + description: "LocalStack Client Version" + required: true package-source: type: choice description: Package Source required: true - default: "myget" + default: "github" options: - - myget - nuget + - github package-id: type: choice description: Package Id @@ -23,48 +26,108 @@ 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: "Checkout" + uses: actions/checkout@v4 - - name: Install NuGet - uses: NuGet/setup-nuget@v1.0.5 - - - name: Install .NET 6 - uses: actions/setup-dotnet@v1 + - name: "Setup .NET SDK" + uses: actions/setup-dotnet@v4 with: - dotnet-version: "6.0.x" + dotnet-version: | + 8.0.x + 9.0.x - - name: Install .NET 7 - uses: actions/setup-dotnet@v1 + - name: "Cache NuGet packages" + uses: actions/cache@v4 with: - dotnet-version: "7.0.x" + 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 + - name: "Build & Test" + run: ./build.sh --target tests --skipFunctionalTest true - - name: "Print Version" + - name: "Print Package Information" run: | - echo "Package Version: ${{ github.event.inputs.package-version }}" + 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: Remove Project Ref & Add latest pack + - name: "Setup GitHub Packages Configuration" + if: ${{ github.event.inputs.package-source == 'github' }} + run: | + echo "πŸ” Adding GitHub Packages authentication..." + 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 + + echo "πŸ“ Backing up original nuget.config..." + cp nuget.config nuget.config.backup + + echo "πŸ”§ Using GitHub-optimized nuget.config..." + cp .github/nuget.config nuget.config + + - name: "Prepare Extensions Project" 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: | + ./build.sh --target nuget-prepare-extensions \ + --package-source ${{ github.event.inputs.package-source }} \ + --package-id ${{ github.event.inputs.package-id }} \ + --client-version ${{ github.event.inputs.localstack-client-version }} - - 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}} + 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 edd58d8..92632c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,153 +1,77 @@ -# LocalStack .NET Client Change Log +# LocalStack .NET Client v2.x Change Log -### [v1.4.1](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.4.1) +This document outlines the changes, updates, and important notes for the LocalStack .NET Client v2.x series, including the latest preview release. -#### 1. New Features +See v1.x change log for previous versions: [CHANGELOG.md](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/sdkv3-lts/CHANGELOG.md) -- **Update Packages and Multi LocalStack Support:** -- New endpoints added from the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v2.3: - - EMRServerless - - Appflow - - Keyspaces - - Scheduler +## [v2.0.0](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v2.0.0) -#### 2. Bug Fixes and Investigations +> **Heads‑up**: Nativeβ€―AOT is not yet supported in GA. +> Follow [draftΒ PRβ€―#49](https://github.com/localstack-dotnet/localstack-dotnet-client/pull/49) for the reflection‑free path planned forβ€―v2.1. -- **Investigation and Fixes:** - - Started investigating issues #23 and #24. - - Bugs have been fixed with [this PR](https://github.com/localstack/localstack/pull/8962) by LocalStack. - - Fixed legacy LocalStack container wait strategy for functional tests. +### ✨ New features (since `v2.0.0‑preview1`) -#### 3. General +- **Added Endpoints from [Localstack Python Client](https://github.com/localstack/localstack-python-client) v2.9:** + - **Account Management** + - **Certificate Manager Private Certificate Authority (ACMPCA)** + - **Bedrock** + - **Cloud Control API** + - **Code Build** + - **Code Connections** + - **Code Deploy** + - **Code Pipeline** + - **Elastic Transcoder** + - **MemoryDB for Redis** + - **Shield** + - **Verified Permissions** -- **New Solution Standards:** - - Introduced new solution-wide coding standards with various analyzers. -- **Code Refactoring According to New Standards:** - - Libraries, sandbox projects, build projects, and test projects have been refactored to adhere to the new coding standards. - - Moved remaining using directives to GlobalUsings.cs files. -- **Centralized Package Management:** - - Managed package versions centrally to resolve issue #28. -- **Package Updates:** - - Updated analyzer packages. - - Updated test packages. - - AWSSDK.Core set to 3.7.201 as the minimum version. -- Tested against LocalStack v1.3.1, v2.0, and the latest containers. +### πŸ› οΈΒ General -#### 4. Warnings +- **Testing Compatibility:** + - Successfully tested against LocalStack versions: + - **v3.7.1** + - **v4.6.0** -- **Legacy LocalStack Versions:** - - This version will be the last to support Legacy LocalStack versions. -- **.NET 4.6.1 Support:** - - .NET 4.6.1 support will be removed in the next release and replaced with .NET 4.6.2. -- **Breaking Changes Ahead:** - - Users should anticipate some breaking changes in the next release due to the removal of Legacy support and changes in configuration. +*See [`v2.0.0-preview1`](#v200-preview1) for the complete migration from v1.x and the AWSΒ SDKΒ v4 overhaul.* -### [v1.4.0](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.4.0) +--- -#### 1. New Features +## [v2.0.0-preview1](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v2.0.0-preview1) -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v1.39 have been added. - - Fault Injection Service (FIS) - - Marketplace Metering - - Amazon Transcribe - - Amazon MQ +### 1. Breaking Changes -#### 2. General +- **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). -- .NET 7 support added -- .NET 5 ve .NET Core 3.1 runtimes removed from Nuget pack (.netstandard2.0 remains) -- Tested against LocalStack v1.3.1 container. -- AWSSDK.Core set to 3.7.103 as the minimum version. - - **Warning** In this version, the ServiceURL property of Amazon.Runtime.ClientConfig adds a trailing `/` to every URL set. - For example, if `http://localhost:1234` is set as the value, it will become `http://localhost:1234/` -- Following depedencies updated from v3.0.0 to v3.1.32 in LocalStack.Client.Extensions for security reasons - - Microsoft.Extensions.Configuration.Abstractions - - Microsoft.Extensions.Configuration.Binder - - Microsoft.Extensions.DependencyInjection.Abstractions - - Microsoft.Extensions.Logging.Abstractions - - Microsoft.Extensions.Options.ConfigurationExtensions +### 2. General -#### 3. Bug Fixes +- **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. -- Write a timestream record using .Net AWSSDK NuGet packages ([#20](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/20)) -- Session does not honor UseSsl and always sets UseHttp to true ([#16](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/16)) +- **Framework Support:** + - **.NET 9** + - **.NET 8** + - **.NET Standard 2.0** + - **.NET Framework 4.7.2** -### [v1.3.1](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.3.1) +- **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** -#### 1. New Features +### 3. Important Notes -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v1.35 have been added. - - Route53Resolver - - KinesisAnalyticsV2 - - OpenSearch - - Amazon Managed Workflows for Apache Airflow (MWAA) +- **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). -#### 2. General - -- Tested against LocalStack v0.14.2 container. -- AWSSDK.Core set to 3.7.9 as the minimum version. -- AWSSDK.Extensions.NETCore.Setup set to 3.7.2 as the minimum version. - -### [v1.3.0](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.3.0) - -#### 1. New Features - -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v1.27 have been added. - - SESv2 - - EventBridge ([#14](https://github.com/localstack-dotnet/localstack-dotnet-client/pull/14)) -- Tested against LocalStack v0.13.0 container. - -#### 2. Enhancements - -- `useServiceUrl` parameter added to change client connection behavior. See [useServiceUrl Parameter](#useserviceurl) -- Readme and SourceLink added to Nuget packages - -#### 3. Bug Fixes - -- Session::RegionName configuration does not honor while creating AWS client ([#15](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/15)) - -Thanks to [petertownsend](https://github.com/petertownsend) for his contribution - -### [v1.2.3](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.2.3) - -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v1.25 have been added. - - Config Service -- .NET 6.0 support added. -- AWSSDK.Core set to 3.7.3.15 as the minimum version. -- AWSSDK.Extensions.NETCore.Setup set to 3.7.1 as the minimum version. -- Tested against LocalStack v0.13.0 container. - -### [v1.2.2](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.2.2) - -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v1.22 have been added. - - EFS, Backup, LakeFormation, WAF, WAF V2 and QLDB Session -- AWSSDK.Core set to 3.7.1 as the minimum version. -- Tested against LocalStack v0.12.16 container. - -### [v1.2](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.2.0) - -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v1.20 have been added. - - IoTAnalytics, IoT Events, IoT Events Data, IoT Wireless, IoT Data Plane, IoT Jobs Data Plane, Support, Neptune, DocDB, ServiceDiscovery, ServerlessApplicationRepository, AppConfig, Cost Explorer, MediaConvert, Resource Groups Tagging API, Resource Groups -- AWSSDK.Core set to 3.7.0 as the minimum version. -- Obsolete methods removed. -- New alternate AddAWSServiceLocalStack method added to prevent mix up with AddAWSService (for LocalStack.Client.Extension v1.1.0). -- Tested against LocalStack v0.12.10 container. - -### [v1.1](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.1.0) - -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v1.10 have been added. - - Transfer, ACM, CodeCommit, Kinesis Analytics, Amplify, Application Auto Scaling, Kafka, Timestream Query, Timestream Write, Timestream Write, S3 Control, Elastic Load Balancing v2, Redshift Data -- .NET 5.0 support added. -- AWSSDK.Core set to 3.5.0 as the minimum version. -- Tested against LocalStack v0.12.07 container. - -### [v1.0](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v1.0.0) - -- New endpoints in the official [Localstack Python Client](https://github.com/localstack/localstack-python-client) v0.23 have been added. - - ElastiCache, Kms, Emr, Ecs, Eks, XRay, ElasticBeanstalk, AppSync, CloudFront, Athena, Glue, Api Gateway V2, RdsData, SageMaker, SageMakerRuntime, Ecr, Qldb -- .netcore2.2 support removed since Microsoft depracated it. .netcore3.1 support added. -- AWSSDK.Core set to 3.3.106.5 as the minimum version. - -### [v0.8.0.163](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v0.8.0.163) - -- First release. +--- diff --git a/Directory.Build.props b/Directory.Build.props index c3d3b9b..2c1d5d2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,11 +5,11 @@ LocalStack.NET https://github.com/localstack-dotnet/localstack-dotnet-client localstack-dotnet-square.png - 1.4.1 - 1.2.1 + 2.0.0 + 2.0.0 true snupkg - 11.0 + 13.0 true latest true diff --git a/Directory.Packages.props b/Directory.Packages.props index d0019b8..2ebfc00 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,150 +1,177 @@ - - - - - - - - + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - - - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + - - + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + - - + + \ No newline at end of file diff --git a/LICENSE b/LICENSE index 79dfe2e..f9478d1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 LocalStack.NET +Copyright (c) 2019-2025 LocalStack.NET Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/LocalStack.sln.DotSettings b/LocalStack.sln.DotSettings index 372394e..974f95b 100644 --- a/LocalStack.sln.DotSettings +++ b/LocalStack.sln.DotSettings @@ -253,6 +253,7 @@ True True True + True True True True diff --git a/LocalStack.slnx b/LocalStack.slnx new file mode 100644 index 0000000..a70d15c --- /dev/null +++ b/LocalStack.slnx @@ -0,0 +1,30 @@ +ο»Ώ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index da002e3..8343b2b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,107 @@ -# 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)](https://www.nuget.org/packages/LocalStack.Client/) [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client%26source%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client%26source%3Dnuget%26track%3D1%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml) [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql) [![Linux Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/linux?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/linux) + +> **πŸš€ Quick Start**: `dotnet add package LocalStack.Client --version 2.0.0` (AWS SDK v4) | [Installation Guide](#-installation) | [GA Timeline](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45) + +--- + +## πŸŽ‰ What's New: AWS SDK v4 Support Available + +**v2.0.0** is live with complete AWS SDK v4 support - easier migration than expected! + +- βœ… **1,168 tests passing** across all frameworks +- βœ… **Minimal breaking changes** (just .NET Framework 4.6.2 β†’ 4.7.2 and AWS SDK v3 β†’ v4) +- βœ… **Public APIs unchanged** - your code should work as-is! +- πŸ“– **[Read Full Roadmap](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45)** + +**Version Strategy**: + +- v2.x (AWS SDK v4) active development on [master branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master) +- v1.x (AWS SDK v3) Available on [sdkv3-lts branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/sdkv3-lts), maintenance until July 2026 ![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 | -| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 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) | +## πŸš€ Platform Compatibility & Quality Status -## Continuous Integration +### Supported Platforms -| 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) | +- [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) | [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) +- [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) +- [.NET Framework 4.7.2 and Above](https://dotnet.microsoft.com/download/dotnet-framework) + +## ⚑ Nativeβ€―AOT & Trimming Status + +> **Heads‑up for `dotnet publish -p:PublishAot=true` / `PublishTrimmed=true` users** + +- **v2.0.0 GA ships without Nativeβ€―AOT support.** + The current build still relies on reflection for some AWS SDK internals. + - Public entry points that touch reflection are tagged with + `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`. + - You’ll see IL3050 / IL2026 warnings at compile time (promoted to errors in a strict AOT publish). +- We already ship a linker descriptor with **`LocalStack.Client.Extensions`** + to keep the private `ClientFactory` machinery alive. **Nothing to configure for that part.** +- Until the reflection‑free, source‑generated path lands (work in progress in + [draftΒ PRβ€―#49](https://github.com/localstack-dotnet/localstack-dotnet-client/pull/49) and tracked on + [roadmapΒ #48](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/48)): + + 1. **Suppress** the warnings in your app *or* call only the APIs that don’t rely on reflection. + 2. Add a tiny **service‑specific** linker descriptor for every `AWSSDK.*` package you reference + (S3, DynamoDB, etc.). For example, for S3: + + ```xml + + + + + + + + + + + + + + + + ``` + + 3. Wire it into your project once: + + ```xml + + + + ``` + + 4. If you hit a runtime β€œmissing member” error, ensure you’re on AWSΒ SDKΒ v4 **β‰₯β€―4.1.\*** and that + the concrete `AWSSDK.*` package you’re using is included in the descriptor above. + +> **Planned** – v2.1 will introduce an AOT‑friendly factory that avoids reflection entirely; once you +> migrate to that API these warnings and extra XML files go away. + +### Build & Test Matrix + +| Category | Platform/Type | Status | Description | +|----------|---------------|--------|-------------| +| **πŸ”§ Build** | Cross-Platform | [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml) | Matrix testing: Windows, Linux, macOS | +| **πŸ”’ Security** | Static Analysis | [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql) | CodeQL analysis & dependency review | +| **πŸ§ͺ Tests** | Linux | [![Linux Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/linux?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/linux) | All framework targets | +| **πŸ§ͺ Tests** | Windows | [![Windows Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/windows?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/windows) | All framework targets | +| **πŸ§ͺ Tests** | macOS | [![macOS Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/macos?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/macos) | All framework targets | + +## Package Status + +| Package | NuGet.org | GitHub Packages (Nightly) | +|---------|-----------|---------------------------| +| **LocalStack.Client v1.x** | [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dnuget%26track%3D1%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) | [![Github v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dgithub%26track%3D1%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client) | +| **LocalStack.Client v2.x** | [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) | [![Github v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dgithub%26track%3D2%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client) | +| **LocalStack.Client.Extensions v1.x** | [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dnuget%26track%3D1%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![Github v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dgithub%26track%3D1%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client.Extensions) | +| **LocalStack.Client.Extensions v2.x** | [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![GitHub Packages v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client.extensions%26source%3Dgithub%26track%3D2%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client.Extensions) | ## Table of Contents @@ -33,20 +119,13 @@ Localstack.NET is an easy-to-use .NET client for [LocalStack](https://github.com 7. [Changelog](#changelog) 8. [License](#license) -## Supported Platforms - -- [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0) -- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) -- [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) -- [.NET 4.6.1 and Above](https://dotnet.microsoft.com/download/dotnet-framework) - ## Why LocalStack.NET Client? - **Consistent Client Configuration:** LocalStack.NET eliminates the need for manual endpoint configuration, providing a standardized and familiar approach to initializing clients. - **Adaptable Environment Transition:** Easily switch between LocalStack and actual AWS services with minimal configuration changes. -- **Versatile .NET Compatibility:** Supports a broad spectrum of .NET versions, from .NET 7.0 and .NET Standard 2.0 to .NET Framework 4.6.1 and above. +- **Versatile .NET Compatibility:** Supports a broad spectrum of .NET versions, from .NET 9.0 and .NET Standard 2.0 to .NET Framework 4.6.2 and above. - **Reduced Learning Curve:** Offers a familiar interface tailored for LocalStack, ideal for developers acquainted with the AWS SDK for .NET. @@ -60,13 +139,36 @@ 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 + +To install the latest version of LocalStack.NET with AWS SDK v4 support, use the following command: ```bash -dotnet add package LocalStack.Client -dotnet add package LocalStack.Client.Extensions +# Install v2.0.0 with AWS SDK v4 support +dotnet add package LocalStack.Client --version 2.0.0 +dotnet add package LocalStack.Client.Extensions --version 2.0.0 ``` +#### 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). @@ -129,7 +231,9 @@ The `RegionName` is important as LocalStack creates resources based on the speci ## Known Issues -- **LocalStack Versions v2.0.1 - v2.2:** In versions v2.0.1 through v2.2 of LocalStack, the URL routing logic was changed, causing issues with SQS and S3 operations. Two issues were opened in LocalStack regarding this: [issue #8928](https://github.com/localstack/localstack/issues/8928) and [issue #8924](https://github.com/localstack/localstack/issues/8924). LocalStack addressed this problem with [PR #8962](https://github.com/localstack/localstack/pull/8962). Therefore, when using LocalStack.NET, either use version v2.0 of LocalStack (there are no issues with the v1 series as well) or the upcoming v2.3 version, or use the latest container from Docker Hub. +- **SNS with LocalStack v3.7.2 and v3.8.0:** During development on the new version, it was discovered that SNS functional tests are not working in LocalStack versions v3.7.2 and v3.8.0. This issue was reported in LocalStack [issue #11652](https://github.com/localstack/localstack/issues/11652). The LocalStack team identified a bug related to handling SNS URIs and resolved it in [PR #11653](https://github.com/localstack/localstack/pull/11653). The fix will be included in an upcoming release of LocalStack. In the meantime, if you're using SNS, it is recommended to stick to version v3.7.1 of LocalStack until the fix is available. + +- **LocalStack Versions v2.0.1 - v2.2:** In versions v2.0.1 through v2.2 of LocalStack, the URL routing logic was changed, causing issues with SQS and S3 operations. Two issues were opened in LocalStack regarding this: [issue #8928](https://github.com/localstack/localstack/issues/8928) and [issue #8924](https://github.com/localstack/localstack/issues/8924). LocalStack addressed this problem with [PR #8962](https://github.com/localstack/localstack/pull/8962). Therefore, when using LocalStack.NET, either use version v2.0 of LocalStack (there are no issues with the v1 series as well) or the upcoming v2.3 version, or use the latest v3 series container from Docker Hub. - **AWS_SERVICE_URL Environment Variable:** Unexpected behaviors might occur in LocalStack.NET when the `AWS_SERVICE_URL` environment variable is set. This environment variable is typically set by LocalStack in the container when using AWS Lambda, and AWS also uses this environment variable in the live environment. Soon, just like in LocalStack's official Python library, this environment variable will be prioritized by LocalStack.NET when configuring the LocalStack host, and there will be a general simplification in the configuration. You can follow this in the issues [issue #27](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/27) and [issue #32](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/32). You set the `AWS_SERVICE_URL` to empty string until this issue is resolved. @@ -139,6 +243,8 @@ Environment.SetEnvironmentVariable("AWS_SERVICE_URL", string.Empty); - **IAmazonLambda Operations:** There's a general issue with `IAmazonLambda` operations. This matter is currently under investigation. +- **AWSSDK.SQS Compatibility:** Starting from version `3.7.300.*` of `AWSSDK.SQS`, there are compatibility issues with LocalStack v1 and v2 series versions. The [v3](https://hub.docker.com/r/localstack/localstack/tags?page=&page_size=&ordering=&name=3.4) series of LocalStack does not have these issues. Therefore, it is recommended to either update your LocalStack container to the v3 series or downgrade your `AWSSDK.SQS` to version `3.7.200.*` if you are using LocalStack v1 or v2 series containers. It is important to note that this is not a problem related to LocalStack.NET, but rather an issue with the LocalStack container and the AWS SDK for .NET. + ## Developing We appreciate contributions in the form of feedback, bug reports, and pull requests. @@ -149,13 +255,13 @@ To build the project, use the following commands based on your operating system: Windows -``` +```powershell build.ps1 ``` Linux -``` +```bash ./build.sh ``` @@ -169,16 +275,22 @@ To execute the tests, use the commands below: Windows -``` +```powershell build.ps1 --target=tests ``` Linux -``` +```bash ./build.sh --target=tests ``` +## Community + +Got questions or wild feature ideas? + +πŸ‘‰ Join the conversation in [GitHubβ€―Discussions](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions). + ## Changelog Please refer to [`CHANGELOG.md`](CHANGELOG.md) to see the complete list of changes for each release. diff --git a/build/LocalStack.Build/BuildContext.cs b/build/LocalStack.Build/BuildContext.cs index d15e5d8..e41badd 100644 --- a/build/LocalStack.Build/BuildContext.cs +++ b/build/LocalStack.Build/BuildContext.cs @@ -1,24 +1,42 @@ -ο»Ώnamespace LocalStack.Build; +ο»Ώ#pragma warning disable CA1515 // Consider making public types internal + +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"; + + // Cached package versions to ensure consistency across pack/publish operations + private string? _clientPackageVersion; + private string? _extensionsPackageVersion; + 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"); + ClientVersion = context.Argument("client-version", default(string)); 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("../../"); @@ -26,18 +44,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(); } @@ -51,12 +69,18 @@ public BuildContext(ICakeContext context) : base(context) public string PackageVersion { get; } + public string ClientVersion { get; } + public string PackageId { get; } public string PackageSecret { get; } public string PackageSource { get; } + public bool UseDirectoryPropsVersion { get; } + + public string BranchName { get; } + public ImmutableDictionary PackageSourceMap { get; } public ImmutableDictionary PackageIdProjMap { get; } @@ -81,37 +105,58 @@ public BuildContext(ICakeContext context) : base(context) public ConvertableFilePath LocalStackClientExtProjFile { get; } - public static void ValidateArgument(string argumentName, string argument) + /// + /// Gets the effective package version for LocalStack.Client package. + /// This value is cached to ensure consistency across pack and publish operations. + /// + public string GetClientPackageVersion() { - if (string.IsNullOrWhiteSpace(argument)) - { - throw new Exception($"{argumentName} can not be null or empty"); - } + return _clientPackageVersion ??= UseDirectoryPropsVersion + ? GetDynamicVersionFromProps("PackageMainVersion") + : PackageVersion; } - public void InstallXUnitNugetPackage() + /// + /// Gets the effective package version for LocalStack.Client.Extensions package. + /// This value is cached to ensure consistency across pack and publish operations. + /// + public string GetExtensionsPackageVersion() { - if (!Directory.Exists("testrunner")) - { - Directory.CreateDirectory("testrunner"); - } + return _extensionsPackageVersion ??= UseDirectoryPropsVersion + ? GetDynamicVersionFromProps("PackageExtensionVersion") + : PackageVersion; + } - var nugetInstallSettings = new NuGetInstallSettings + /// + /// Gets the effective package version for the specified package ID. + /// This method provides a unified interface for accessing cached package versions. + /// + /// The package ID (LocalStack.Client or LocalStack.Client.Extensions) + /// The cached package version + public string GetEffectivePackageVersion(string packageId) + { + return packageId switch { - Version = "2.4.1", Verbosity = NuGetVerbosity.Normal, OutputDirectory = "testrunner", WorkingDirectory = "." + LocalStackClientProjName => GetClientPackageVersion(), + LocalStackClientExtensionsProjName => GetExtensionsPackageVersion(), + _ => throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId)), }; + } - this.NuGetInstall("xunit.runner.console", nugetInstallSettings); + public static void ValidateArgument(string argumentName, string argument) + { + if (string.IsNullOrWhiteSpace(argument)) + { + throw new Exception($"{argumentName} can not be null or empty"); + } } 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))]; - IList projMetadata = new List(); + var projMetadata = new List(); foreach (FilePath csProj in csProjFile) { @@ -128,50 +173,164 @@ 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.4.1/tools/{targetFramework}/xunit.console.exe {assemblyPath}" }); + int result = this.StartProcess("mono", new ProcessSettings + { + Arguments = "--version", + RedirectStandardOutput = true, + NoWorkingDirectory = true, + }); - if (exitCode != 0) + if (result == 0) { - throw new InvalidOperationException($"Exit code: {exitCode}"); + this.Information("βœ… Mono is already installed. Skipping installation."); + return; } - } - public string GetProjectVersion() - { - FilePath file = this.File("./src/Directory.Build.props"); + this.Information("Mono not found. Starting installation on Linux for .NET Framework test platform support..."); - this.Information(file.FullPath); + // Add Mono repository key + int exitCode1 = this.StartProcess("sudo", new ProcessSettings + { + Arguments = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF", + }); - string project = File.ReadAllText(file.FullPath, Encoding.UTF8); - int startIndex = project.IndexOf("", StringComparison.Ordinal) + "".Length; - int endIndex = project.IndexOf("", startIndex, StringComparison.Ordinal); + if (exitCode1 != 0) + { + this.Warning($"⚠️ Failed to add Mono repository key (exit code: {exitCode1})"); + return; + } - string version = project.Substring(startIndex, endIndex - startIndex); - version = $"{version}.{PackageVersion}"; + // 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" }); - return version; + if (exitCode4 != 0) + { + 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 GetExtensionProjectVersion() + /// + /// 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) { - FilePath file = this.File(LocalStackClientExtProjFile); + if (!PackageIdProjMap.TryGetValue(packageId, out FilePath? projectFile) || projectFile == null) + { + throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId)); + } - this.Information(file.FullPath); + string[] frameworks = GetProjectTargetFrameworks(projectFile.FullPath); + return string.Join(", ", frameworks); + } - string project = File.ReadAllText(file.FullPath, Encoding.UTF8); - int startIndex = project.IndexOf("", StringComparison.Ordinal) + "".Length; - int endIndex = project.IndexOf("", startIndex, StringComparison.Ordinal); + /// + /// 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 = $""; - string version = project.Substring(startIndex, endIndex - startIndex); - version = $"{version}.{PackageVersion}"; + 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('_', '-'); + + // SemVer-compliant pre-release versioning + if (BranchName == "master") + { + // Master nightlies: 2.0.0-nightly.20250725.sha + return $"{baseVersion}-nightly.{buildDate}.{commitSha}"; + } + else + { + // Feature branches: 2.0.0-feature-name.20250725.sha + 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}"); + } - return version; + // Fallback to timestamp-based identifier + return DateTime.UtcNow.ToString("HHmmss", System.Globalization.CultureInfo.InvariantCulture); } - private IEnumerable GetProjectTargetFrameworks(string csprojPath) + private string[] GetProjectTargetFrameworks(string csprojPath) { FilePath file = this.File(csprojPath); string project = File.ReadAllText(file.FullPath, Encoding.UTF8); @@ -183,7 +342,7 @@ private IEnumerable 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(';'); } @@ -202,14 +361,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("/", StringComparison.Ordinal) + 1; + 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/CakeTasks/BuildTask.cs b/build/LocalStack.Build/CakeTasks/BuildTask.cs new file mode 100644 index 0000000..e07da72 --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/BuildTask.cs @@ -0,0 +1,8 @@ +ο»Ώ[TaskName("build"), IsDependentOn(typeof(InitTask))] +public sealed class BuildTask : FrostingTask +{ + public override void Run(BuildContext context) + { + context.DotNetBuild(context.SlnFilePath, new DotNetBuildSettings { Configuration = context.BuildConfiguration }); + } +} \ No newline at end of file diff --git a/build/LocalStack.Build/CakeTasks/InitTask.cs b/build/LocalStack.Build/CakeTasks/InitTask.cs new file mode 100644 index 0000000..d438009 --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/InitTask.cs @@ -0,0 +1,18 @@ +ο»Ώ[TaskName("init")] +public sealed class InitTask : FrostingTask +{ + public override void Run(BuildContext context) + { + ConsoleHelper.WriteRule("Initialization"); + ConsoleHelper.WriteHeader(); + + context.StartProcess("dotnet", new ProcessSettings { Arguments = "--info" }); + + if (!context.IsRunningOnUnix()) + { + return; + } + + context.StartProcess("git", new ProcessSettings { Arguments = "config --global core.autocrlf true" }); + } +} \ No newline at end of file diff --git a/build/LocalStack.Build/CakeTasks/Nuget/NugetPackAndPublishTask.cs b/build/LocalStack.Build/CakeTasks/Nuget/NugetPackAndPublishTask.cs new file mode 100644 index 0000000..215220b --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/Nuget/NugetPackAndPublishTask.cs @@ -0,0 +1,22 @@ +ο»Ώusing LocalStack.Build.CakeTasks.Nuget.Services; + +[TaskName("nuget-pack-and-publish")] +public sealed class NugetPackAndPublishTask : FrostingTask +{ + public override void Run(BuildContext context) + { + ConsoleHelper.WriteRule("Pack & Publish Pipeline"); + + string effectiveVersion = context.GetEffectivePackageVersion(context.PackageId); + ConsoleHelper.WriteInfo($"Using consistent version: {effectiveVersion}"); + + ConsoleHelper.WriteProcessing("Step 1: Creating package..."); + PackageOperations.PackSinglePackage(context, context.PackageId); + + ConsoleHelper.WriteProcessing("Step 2: Publishing package..."); + PackageOperations.PublishSinglePackage(context, context.PackageId); + + ConsoleHelper.WriteSuccess("Pack & Publish pipeline completed successfully!"); + ConsoleHelper.WriteRule(); + } +} \ No newline at end of file diff --git a/build/LocalStack.Build/CakeTasks/Nuget/NugetPackTask.cs b/build/LocalStack.Build/CakeTasks/Nuget/NugetPackTask.cs new file mode 100644 index 0000000..c1fe0f0 --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/Nuget/NugetPackTask.cs @@ -0,0 +1,37 @@ +ο»Ώusing LocalStack.Build.CakeTasks.Nuget.Services; + +[TaskName("nuget-pack")] +public sealed class NugetPackTask : FrostingTask +{ + public override void Run(BuildContext 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) + { + PackageOperations.PackSinglePackage(context, packageId); + } +} \ No newline at end of file diff --git a/build/LocalStack.Build/CakeTasks/Nuget/NugetPrepareExtensionsTask.cs b/build/LocalStack.Build/CakeTasks/Nuget/NugetPrepareExtensionsTask.cs new file mode 100644 index 0000000..053dcc9 --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/Nuget/NugetPrepareExtensionsTask.cs @@ -0,0 +1,65 @@ +[TaskName("nuget-prepare-extensions")] +public sealed class NugetPrepareExtensionsTask : FrostingTask +{ + public override void Run(BuildContext context) + { + ConsoleHelper.WriteRule("Prepare Extensions Project"); + + // Validate that this is for Extensions package + if (context.PackageId != BuildContext.LocalStackClientExtensionsProjName) + { + throw new InvalidOperationException($"This task is only for {BuildContext.LocalStackClientExtensionsProjName}, but received: {context.PackageId}"); + } + + // Client version must be explicitly provided + if (string.IsNullOrWhiteSpace(context.ClientVersion)) + { + throw new InvalidOperationException("Client version must be specified via --client-version parameter. This task does not generate versions automatically."); + } + + ConsoleHelper.WriteInfo($"Preparing Extensions project for LocalStack.Client v{context.ClientVersion}"); + ConsoleHelper.WriteInfo($"Package source: {context.PackageSource}"); + + PrepareExtensionsProject(context, context.ClientVersion); + + ConsoleHelper.WriteSuccess("Extensions project preparation completed!"); + ConsoleHelper.WriteRule(); + } + + private static void PrepareExtensionsProject(BuildContext context, string version) + { + ConsoleHelper.WriteProcessing("Updating Extensions project dependencies..."); + + try + { + // Use the Extensions project file path directly + string extensionsProject = context.LocalStackClientExtProjFile.Path.FullPath; + var clientProjectRef = context.File(context.LocalStackClientProjFile.Path.FullPath); + + // Remove project reference + context.DotNetRemoveReference(extensionsProject, [clientProjectRef]); + ConsoleHelper.WriteInfo("Removed project reference to LocalStack.Client"); + + // Add package reference with specific version and source + var packageSettings = new DotNetPackageAddSettings + { + Version = version, + }; + + // Add source if not NuGet (GitHub Packages, MyGet, etc.) + if (context.PackageSource != BuildContext.NuGetPackageSource) + { + packageSettings.Source = context.PackageSourceMap[context.PackageSource]; + ConsoleHelper.WriteInfo($"Using package source: {context.PackageSource}"); + } + + context.DotNetAddPackage(BuildContext.LocalStackClientProjName, extensionsProject, packageSettings); + ConsoleHelper.WriteSuccess($"Added package reference for {BuildContext.LocalStackClientProjName} v{version}"); + } + catch (Exception ex) + { + ConsoleHelper.WriteError($"Failed to prepare Extensions project: {ex.Message}"); + throw; + } + } +} \ No newline at end of file diff --git a/build/LocalStack.Build/CakeTasks/Nuget/NugetPushTask.cs b/build/LocalStack.Build/CakeTasks/Nuget/NugetPushTask.cs new file mode 100644 index 0000000..aea54d8 --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/Nuget/NugetPushTask.cs @@ -0,0 +1,14 @@ +ο»Ώusing LocalStack.Build.CakeTasks.Nuget.Services; + +[TaskName("nuget-push")] +public sealed class NugetPushTask : FrostingTask +{ + public override void Run(BuildContext context) + { + ConsoleHelper.WriteRule("Package Publishing"); + + PackageOperations.PublishSinglePackage(context, context.PackageId); + + ConsoleHelper.WriteRule(); + } +} \ No newline at end of file diff --git a/build/LocalStack.Build/CakeTasks/Nuget/Services/PackageOperations.cs b/build/LocalStack.Build/CakeTasks/Nuget/Services/PackageOperations.cs new file mode 100644 index 0000000..afdf69a --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/Nuget/Services/PackageOperations.cs @@ -0,0 +1,227 @@ +#pragma warning disable CA1515 // Consider making public types internal + +namespace LocalStack.Build.CakeTasks.Nuget.Services; + +/// +/// Provides high-level package operations shared across NuGet tasks. +/// Two simple methods that handle all the complexity internally. +/// +public static class PackageOperations +{ + /// + /// Complete pack operation for a single package - handles everything from validation to success message. + /// + /// The build context + /// The package identifier + public static void PackSinglePackage(BuildContext context, string packageId) + { + string effectiveVersion = context.GetEffectivePackageVersion(packageId); + string packageTargetFrameworks = context.GetPackageTargetFrameworks(packageId); + + // Display package info + ConsoleHelper.WritePackageInfoTable(packageId, effectiveVersion, packageTargetFrameworks, context.BuildConfiguration, context.PackageSource); + + // Validate inputs + ValidatePackInputs(context, packageId, effectiveVersion); + + // Create package with progress + ConsoleHelper.WithProgress($"Creating {packageId} package", _ => CreatePackage(context, packageId, effectiveVersion)); + + // Success message + ConsoleHelper.WriteSuccess($"Successfully created {packageId} v{effectiveVersion}"); + ConsoleHelper.WriteInfo($"Package location: {context.ArtifactOutput}"); + + // Output version to GitHub Actions if this is LocalStack.Client + if (packageId == BuildContext.LocalStackClientProjName) + { + OutputVersionToGitHubActions(effectiveVersion); + } + } + + /// + /// Complete publish operation for a single package - handles everything from validation to success message. + /// + /// The build context + /// The package identifier + public static void PublishSinglePackage(BuildContext context, string packageId) + { + string effectiveVersion = context.GetEffectivePackageVersion(packageId); + string packageTargetFrameworks = context.GetPackageTargetFrameworks(packageId); + + // Validate inputs for publishing + ValidatePublishInputs(context, packageId); + + // Show version info + if (context.UseDirectoryPropsVersion) + { + ConsoleHelper.WriteInfo($"Using dynamic version: {effectiveVersion}"); + } + else + { + ConsoleHelper.WriteInfo($"Using version: {effectiveVersion}"); + } + + // Display package info + ConsoleHelper.WritePackageInfoTable(packageId, effectiveVersion, packageTargetFrameworks, context.BuildConfiguration, context.PackageSource); + + // Publish package with progress + ConsoleHelper.WithProgress("Publishing package", _ => PublishPackage(context, packageId, effectiveVersion)); + + // Success summary + var downloadUrl = GetDownloadUrl(context.PackageSource, packageId, effectiveVersion); + ConsoleHelper.WritePublicationSummary(packageId, effectiveVersion, context.PackageSource, downloadUrl); + } + + #region Private Implementation Details + + private static void CreatePackage(BuildContext context, string packageId, string version) + { + if (!Directory.Exists(context.ArtifactOutput)) + { + Directory.CreateDirectory(context.ArtifactOutput); + } + + 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, + NoBuild = false, + NoRestore = false, + MSBuildSettings = new DotNetMSBuildSettings(), + }; + + settings.MSBuildSettings.SetVersion(version); + context.DotNetPack(packageCsProj.FullPath, settings); + } + + private static void PublishPackage(BuildContext context, string packageId, string version) + { + ConvertableFilePath packageFile = context.ArtifactOutput + context.File($"{packageId}.{version}.nupkg"); + + if (!context.FileExists(packageFile)) + { + 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 {packageId} to {context.PackageSource}..."); + + context.DotNetNuGetPush(packageFile.Path.FullPath, new DotNetNuGetPushSettings() + { + ApiKey = packageSecret, + Source = packageSource, + }); + + ConsoleHelper.WriteSuccess($"Successfully published {packageId} v{version}"); + } + + private static void ValidatePackInputs(BuildContext context, string packageId, string effectiveVersion) + { + BuildContext.ValidateArgument("package-id", packageId); + BuildContext.ValidateArgument("package-source", context.PackageSource); + + if (context.UseDirectoryPropsVersion) + { + ConsoleHelper.WriteInfo("Using dynamic version generation from Directory.Build.props"); + return; + } + + ValidatePackageVersion(context, packageId, effectiveVersion); + } + + private static void ValidatePublishInputs(BuildContext context, string packageId) + { + BuildContext.ValidateArgument("package-id", packageId); + BuildContext.ValidateArgument("package-secret", context.PackageSecret); + BuildContext.ValidateArgument("package-source", context.PackageSource); + + if (!context.UseDirectoryPropsVersion) + { + BuildContext.ValidateArgument("package-version", context.PackageVersion); + } + } + + private static void ValidatePackageVersion(BuildContext context, string packageId, string version) + { + Match match = Regex.Match(version, @"^(\d+)\.(\d+)\.(\d+)([\.\-].*)*$", RegexOptions.IgnoreCase); + + if (!match.Success) + { + throw new Exception($"Invalid version: {version}"); + } + + if (context.PackageSource == BuildContext.GitHubPackageSource) + { + ConsoleHelper.WriteInfo("Skipping version validation for GitHub Packages source"); + return; + } + + try + { + string packageSource = context.PackageSourceMap[context.PackageSource]; + var nuGetListSettings = new NuGetListSettings { AllVersions = false, Source = [packageSource] }; + NuGetListItem nuGetListItem = context.NuGetList(packageId, nuGetListSettings).Single(item => item.Name == packageId); + string latestPackVersionStr = nuGetListItem.Version; + + Version packageVersion = Version.Parse(version); + Version latestPackVersion = Version.Parse(latestPackVersionStr); + + 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) + { + ConsoleHelper.WriteWarning($"Could not validate version against existing packages: {ex.Message}"); + } + } + + private static string GetDownloadUrl(string packageSource, string packageId, string version) + { + return packageSource switch + { + BuildContext.GitHubPackageSource => $"https://github.com/localstack-dotnet/localstack-dotnet-client/packages/nuget/{packageId}", + BuildContext.NuGetPackageSource => $"https://www.nuget.org/packages/{packageId}/{version}", + BuildContext.MyGetPackageSource => $"https://www.myget.org/packages/{packageId}/{version}", + _ => "Unknown package source", + }; + } + + /// + /// Outputs the package version to GitHub Actions for use in subsequent steps. + /// Only outputs if running in GitHub Actions environment. + /// + /// The package version to output + private static void OutputVersionToGitHubActions(string version) + { + string? githubOutput = Environment.GetEnvironmentVariable("GITHUB_OUTPUT"); + + if (string.IsNullOrWhiteSpace(githubOutput)) + { + return; + } + + try + { + var outputLine = $"client-version={version}"; + File.AppendAllText(githubOutput, outputLine + Environment.NewLine); + ConsoleHelper.WriteInfo($"πŸ“€ GitHub Actions Output: {outputLine}"); + } + catch (Exception ex) + { + ConsoleHelper.WriteWarning($"Failed to write to GitHub Actions output: {ex.Message}"); + } + } + + #endregion +} \ No newline at end of file diff --git a/build/LocalStack.Build/CakeTasks/TestTask.cs b/build/LocalStack.Build/CakeTasks/TestTask.cs new file mode 100644 index 0000000..319bcee --- /dev/null +++ b/build/LocalStack.Build/CakeTasks/TestTask.cs @@ -0,0 +1,73 @@ +ο»Ώ[TaskName("tests"), IsDependentOn(typeof(BuildTask))] +public sealed class TestTask : FrostingTask +{ + public override void Run(BuildContext context) + { + const string testResults = "results.trx"; + + var settings = new DotNetTestSettings + { + NoRestore = !context.ForceRestore, NoBuild = !context.ForceBuild, Configuration = context.BuildConfiguration, Blame = true, + }; + + IEnumerable projMetadata = context.GetProjMetadata(); + + foreach (ProjMetadata testProj in projMetadata) + { + string testProjectPath = testProj.CsProjPath; + string targetFrameworks = string.Join(',', testProj.TargetFrameworks); + + ConsoleHelper.WriteInfo($"Target Frameworks: {targetFrameworks}"); + + foreach (string targetFramework in testProj.TargetFrameworks) + { + if (context.SkipFunctionalTest && testProj.AssemblyName == "LocalStack.Client.Functional.Tests") + { + ConsoleHelper.WriteWarning("Skipping Functional Tests"); + + continue; + } + + ConsoleHelper.WriteRule($"Running {targetFramework.ToUpper(System.Globalization.CultureInfo.CurrentCulture)} tests for {testProj.AssemblyName}"); + settings.Framework = targetFramework; + + if (testProj.AssemblyName == "LocalStack.Client.Functional.Tests") + { + ConsoleHelper.WriteProcessing("Deleting running docker containers"); + + try + { + string psOutput = context.DockerPs(new DockerContainerPsSettings() { All = true, Quiet = true }); + + if (!string.IsNullOrEmpty(psOutput)) + { + ConsoleHelper.WriteInfo($"Found containers: {psOutput}"); + + string[] containers = psOutput.Split([Environment.NewLine], StringSplitOptions.None); + context.DockerRm(containers); + } + } + catch + { + // ignored + } + } + + // .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 platform = context.IsRunningOnLinux() ? "Linux (with external Mono)" : "macOS (built-in Mono)"; + ConsoleHelper.WriteInfo($"Running .NET Framework tests on {platform}"); + } + + string testFilePrefix = targetFramework.Replace('.', '-'); + settings.ArgumentCustomization = args => args.Append($" --logger \"trx;LogFileName={testFilePrefix}_{testResults}\""); + context.DotNetTest(testProjectPath, settings); + + ConsoleHelper.WriteSuccess($"Completed {targetFramework} tests for {testProj.AssemblyName}"); + } + } + } +} \ No newline at end of file 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..7ce57ab 100644 --- a/build/LocalStack.Build/GlobalUsings.cs +++ b/build/LocalStack.Build/GlobalUsings.cs @@ -2,15 +2,18 @@ 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.DotNet.Package.Add; 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 +25,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 3662696..4b263e5 100644 --- a/build/LocalStack.Build/LocalStack.Build.csproj +++ b/build/LocalStack.Build/LocalStack.Build.csproj @@ -1,15 +1,15 @@ Exe - net7.0 + net9.0 $(MSBuildProjectDirectory) latest - $(NoWarn);CA1303;CA1707;CS8601;CS8618;MA0047;MA0048;CA1050;S3903;MA0006;CA1031;CA1062;MA0051;S112;CA2201;CA1307;MA0074;MA0023;MA0009;CA1307;CA1310 + $(NoWarn);CA1303;CA1707;CS8601;CS8618;MA0047;MA0048;CA1050;S3903;MA0006;CA1031;CA1062;MA0051;S112;CA2201;CA1307;MA0074;MA0023;MA0009;CA1307;CA1310;CA1515;CA1054;CA1055 - + \ No newline at end of file diff --git a/build/LocalStack.Build/Models/ProjMetadata.cs b/build/LocalStack.Build/Models/ProjMetadata.cs index 27a3dc9..38783f2 100644 --- a/build/LocalStack.Build/Models/ProjMetadata.cs +++ b/build/LocalStack.Build/Models/ProjMetadata.cs @@ -1,3 +1,5 @@ -ο»Ώnamespace LocalStack.Build.Models; +ο»Ώ#pragma warning disable CA1515 // Consider making public types internal + +namespace LocalStack.Build.Models; public record ProjMetadata(string DirectoryPath, string CsProjPath, IEnumerable TargetFrameworks, string AssemblyName); \ No newline at end of file diff --git a/build/LocalStack.Build/Program.cs b/build/LocalStack.Build/Program.cs index 15d5c56..388a455 100644 --- a/build/LocalStack.Build/Program.cs +++ b/build/LocalStack.Build/Program.cs @@ -1,210 +1,6 @@ -ο»Ώreturn new CakeHost() - .UseContext() - .Run(args); +ο»Ώ#pragma warning disable CA1515 // Consider making public types internal -[TaskName("Default"), IsDependentOn(typeof(TestTask))] -public class DefaultTask : FrostingTask -{ -} - -[TaskName("init")] -public sealed class InitTask : FrostingTask -{ - public override void Run(BuildContext context) - { - context.StartProcess("dotnet", new ProcessSettings - { - Arguments = "--info" - }); - - if (!context.IsRunningOnUnix()) - { - return; - } - - 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)),] -public sealed class BuildTask : FrostingTask -{ - public override void Run(BuildContext context) - { - context.DotNetBuild(context.SlnFilePath, - new DotNetBuildSettings - { - Configuration = context.BuildConfiguration - }); - } -} - -[TaskName("tests"), IsDependentOn(typeof(BuildTask))] -public sealed class TestTask : FrostingTask -{ - public override void Run(BuildContext context) - { - const string testResults = "results.trx"; - - var settings = new DotNetTestSettings - { - NoRestore = !context.ForceRestore, - NoBuild = !context.ForceBuild, - Configuration = context.BuildConfiguration, - Blame = true - }; - - IEnumerable projMetadata = context.GetProjMetadata(); - - foreach (ProjMetadata testProj in projMetadata) - { - string testProjectPath = testProj.CsProjPath; - string targetFrameworks = string.Join(",", testProj.TargetFrameworks); - - context.Warning($"Target Frameworks {targetFrameworks}"); - - foreach (string targetFramework in testProj.TargetFrameworks) - { - if (context.SkipFunctionalTest && testProj.AssemblyName == "LocalStack.Client.Functional.Tests") - { - context.Warning("Skipping Functional Tests"); - continue; - } - - context.Warning($"=============Running {targetFramework.ToUpper(System.Globalization.CultureInfo.CurrentCulture)} tests for {testProj.AssemblyName}============="); - settings.Framework = targetFramework; - - if (testProj.AssemblyName == "LocalStack.Client.Functional.Tests") - { - context.Warning("Deleting running docker containers"); - - try - { - string psOutput = context.DockerPs(new DockerContainerPsSettings() { All = true, Quiet = true}); - - if (!string.IsNullOrEmpty(psOutput)) - { - context.Warning(psOutput); - - string[] containers = psOutput.Split(new[]{ Environment.NewLine }, StringSplitOptions.None); - context.DockerRm(containers); - } - } - catch - { - // ignored - } - } - - - if (context.IsRunningOnUnix() && targetFramework == "net461") - { - context.RunXUnitUsingMono(targetFramework, $"{testProj.DirectoryPath}/bin/{context.BuildConfiguration}/{targetFramework}/{testProj.AssemblyName}.dll"); - } - else - { - string testFilePrefix = targetFramework.Replace(".", "-"); - settings.ArgumentCustomization = args => args.Append($" --logger \"trx;LogFileName={testFilePrefix}_{testResults}\""); - context.DotNetTest(testProjectPath, settings); - } - context.Warning("=============================================================="); - } - } - } -} +return new CakeHost().UseContext().Run(args); -[TaskName("nuget-pack")] -public sealed class NugetPackTask : FrostingTask -{ - public override void Run(BuildContext context) - { - ValidatePackageVersion(context); - - if (!Directory.Exists(context.ArtifactOutput)) - { - Directory.CreateDirectory(context.ArtifactOutput); - } - - FilePath packageCsProj = context.PackageIdProjMap[context.PackageId]; - - var settings = new DotNetPackSettings - { - Configuration = context.BuildConfiguration, - OutputDirectory = context.ArtifactOutput, - MSBuildSettings = new DotNetMSBuildSettings() - }; - - settings.MSBuildSettings.SetVersion(context.PackageVersion); - - context.DotNetPack(packageCsProj.FullPath, settings); - } - - private static void ValidatePackageVersion(BuildContext context) - { - BuildContext.ValidateArgument("package-id", context.PackageId); - BuildContext.ValidateArgument("package-version", context.PackageVersion); - BuildContext.ValidateArgument("package-source", context.PackageSource); - - Match match = Regex.Match(context.PackageVersion, @"^(\d+)\.(\d+)\.(\d+)(\.(\d+))*$", RegexOptions.IgnoreCase); - - if (!match.Success) - { - throw new Exception($"Invalid version: {context.PackageVersion}"); - } - - string packageSource = context.PackageSourceMap[context.PackageSource]; - - 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; - - Version packageVersion = Version.Parse(context.PackageVersion); - Version latestPackVersion = Version.Parse(latestPackVersionStr); - - if (packageVersion <= latestPackVersion) - { - throw new Exception($"The new package version {context.PackageVersion} should be greater than the latest package version {latestPackVersionStr}"); - } - } -} - -[TaskName("nuget-push")] -public sealed class NugetPushTask : FrostingTask -{ - public override void Run(BuildContext context) - { - 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; - - ConvertableFilePath packageFile = context.ArtifactOutput + context.File($"{packageId}.{packageVersion}.nupkg"); - - if (!context.FileExists(packageFile)) - { - throw new Exception($"The specified {packageFile.Path} package file does not exists"); - } - - string packageSecret = context.PackageSecret; - string packageSource = context.PackageSourceMap[context.PackageSource]; - - context.DotNetNuGetPush(packageFile.Path.FullPath, new DotNetNuGetPushSettings() - { - ApiKey = packageSecret, - Source = packageSource, - }); - } -} \ No newline at end of file +[TaskName("Default"), IsDependentOn(typeof(TestTask))] +public class DefaultTask : FrostingTask; \ 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..0062d08 --- /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.GetClientPackageVersion(), + BuildContext.LocalStackClientExtensionsProjName => context.GetExtensionsPackageVersion(), + _ => "Unknown", + }; + } +} \ No newline at end of file diff --git a/global.json b/global.json index 691f12c..2bb272e 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "7.0.400", + "version": "9.0.200", "rollForward": "latestFeature", "allowPrerelease": false } -} +} \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs b/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs index 19bb4bb..ffe3977 100644 --- a/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs +++ b/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs @@ -5,39 +5,40 @@ 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"; +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Creates generic ClientFactory and invokes internal members via reflection"), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif 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/Exceptions/LocalStackClientConfigurationException.cs b/src/LocalStack.Client.Extensions/Exceptions/LocalStackClientConfigurationException.cs index 53f2923..ed4e0ba 100644 --- a/src/LocalStack.Client.Extensions/Exceptions/LocalStackClientConfigurationException.cs +++ b/src/LocalStack.Client.Extensions/Exceptions/LocalStackClientConfigurationException.cs @@ -29,6 +29,11 @@ public LocalStackClientConfigurationException() /// /// The information to use when serializing the exception. /// The context for the serialization. +#if NET8_0_OR_GREATER +#pragma warning disable S1133, MA0070, CA1041 + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#pragma warning restore MA0070, S1133, CA1041 +#endif protected LocalStackClientConfigurationException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { } diff --git a/src/LocalStack.Client.Extensions/GlobalUsings.cs b/src/LocalStack.Client.Extensions/GlobalUsings.cs index fdd94f5..de1f8e2 100644 --- a/src/LocalStack.Client.Extensions/GlobalUsings.cs +++ b/src/LocalStack.Client.Extensions/GlobalUsings.cs @@ -1,4 +1,5 @@ ο»Ώglobal using System; +global using System.Diagnostics.CodeAnalysis; global using System.Reflection; global using System.Runtime.Serialization; diff --git a/src/LocalStack.Client.Extensions/ILLink.Descriptors.xml b/src/LocalStack.Client.Extensions/ILLink.Descriptors.xml new file mode 100644 index 0000000..0dbbf42 --- /dev/null +++ b/src/LocalStack.Client.Extensions/ILLink.Descriptors.xml @@ -0,0 +1,5 @@ +ο»Ώ + + + + \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/LICENSE.txt b/src/LocalStack.Client.Extensions/LICENSE.txt index 79dfe2e..f9478d1 100644 --- a/src/LocalStack.Client.Extensions/LICENSE.txt +++ b/src/LocalStack.Client.Extensions/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 LocalStack.NET +Copyright (c) 2019-2025 LocalStack.NET Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ 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 50104c9..d23ee89 100644 --- a/src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj +++ b/src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj @@ -1,63 +1,78 @@ ο»Ώ - - netstandard2.0;net6.0;net7.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.0 - true - true - - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - Always - - - - - - - - - + + 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 + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 da002e3..88d6fb0 100644 --- a/src/LocalStack.Client.Extensions/README.md +++ b/src/LocalStack.Client.Extensions/README.md @@ -1,21 +1,107 @@ -# 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)](https://www.nuget.org/packages/LocalStack.Client/) [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client%26source%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client%26source%3Dnuget%26track%3D1%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml) [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql) [![Linux Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/linux?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/linux) + +> **πŸš€ Quick Start**: `dotnet add package LocalStack.Client --version 2.0.0` (AWS SDK v4) | [Installation Guide](#-installation) | [GA Timeline](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45) + +--- + +## πŸŽ‰ What's New: AWS SDK v4 Support Available + +**v2.0.0** is live with complete AWS SDK v4 support - easier migration than expected! + +- βœ… **1,168 tests passing** across all frameworks +- βœ… **Minimal breaking changes** (just .NET Framework 4.6.2 β†’ 4.7.2 and AWS SDK v3 β†’ v4) +- βœ… **Public APIs unchanged** - your code should work as-is! +- πŸ“– **[Read Full Roadmap](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45)** + +**Version Strategy**: + +- v2.x (AWS SDK v4) active development on [master branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master) +- v1.x (AWS SDK v3) Available on [sdkv3-lts branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/sdkv3-lts), maintenance until July 2026 ![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 | -| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 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) | +## πŸš€ Platform Compatibility & Quality Status -## Continuous Integration +### Supported Platforms -| 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) | +- [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) | [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) +- [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) +- [.NET Framework 4.7.2 and Above](https://dotnet.microsoft.com/download/dotnet-framework) + +## ⚑ Nativeβ€―AOT & Trimming Status + +> **Heads‑up for `dotnet publish -p:PublishAot=true` / `PublishTrimmed=true` users** + +- **v2.0.0 GA ships without Nativeβ€―AOT support.** + The current build still relies on reflection for some AWS SDK internals. + - Public entry points that touch reflection are tagged with + `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`. + - You’ll see IL3050 / IL2026 warnings at compile time (promoted to errors in a strict AOT publish). +- We already ship a linker descriptor with **`LocalStack.Client.Extensions`** + to keep the private `ClientFactory` machinery alive. **Nothing to configure for that part.** +- Until the reflection‑free, source‑generated path lands (work in progress in + [draftΒ PRβ€―#49](https://github.com/localstack-dotnet/localstack-dotnet-client/pull/49) and tracked on + [roadmapΒ #48](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/48)): + + 1. **Suppress** the warnings in your app *or* call only the APIs that don’t rely on reflection. + 2. Add a tiny **service‑specific** linker descriptor for every `AWSSDK.*` package you reference + (S3, DynamoDB, etc.). For example, for S3: + + ```xml + + + + + + + + + + + + + + + + ``` + + 3. Wire it into your project once: + + ```xml + + + + ``` + + 4. If you hit a runtime β€œmissing member” error, ensure you’re on AWSΒ SDKΒ v4 **β‰₯β€―4.1.\*** and that + the concrete `AWSSDK.*` package you’re using is included in the descriptor above. + +> **Planned** – v2.1 will introduce an AOT‑friendly factory that avoids reflection entirely; once you +> migrate to that API these warnings and extra XML files go away. + +### Build & Test Matrix + +| Category | Platform/Type | Status | Description | +|----------|---------------|--------|-------------| +| **πŸ”§ Build** | Cross-Platform | [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml) | Matrix testing: Windows, Linux, macOS | +| **πŸ”’ Security** | Static Analysis | [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql) | CodeQL analysis & dependency review | +| **πŸ§ͺ Tests** | Linux | [![Linux Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/linux?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/linux) | All framework targets | +| **πŸ§ͺ Tests** | Windows | [![Windows Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/windows?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/windows) | All framework targets | +| **πŸ§ͺ Tests** | macOS | [![macOS Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/macos?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/macos) | All framework targets | + +## Package Status + +| Package | NuGet.org | GitHub Packages (Nightly) | +|---------|-----------|---------------------------| +| **LocalStack.Client v1.x** | [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dnuget%26track%3D1%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) | [![Github v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dgithub%26track%3D1%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client) | +| **LocalStack.Client v2.x** | [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) | [![Github v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dgithub%26track%3D2%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client) | +| **LocalStack.Client.Extensions v1.x** | [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dnuget%26track%3D1%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![Github v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dgithub%26track%3D1%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client.Extensions) | +| **LocalStack.Client.Extensions v2.x** | [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![GitHub Packages v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client.extensions%26source%3Dgithub%26track%3D2%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client.Extensions) | ## Table of Contents @@ -33,20 +119,13 @@ Localstack.NET is an easy-to-use .NET client for [LocalStack](https://github.com 7. [Changelog](#changelog) 8. [License](#license) -## Supported Platforms - -- [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0) -- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) -- [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) -- [.NET 4.6.1 and Above](https://dotnet.microsoft.com/download/dotnet-framework) - ## Why LocalStack.NET Client? - **Consistent Client Configuration:** LocalStack.NET eliminates the need for manual endpoint configuration, providing a standardized and familiar approach to initializing clients. - **Adaptable Environment Transition:** Easily switch between LocalStack and actual AWS services with minimal configuration changes. -- **Versatile .NET Compatibility:** Supports a broad spectrum of .NET versions, from .NET 7.0 and .NET Standard 2.0 to .NET Framework 4.6.1 and above. +- **Versatile .NET Compatibility:** Supports a broad spectrum of .NET versions, from .NET 9.0 and .NET Standard 2.0 to .NET Framework 4.6.2 and above. - **Reduced Learning Curve:** Offers a familiar interface tailored for LocalStack, ideal for developers acquainted with the AWS SDK for .NET. @@ -60,13 +139,36 @@ 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 + +To install the latest version of LocalStack.NET with AWS SDK v4 support, use the following command: ```bash -dotnet add package LocalStack.Client -dotnet add package LocalStack.Client.Extensions +# Install v2.0.0 with AWS SDK v4 support +dotnet add package LocalStack.Client --version 2.0.0 +dotnet add package LocalStack.Client.Extensions --version 2.0.0 ``` +#### 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). @@ -129,7 +231,9 @@ The `RegionName` is important as LocalStack creates resources based on the speci ## Known Issues -- **LocalStack Versions v2.0.1 - v2.2:** In versions v2.0.1 through v2.2 of LocalStack, the URL routing logic was changed, causing issues with SQS and S3 operations. Two issues were opened in LocalStack regarding this: [issue #8928](https://github.com/localstack/localstack/issues/8928) and [issue #8924](https://github.com/localstack/localstack/issues/8924). LocalStack addressed this problem with [PR #8962](https://github.com/localstack/localstack/pull/8962). Therefore, when using LocalStack.NET, either use version v2.0 of LocalStack (there are no issues with the v1 series as well) or the upcoming v2.3 version, or use the latest container from Docker Hub. +- **SNS with LocalStack v3.7.2 and v3.8.0:** During development on the new version, it was discovered that SNS functional tests are not working in LocalStack versions v3.7.2 and v3.8.0. This issue was reported in LocalStack [issue #11652](https://github.com/localstack/localstack/issues/11652). The LocalStack team identified a bug related to handling SNS URIs and resolved it in [PR #11653](https://github.com/localstack/localstack/pull/11653). The fix will be included in an upcoming release of LocalStack. In the meantime, if you're using SNS, it is recommended to stick to version v3.7.1 of LocalStack until the fix is available. + +- **LocalStack Versions v2.0.1 - v2.2:** In versions v2.0.1 through v2.2 of LocalStack, the URL routing logic was changed, causing issues with SQS and S3 operations. Two issues were opened in LocalStack regarding this: [issue #8928](https://github.com/localstack/localstack/issues/8928) and [issue #8924](https://github.com/localstack/localstack/issues/8924). LocalStack addressed this problem with [PR #8962](https://github.com/localstack/localstack/pull/8962). Therefore, when using LocalStack.NET, either use version v2.0 of LocalStack (there are no issues with the v1 series as well) or the upcoming v2.3 version, or use the latest v3 series container from Docker Hub. - **AWS_SERVICE_URL Environment Variable:** Unexpected behaviors might occur in LocalStack.NET when the `AWS_SERVICE_URL` environment variable is set. This environment variable is typically set by LocalStack in the container when using AWS Lambda, and AWS also uses this environment variable in the live environment. Soon, just like in LocalStack's official Python library, this environment variable will be prioritized by LocalStack.NET when configuring the LocalStack host, and there will be a general simplification in the configuration. You can follow this in the issues [issue #27](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/27) and [issue #32](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/32). You set the `AWS_SERVICE_URL` to empty string until this issue is resolved. @@ -139,6 +243,8 @@ Environment.SetEnvironmentVariable("AWS_SERVICE_URL", string.Empty); - **IAmazonLambda Operations:** There's a general issue with `IAmazonLambda` operations. This matter is currently under investigation. +- **AWSSDK.SQS Compatibility:** Starting from version `3.7.300.*` of `AWSSDK.SQS`, there are compatibility issues with LocalStack v1 and v2 series versions. The [v3](https://hub.docker.com/r/localstack/localstack/tags?page=&page_size=&ordering=&name=3.4) series of LocalStack does not have these issues. Therefore, it is recommended to either update your LocalStack container to the v3 series or downgrade your `AWSSDK.SQS` to version `3.7.200.*` if you are using LocalStack v1 or v2 series containers. It is important to note that this is not a problem related to LocalStack.NET, but rather an issue with the LocalStack container and the AWS SDK for .NET. + ## Developing We appreciate contributions in the form of feedback, bug reports, and pull requests. @@ -149,13 +255,13 @@ To build the project, use the following commands based on your operating system: Windows -``` +```powershell build.ps1 ``` Linux -``` +```bash ./build.sh ``` @@ -169,13 +275,13 @@ To execute the tests, use the commands below: Windows -``` +```powershell build.ps1 --target=tests ``` Linux -``` +```bash ./build.sh --target=tests ``` diff --git a/src/LocalStack.Client.Extensions/ServiceCollectionExtensions.cs b/src/LocalStack.Client.Extensions/ServiceCollectionExtensions.cs index 2fd7e65..6afe80b 100644 --- a/src/LocalStack.Client.Extensions/ServiceCollectionExtensions.cs +++ b/src/LocalStack.Client.Extensions/ServiceCollectionExtensions.cs @@ -4,6 +4,9 @@ public static class ServiceCollectionExtensions { private const string LocalStackSectionName = "LocalStack"; +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("ConfigurationBinder.Bind() sets options via reflection; keep non‑public setters or suppress in AOT build.")] +#endif public static IServiceCollection AddLocalStack(this IServiceCollection collection, IConfiguration configuration) { collection.Configure(options => configuration.GetSection(LocalStackSectionName).Bind(options, c => c.BindNonPublicProperties = true)); diff --git a/src/LocalStack.Client/CompatibilitySuppressions.xml b/src/LocalStack.Client/CompatibilitySuppressions.xml new file mode 100644 index 0000000..5a62906 --- /dev/null +++ b/src/LocalStack.Client/CompatibilitySuppressions.xml @@ -0,0 +1,39 @@ +ο»Ώ + + + + CP0008 + T:LocalStack.Client.Exceptions.LocalStackClientException + lib/net461/LocalStack.Client.dll + lib/netstandard2.0/LocalStack.Client.dll + true + + + CP0008 + T:LocalStack.Client.Exceptions.MisconfiguredClientException + lib/net461/LocalStack.Client.dll + lib/netstandard2.0/LocalStack.Client.dll + true + + + CP0008 + T:LocalStack.Client.Exceptions.NotSupportedClientException + lib/net461/LocalStack.Client.dll + lib/netstandard2.0/LocalStack.Client.dll + true + + + CP0008 + T:LocalStack.Client.Enums.AwsService + lib/net6.0/LocalStack.Client.dll + lib/netstandard2.0/LocalStack.Client.dll + true + + + CP0008 + T:LocalStack.Client.Enums.AwsService + lib/net7.0/LocalStack.Client.dll + lib/netstandard2.0/LocalStack.Client.dll + true + + \ No newline at end of file diff --git a/src/LocalStack.Client/Enums/AwsService.cs b/src/LocalStack.Client/Enums/AwsService.cs index 3688025..22dd038 100644 --- a/src/LocalStack.Client/Enums/AwsService.cs +++ b/src/LocalStack.Client/Enums/AwsService.cs @@ -2,110 +2,126 @@ public enum AwsService { - ApiGateway, - ApiGatewayV2, - Kinesis, - DynamoDb, - DynamoDbStreams, - ElasticSearch, - OpenSearch, - S3, - Firehose, - Lambda, - Sns, - Sqs, - Redshift, - RedshiftData, - Es, - Ses, - Sesv2, - Route53, - Route53Resolver, - Route53Domains, - CloudFormation, - CloudWatch, - Ssm, - SecretsManager, - StepFunctions, - Logs, - Events, - Elb, - Iot, - IoTAnalytics, - IoTEvents, - IoTEventsData, - IoTWireless, - IoTDataPlane, - IoTJobsDataPlane, - CognitoIdp, - CognitoIdentity, - Sts, - Iam, - Rds, - RdsData, - CloudSearch, - Swf, - Ec2, - ElastiCache, - Kms, - Emr, - Ecs, - Eks, - XRay, - ElasticBeanstalk, - AppSync, - CloudFront, - Athena, - Glue, - SageMaker, - SageMakerRuntime, - Ecr, - Qldb, - QldbSession, - CloudTrail, - Glacier, - Batch, - Organizations, - AutoScaling, - MediaStore, - MediaStoreData, - Transfer, - Acm, - CodeCommit, - KinesisAnalytics, - KinesisAnalyticsV2, - Amplify, - ApplicationAutoscaling, - Kafka, - ApiGatewayManagementApi, - TimeStreamQuery, - TimeStreamWrite, - S3Control, - ElbV2, - Support, - Neptune, - DocDb, - ServiceDiscovery, - ServerlessApplicationRepository, - AppConfig, - CostExplorer, - MediaConvert, - ResourceGroupsTaggingApi, - ResourceGroups, - Efs, - Backup, - LakeFormation, - Waf, - WafV2, - ConfigService, - Mwaa, - EventBridge, - Fis, - MarketplaceMetering, - Transcribe, - Mq, - EmrServerless, - Appflow, - Keyspaces, - Scheduler, + ApiGateway, + ApiGatewayV2, + Kinesis, + DynamoDb, + DynamoDbStreams, + ElasticSearch, + OpenSearch, + S3, + Firehose, + Lambda, + Sns, + Sqs, + Redshift, + RedshiftData, + Es, + Ses, + Sesv2, + Route53, + Route53Resolver, + Route53Domains, + CloudFormation, + CloudWatch, + Ssm, + SecretsManager, + StepFunctions, + Logs, + Events, + Elb, + Iot, + IoTAnalytics, + IoTEvents, + IoTEventsData, + IoTWireless, + IoTDataPlane, + IoTJobsDataPlane, + CognitoIdp, + CognitoIdentity, + Sts, + Iam, + Rds, + RdsData, + CloudSearch, + Swf, + Ec2, + ElastiCache, + Kms, + Emr, + Ecs, + Eks, + XRay, + ElasticBeanstalk, + AppSync, + CloudFront, + Athena, + Glue, + SageMaker, + SageMakerRuntime, + Ecr, + Qldb, + QldbSession, + CloudTrail, + Glacier, + Batch, + Organizations, + AutoScaling, + MediaStore, + MediaStoreData, + Transfer, + Acm, + CodeCommit, + KinesisAnalytics, + KinesisAnalyticsV2, + Amplify, + ApplicationAutoscaling, + Kafka, + ApiGatewayManagementApi, + TimeStreamQuery, + TimeStreamWrite, + S3Control, + ElbV2, + Support, + Neptune, + DocDb, + ServiceDiscovery, + ServerlessApplicationRepository, + AppConfig, + CostExplorer, + MediaConvert, + ResourceGroupsTaggingApi, + ResourceGroups, + Efs, + Backup, + LakeFormation, + Waf, + WafV2, + ConfigService, + Mwaa, + EventBridge, + Fis, + MarketplaceMetering, + Transcribe, + Mq, + EmrServerless, + Appflow, + Keyspaces, + Scheduler, + Ram, + AppConfigData, + Pinpoint, + Pipes, + Account, + ACMPCA, + Bedrock, + CloudControl, + CodeBuild, + CodeConnections, + CodeDeploy, + CodePipeline, + ElasticTranscoder, + MemoryDb, + Shield, + VerifiedPermissions, } \ No newline at end of file diff --git a/src/LocalStack.Client/Enums/AwsServiceEndpointMetadata.cs b/src/LocalStack.Client/Enums/AwsServiceEndpointMetadata.cs index e24fa41..b5405fd 100644 --- a/src/LocalStack.Client/Enums/AwsServiceEndpointMetadata.cs +++ b/src/LocalStack.Client/Enums/AwsServiceEndpointMetadata.cs @@ -111,16 +111,33 @@ public class AwsServiceEndpointMetadata public static readonly AwsServiceEndpointMetadata Appflow = new("Appflow", "appflow", CommonEndpointPattern, 4566, AwsService.Appflow); public static readonly AwsServiceEndpointMetadata Keyspaces = new("Keyspaces", "keyspaces", CommonEndpointPattern, 4566, AwsService.Keyspaces); public static readonly AwsServiceEndpointMetadata Scheduler = new("Scheduler", "scheduler", CommonEndpointPattern, 4566, AwsService.Scheduler); + public static readonly AwsServiceEndpointMetadata Ram = new("RAM", "ram", CommonEndpointPattern, 4566, AwsService.Ram); + public static readonly AwsServiceEndpointMetadata AppConfigData = new("AppConfigData", "appconfigdata", CommonEndpointPattern, 4632, AwsService.AppConfigData); + public static readonly AwsServiceEndpointMetadata Pinpoint = new("Pinpoint", "pinpoint", CommonEndpointPattern, 4566, AwsService.Pinpoint); + public static readonly AwsServiceEndpointMetadata Pipes = new("Pipes", "pipes", CommonEndpointPattern, 4566, AwsService.Pipes); + public static readonly AwsServiceEndpointMetadata Account = new("Account", "account", CommonEndpointPattern, 4567, AwsService.Account); + public static readonly AwsServiceEndpointMetadata ACMPCA = new("ACM PCA", "acm-pca", CommonEndpointPattern, 4567, AwsService.ACMPCA); + public static readonly AwsServiceEndpointMetadata Bedrock = new("Bedrock", "bedrock", CommonEndpointPattern, 4567, AwsService.Bedrock); + public static readonly AwsServiceEndpointMetadata CloudControl = new("CloudControl", "cloudcontrol", CommonEndpointPattern, 4567, AwsService.CloudControl); + public static readonly AwsServiceEndpointMetadata CodeBuild = new("CodeBuild", "codebuild", CommonEndpointPattern, 4567, AwsService.CodeBuild); + public static readonly AwsServiceEndpointMetadata CodeConnections = new("CodeConnections", "codeconnections", CommonEndpointPattern, 4567, AwsService.CodeConnections); + public static readonly AwsServiceEndpointMetadata CodeDeploy = new("CodeDeploy", "codedeploy", CommonEndpointPattern, 4567, AwsService.CodeDeploy); + public static readonly AwsServiceEndpointMetadata CodePipeline = new("CodePipeline", "codepipeline", CommonEndpointPattern, 4567, AwsService.CodePipeline); + public static readonly AwsServiceEndpointMetadata ElasticTranscoder = new("Elastic Transcoder", "elastictranscoder", CommonEndpointPattern, 4567, AwsService.ElasticTranscoder); + public static readonly AwsServiceEndpointMetadata MemoryDb = new("MemoryDB", "memorydb", CommonEndpointPattern, 4567, AwsService.MemoryDb); + public static readonly AwsServiceEndpointMetadata Shield = new("Shield", "shield", CommonEndpointPattern, 4567, AwsService.Shield); + public static readonly AwsServiceEndpointMetadata VerifiedPermissions = new("VerifiedPermissions", "verifiedpermissions", CommonEndpointPattern, 4567, AwsService.VerifiedPermissions); public static readonly AwsServiceEndpointMetadata[] All = - { + [ ApiGateway, ApiGatewayV2, Kinesis, DynamoDb, DynamoDbStreams, ElasticSearch, OpenSearch, S3, Firehose, Lambda, Sns, Sqs, Redshift, RedshiftData, Es, Ses, Sesv2, Route53, Route53Resolver, CloudFormation, CloudWatch, Ssm, SecretsManager, StepFunctions, Logs, Events, Elb, Iot, IoTAnalytics, IoTEvents, IoTEventsData, IoTWireless, IoTDataPlane, IoTJobsDataPlane, CognitoIdp, CognitoIdentity, Sts, Iam, Rds, RdsData, CloudSearch, Swf, Ec2, ElastiCache, Kms, Emr, Ecs, Eks, XRay, ElasticBeanstalk, AppSync, CloudFront, Athena, Glue, SageMaker, SageMakerRuntime, Ecr, Qldb, QldbSession, - CloudTrail, Glacier, Batch, Organizations, AutoScaling, MediaStore, MediaStoreData, Transfer, Acm, CodeCommit, KinesisAnalytics, KinesisAnalyticsV2, Amplify, ApplicationAutoscaling, Kafka, ApiGatewayManagementApi, + CloudTrail, Glacier, Batch, Organizations, AutoScaling, MediaStore, MediaStoreData, Transfer, Acm, CodeCommit, KinesisAnalytics, KinesisAnalyticsV2, Amplify, ApplicationAutoscaling, Kafka, ApiGatewayManagementApi, TimeStreamQuery, TimeStreamWrite, S3Control, ElbV2, Support, Neptune, DocDb, ServiceDiscovery, ServerlessApplicationRepository, AppConfig, CostExplorer, MediaConvert, ResourceGroupsTaggingApi, - ResourceGroups, Efs, Backup, LakeFormation, Waf, WafV2, ConfigService, Mwaa, EventBridge, Fis, MarketplaceMetering, Transcribe, Mq, EmrServerless, Appflow, Route53Domains, Keyspaces, Scheduler - }; + ResourceGroups, Efs, Backup, LakeFormation, Waf, WafV2, ConfigService, Mwaa, EventBridge, Fis, MarketplaceMetering, Transcribe, Mq, EmrServerless, Appflow, Route53Domains, Keyspaces, Scheduler, Ram, AppConfigData, + Pinpoint, Pipes, Account, ACMPCA, Bedrock, CloudControl, CodeBuild, CodeConnections, CodeDeploy, CodePipeline, ElasticTranscoder, MemoryDb, Shield, VerifiedPermissions, + ]; private AwsServiceEndpointMetadata(string serviceId, string cliName, string endPointPattern, int port, AwsService @enum) { diff --git a/src/LocalStack.Client/Exceptions/LocalStackClientException.cs b/src/LocalStack.Client/Exceptions/LocalStackClientException.cs index bebb273..03aced5 100644 --- a/src/LocalStack.Client/Exceptions/LocalStackClientException.cs +++ b/src/LocalStack.Client/Exceptions/LocalStackClientException.cs @@ -1,7 +1,11 @@ ο»Ώnamespace LocalStack.Client.Exceptions; [Serializable] -public abstract class LocalStackClientException : Exception +#if NETFRAMEWORK +public class LocalStackClientException : Exception, System.Runtime.InteropServices._Exception +#else +public class LocalStackClientException : Exception +#endif { /// /// Construct instance of ConfigurationException @@ -30,8 +34,12 @@ protected LocalStackClientException() /// /// The information to use when serializing the exception. /// The context for the serialization. - protected LocalStackClientException(SerializationInfo serializationInfo, StreamingContext streamingContext) - : base(serializationInfo, streamingContext) +#if NET8_0_OR_GREATER +#pragma warning disable S1133, MA0070, CA1041 + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#pragma warning restore MA0070, S1133, CA1041 +#endif + protected LocalStackClientException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { } } \ No newline at end of file diff --git a/src/LocalStack.Client/Exceptions/MisconfiguredClientException.cs b/src/LocalStack.Client/Exceptions/MisconfiguredClientException.cs index 6df9937..5ab7bb9 100644 --- a/src/LocalStack.Client/Exceptions/MisconfiguredClientException.cs +++ b/src/LocalStack.Client/Exceptions/MisconfiguredClientException.cs @@ -19,6 +19,11 @@ public MisconfiguredClientException() } /// +#if NET8_0_OR_GREATER +#pragma warning disable S1133, MA0070, CA1041 + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#pragma warning restore MA0070, S1133, CA1041 +#endif protected MisconfiguredClientException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { } diff --git a/src/LocalStack.Client/Exceptions/NotSupportedClientException.cs b/src/LocalStack.Client/Exceptions/NotSupportedClientException.cs index 4d3918b..e8bc125 100644 --- a/src/LocalStack.Client/Exceptions/NotSupportedClientException.cs +++ b/src/LocalStack.Client/Exceptions/NotSupportedClientException.cs @@ -17,7 +17,11 @@ public NotSupportedClientException() { } - /// +#if NET8_0_OR_GREATER +#pragma warning disable S1133, MA0070, CA1041 + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#pragma warning restore MA0070, S1133, CA1041 +#endif protected NotSupportedClientException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { } diff --git a/src/LocalStack.Client/GlobalUsings.cs b/src/LocalStack.Client/GlobalUsings.cs index ebddbb7..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 || NET461 +#if NETSTANDARD || NET472 namespace System.Runtime.CompilerServices { using System.ComponentModel; diff --git a/src/LocalStack.Client/LICENSE.txt b/src/LocalStack.Client/LICENSE.txt index 79dfe2e..f9478d1 100644 --- a/src/LocalStack.Client/LICENSE.txt +++ b/src/LocalStack.Client/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 LocalStack.NET +Copyright (c) 2019-2025 LocalStack.NET Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/src/LocalStack.Client/LocalStack.Client.csproj b/src/LocalStack.Client/LocalStack.Client.csproj index 33e736e..3a7e54c 100644 --- a/src/LocalStack.Client/LocalStack.Client.csproj +++ b/src/LocalStack.Client/LocalStack.Client.csproj @@ -1,7 +1,7 @@ ο»Ώ - netstandard2.0;net461;net6.0;net7.0 + netstandard2.0;net472;net8.0;net9.0 LocalStack.Client LocalStack.Client @@ -14,10 +14,11 @@ LICENSE.txt README.md true - + 1.4.1 + true true - $(NoWarn);MA0006 + $(NoWarn);MA0006;CA1510 @@ -50,8 +51,8 @@ - - NET461 + + NET472 \ No newline at end of file diff --git a/src/LocalStack.Client/Options/ConfigOptions.cs b/src/LocalStack.Client/Options/ConfigOptions.cs index 3c5c77c..f4c5957 100644 --- a/src/LocalStack.Client/Options/ConfigOptions.cs +++ b/src/LocalStack.Client/Options/ConfigOptions.cs @@ -1,5 +1,8 @@ ο»Ώnamespace LocalStack.Client.Options; +#if NET8_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif public class ConfigOptions : IConfigOptions { public ConfigOptions() diff --git a/src/LocalStack.Client/Options/LocalStackOptions.cs b/src/LocalStack.Client/Options/LocalStackOptions.cs index ae4ea48..0e96aba 100644 --- a/src/LocalStack.Client/Options/LocalStackOptions.cs +++ b/src/LocalStack.Client/Options/LocalStackOptions.cs @@ -1,5 +1,8 @@ ο»Ώnamespace LocalStack.Client.Options; +#if NET8_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif public class LocalStackOptions : ILocalStackOptions { public LocalStackOptions() diff --git a/src/LocalStack.Client/Options/SessionOptions.cs b/src/LocalStack.Client/Options/SessionOptions.cs index 092e1df..8cce43f 100644 --- a/src/LocalStack.Client/Options/SessionOptions.cs +++ b/src/LocalStack.Client/Options/SessionOptions.cs @@ -1,5 +1,8 @@ ο»Ώnamespace LocalStack.Client.Options; +#if NET8_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif public class SessionOptions : ISessionOptions { public SessionOptions() diff --git a/src/LocalStack.Client/README.md b/src/LocalStack.Client/README.md index da002e3..88d6fb0 100644 --- a/src/LocalStack.Client/README.md +++ b/src/LocalStack.Client/README.md @@ -1,21 +1,107 @@ -# 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)](https://www.nuget.org/packages/LocalStack.Client/) [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client%26source%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client%26source%3Dnuget%26track%3D1%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml) [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql) [![Linux Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/linux?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/linux) + +> **πŸš€ Quick Start**: `dotnet add package LocalStack.Client --version 2.0.0` (AWS SDK v4) | [Installation Guide](#-installation) | [GA Timeline](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45) + +--- + +## πŸŽ‰ What's New: AWS SDK v4 Support Available + +**v2.0.0** is live with complete AWS SDK v4 support - easier migration than expected! + +- βœ… **1,168 tests passing** across all frameworks +- βœ… **Minimal breaking changes** (just .NET Framework 4.6.2 β†’ 4.7.2 and AWS SDK v3 β†’ v4) +- βœ… **Public APIs unchanged** - your code should work as-is! +- πŸ“– **[Read Full Roadmap](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45)** + +**Version Strategy**: + +- v2.x (AWS SDK v4) active development on [master branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master) +- v1.x (AWS SDK v3) Available on [sdkv3-lts branch](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/sdkv3-lts), maintenance until July 2026 ![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 | -| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 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) | +## πŸš€ Platform Compatibility & Quality Status -## Continuous Integration +### Supported Platforms -| 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) | +- [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) | [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) +- [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) +- [.NET Framework 4.7.2 and Above](https://dotnet.microsoft.com/download/dotnet-framework) + +## ⚑ Nativeβ€―AOT & Trimming Status + +> **Heads‑up for `dotnet publish -p:PublishAot=true` / `PublishTrimmed=true` users** + +- **v2.0.0 GA ships without Nativeβ€―AOT support.** + The current build still relies on reflection for some AWS SDK internals. + - Public entry points that touch reflection are tagged with + `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`. + - You’ll see IL3050 / IL2026 warnings at compile time (promoted to errors in a strict AOT publish). +- We already ship a linker descriptor with **`LocalStack.Client.Extensions`** + to keep the private `ClientFactory` machinery alive. **Nothing to configure for that part.** +- Until the reflection‑free, source‑generated path lands (work in progress in + [draftΒ PRβ€―#49](https://github.com/localstack-dotnet/localstack-dotnet-client/pull/49) and tracked on + [roadmapΒ #48](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/48)): + + 1. **Suppress** the warnings in your app *or* call only the APIs that don’t rely on reflection. + 2. Add a tiny **service‑specific** linker descriptor for every `AWSSDK.*` package you reference + (S3, DynamoDB, etc.). For example, for S3: + + ```xml + + + + + + + + + + + + + + + + ``` + + 3. Wire it into your project once: + + ```xml + + + + ``` + + 4. If you hit a runtime β€œmissing member” error, ensure you’re on AWSΒ SDKΒ v4 **β‰₯β€―4.1.\*** and that + the concrete `AWSSDK.*` package you’re using is included in the descriptor above. + +> **Planned** – v2.1 will introduce an AOT‑friendly factory that avoids reflection entirely; once you +> migrate to that API these warnings and extra XML files go away. + +### Build & Test Matrix + +| Category | Platform/Type | Status | Description | +|----------|---------------|--------|-------------| +| **πŸ”§ Build** | Cross-Platform | [![CI/CD Pipeline](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/ci-cd.yml) | Matrix testing: Windows, Linux, macOS | +| **πŸ”’ Security** | Static Analysis | [![Security](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/localstack-dotnet/localstack-dotnet-client/actions/workflows/github-code-scanning/codeql) | CodeQL analysis & dependency review | +| **πŸ§ͺ Tests** | Linux | [![Linux Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/linux?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/linux) | All framework targets | +| **πŸ§ͺ Tests** | Windows | [![Windows Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/windows?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/windows) | All framework targets | +| **πŸ§ͺ Tests** | macOS | [![macOS Tests](https://img.shields.io/endpoint?url=https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/badge/tests/macos?label=Tests)](https://yvfdbfas85.execute-api.eu-central-1.amazonaws.com/live/redirect/test-results/macos) | All framework targets | + +## Package Status + +| Package | NuGet.org | GitHub Packages (Nightly) | +|---------|-----------|---------------------------| +| **LocalStack.Client v1.x** | [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dnuget%26track%3D1%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) | [![Github v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dgithub%26track%3D1%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client) | +| **LocalStack.Client v2.x** | [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client/) | [![Github v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client%3Fsource%3Dgithub%26track%3D2%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client) | +| **LocalStack.Client.Extensions v1.x** | [![NuGet v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dnuget%26track%3D1%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![Github v1.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dgithub%26track%3D1%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client.Extensions) | +| **LocalStack.Client.Extensions v2.x** | [![NuGet v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2Fbadge%2Fpackages%2Flocalstack.client.extensions%3Fsource%3Dnuget%26track%3D2%26includeprerelease%3Dtrue%26label%3Dnuget)](https://www.nuget.org/packages/LocalStack.Client.Extensions/) | [![GitHub Packages v2.x](https://img.shields.io/endpoint?url=https%3A%2F%2Fyvfdbfas85.execute-api.eu-central-1.amazonaws.com%2Flive%2F%3Fpackage%3Dlocalstack.client.extensions%26source%3Dgithub%26track%3D2%26includeprerelease%3Dtrue%26label%3Dgithub)](https://github.com/localstack-dotnet/localstack-dotnet-client/pkgs/nuget/LocalStack.Client.Extensions) | ## Table of Contents @@ -33,20 +119,13 @@ Localstack.NET is an easy-to-use .NET client for [LocalStack](https://github.com 7. [Changelog](#changelog) 8. [License](#license) -## Supported Platforms - -- [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0) -- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) -- [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) -- [.NET 4.6.1 and Above](https://dotnet.microsoft.com/download/dotnet-framework) - ## Why LocalStack.NET Client? - **Consistent Client Configuration:** LocalStack.NET eliminates the need for manual endpoint configuration, providing a standardized and familiar approach to initializing clients. - **Adaptable Environment Transition:** Easily switch between LocalStack and actual AWS services with minimal configuration changes. -- **Versatile .NET Compatibility:** Supports a broad spectrum of .NET versions, from .NET 7.0 and .NET Standard 2.0 to .NET Framework 4.6.1 and above. +- **Versatile .NET Compatibility:** Supports a broad spectrum of .NET versions, from .NET 9.0 and .NET Standard 2.0 to .NET Framework 4.6.2 and above. - **Reduced Learning Curve:** Offers a familiar interface tailored for LocalStack, ideal for developers acquainted with the AWS SDK for .NET. @@ -60,13 +139,36 @@ 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 + +To install the latest version of LocalStack.NET with AWS SDK v4 support, use the following command: ```bash -dotnet add package LocalStack.Client -dotnet add package LocalStack.Client.Extensions +# Install v2.0.0 with AWS SDK v4 support +dotnet add package LocalStack.Client --version 2.0.0 +dotnet add package LocalStack.Client.Extensions --version 2.0.0 ``` +#### 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). @@ -129,7 +231,9 @@ The `RegionName` is important as LocalStack creates resources based on the speci ## Known Issues -- **LocalStack Versions v2.0.1 - v2.2:** In versions v2.0.1 through v2.2 of LocalStack, the URL routing logic was changed, causing issues with SQS and S3 operations. Two issues were opened in LocalStack regarding this: [issue #8928](https://github.com/localstack/localstack/issues/8928) and [issue #8924](https://github.com/localstack/localstack/issues/8924). LocalStack addressed this problem with [PR #8962](https://github.com/localstack/localstack/pull/8962). Therefore, when using LocalStack.NET, either use version v2.0 of LocalStack (there are no issues with the v1 series as well) or the upcoming v2.3 version, or use the latest container from Docker Hub. +- **SNS with LocalStack v3.7.2 and v3.8.0:** During development on the new version, it was discovered that SNS functional tests are not working in LocalStack versions v3.7.2 and v3.8.0. This issue was reported in LocalStack [issue #11652](https://github.com/localstack/localstack/issues/11652). The LocalStack team identified a bug related to handling SNS URIs and resolved it in [PR #11653](https://github.com/localstack/localstack/pull/11653). The fix will be included in an upcoming release of LocalStack. In the meantime, if you're using SNS, it is recommended to stick to version v3.7.1 of LocalStack until the fix is available. + +- **LocalStack Versions v2.0.1 - v2.2:** In versions v2.0.1 through v2.2 of LocalStack, the URL routing logic was changed, causing issues with SQS and S3 operations. Two issues were opened in LocalStack regarding this: [issue #8928](https://github.com/localstack/localstack/issues/8928) and [issue #8924](https://github.com/localstack/localstack/issues/8924). LocalStack addressed this problem with [PR #8962](https://github.com/localstack/localstack/pull/8962). Therefore, when using LocalStack.NET, either use version v2.0 of LocalStack (there are no issues with the v1 series as well) or the upcoming v2.3 version, or use the latest v3 series container from Docker Hub. - **AWS_SERVICE_URL Environment Variable:** Unexpected behaviors might occur in LocalStack.NET when the `AWS_SERVICE_URL` environment variable is set. This environment variable is typically set by LocalStack in the container when using AWS Lambda, and AWS also uses this environment variable in the live environment. Soon, just like in LocalStack's official Python library, this environment variable will be prioritized by LocalStack.NET when configuring the LocalStack host, and there will be a general simplification in the configuration. You can follow this in the issues [issue #27](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/27) and [issue #32](https://github.com/localstack-dotnet/localstack-dotnet-client/issues/32). You set the `AWS_SERVICE_URL` to empty string until this issue is resolved. @@ -139,6 +243,8 @@ Environment.SetEnvironmentVariable("AWS_SERVICE_URL", string.Empty); - **IAmazonLambda Operations:** There's a general issue with `IAmazonLambda` operations. This matter is currently under investigation. +- **AWSSDK.SQS Compatibility:** Starting from version `3.7.300.*` of `AWSSDK.SQS`, there are compatibility issues with LocalStack v1 and v2 series versions. The [v3](https://hub.docker.com/r/localstack/localstack/tags?page=&page_size=&ordering=&name=3.4) series of LocalStack does not have these issues. Therefore, it is recommended to either update your LocalStack container to the v3 series or downgrade your `AWSSDK.SQS` to version `3.7.200.*` if you are using LocalStack v1 or v2 series containers. It is important to note that this is not a problem related to LocalStack.NET, but rather an issue with the LocalStack container and the AWS SDK for .NET. + ## Developing We appreciate contributions in the form of feedback, bug reports, and pull requests. @@ -149,13 +255,13 @@ To build the project, use the following commands based on your operating system: Windows -``` +```powershell build.ps1 ``` Linux -``` +```bash ./build.sh ``` @@ -169,13 +275,13 @@ To execute the tests, use the commands below: Windows -``` +```powershell build.ps1 --target=tests ``` Linux -``` +```bash ./build.sh --target=tests ``` diff --git a/src/LocalStack.Client/Session.cs b/src/LocalStack.Client/Session.cs index 56523ec..72a5c5a 100644 --- a/src/LocalStack.Client/Session.cs +++ b/src/LocalStack.Client/Session.cs @@ -15,6 +15,10 @@ public Session(ISessionOptions sessionOptions, IConfig config, ISessionReflectio _sessionReflection = sessionReflection; } +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Uses Activator/CreateInstance and private‑field reflection; not safe for Native AOT."), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif public TClient CreateClientByImplementation(bool useServiceUrl = false) where TClient : AmazonServiceClient { Type clientType = typeof(TClient); @@ -22,7 +26,16 @@ public TClient CreateClientByImplementation(bool useServiceUrl = false) return (TClient)CreateClientByImplementation(clientType, useServiceUrl); } - public AmazonServiceClient CreateClientByImplementation(Type implType, bool useServiceUrl = false) +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Uses Activator/CreateInstance and private‑field reflection; not safe for Native AOT."), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif + public AmazonServiceClient CreateClientByImplementation( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicFields)] +#endif + Type implType, + bool useServiceUrl = false) { if (!useServiceUrl && string.IsNullOrWhiteSpace(_sessionOptions.RegionName)) { @@ -55,6 +68,10 @@ public AmazonServiceClient CreateClientByImplementation(Type implType, bool useS return clientInstance; } +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Uses Activator/CreateInstance and private‑field reflection; not safe for Native AOT."), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif public AmazonServiceClient CreateClientByInterface(bool useServiceUrl = false) where TClient : IAmazonService { Type serviceInterfaceType = typeof(TClient); @@ -62,7 +79,12 @@ public AmazonServiceClient CreateClientByInterface(bool useServiceUrl = return CreateClientByInterface(serviceInterfaceType, useServiceUrl); } - public AmazonServiceClient CreateClientByInterface(Type serviceInterfaceType, bool useServiceUrl = false) + public AmazonServiceClient CreateClientByInterface( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicFields)] +#endif + Type serviceInterfaceType, + bool useServiceUrl = false) { if (serviceInterfaceType == null) { diff --git a/src/LocalStack.Client/Utils/SessionReflection.cs b/src/LocalStack.Client/Utils/SessionReflection.cs index aee901f..7f5b748 100644 --- a/src/LocalStack.Client/Utils/SessionReflection.cs +++ b/src/LocalStack.Client/Utils/SessionReflection.cs @@ -10,7 +10,16 @@ public IServiceMetadata ExtractServiceMetadata() where TClient : Amazon return ExtractServiceMetadata(clientType); } - public IServiceMetadata ExtractServiceMetadata(Type clientType) +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Accesses private field 'serviceMetadata' with reflection; not safe for Native AOT."), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif + public IServiceMetadata ExtractServiceMetadata( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields)] +#endif + Type clientType + ) { if (clientType == null) { @@ -33,7 +42,17 @@ public ClientConfig CreateClientConfig() where TClient : AmazonServiceC return CreateClientConfig(clientType); } - public ClientConfig CreateClientConfig(Type clientType) +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Uses Activator.CreateInstance on derived ClientConfig types; not safe for Native AOT."), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif + public ClientConfig CreateClientConfig( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type clientType + + ) { if (clientType == null) { @@ -46,7 +65,16 @@ public ClientConfig CreateClientConfig(Type clientType) return (ClientConfig)Activator.CreateInstance(clientConfigParam.ParameterType); } - public void SetClientRegion(AmazonServiceClient amazonServiceClient, string systemName) +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Reflects over Config.RegionEndpoint property; not safe for Native AOT."), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif + public void SetClientRegion( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + AmazonServiceClient amazonServiceClient, + string systemName) { if (amazonServiceClient == null) { @@ -59,7 +87,16 @@ public void SetClientRegion(AmazonServiceClient amazonServiceClient, string syst regionEndpointProperty?.SetValue(amazonServiceClient.Config, RegionEndpoint.GetBySystemName(systemName)); } - public bool SetForcePathStyle(ClientConfig clientConfig, bool value = true) +#if NET8_0_OR_GREATER + [RequiresDynamicCode("Reflects over ForcePathStyle property; not safe for Native AOT."), + RequiresUnreferencedCode("Reflection may break when IL trimming removes private members. We’re migrating to a source‑generated path in vNext.")] +#endif + public bool SetForcePathStyle( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + ClientConfig clientConfig, + bool value = true) { if (clientConfig == null) { diff --git a/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs b/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs index 93a06bf..936e416 100644 --- a/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs +++ b/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs @@ -2,7 +2,7 @@ public class AwsClientFactoryWrapperTests { - private readonly IAwsClientFactoryWrapper _awsClientFactoryWrapper; + private readonly AwsClientFactoryWrapper _awsClientFactoryWrapper; private readonly Mock _mockServiceProvider; private readonly AWSOptions _awsOptions; @@ -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/Extensions/ObjectExtensions.cs b/tests/LocalStack.Client.Extensions.Tests/Extensions/ObjectExtensions.cs index 02ec093..edb9db1 100644 --- a/tests/LocalStack.Client.Extensions.Tests/Extensions/ObjectExtensions.cs +++ b/tests/LocalStack.Client.Extensions.Tests/Extensions/ObjectExtensions.cs @@ -1,4 +1,6 @@ -ο»Ώnamespace LocalStack.Client.Extensions.Tests.Extensions; +ο»Ώ#pragma warning disable CA1515 // Because an application's API isn't typically referenced from outside the assembly, types can be made internal + +namespace LocalStack.Client.Extensions.Tests.Extensions; public static class ObjectExtensions { @@ -9,4 +11,4 @@ public static bool DeepEquals(this object obj1, object obj2) return string.Equals(obj1Ser, obj2Ser, StringComparison.Ordinal); } -} +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Extensions.Tests/LocalStack.Client.Extensions.Tests.csproj b/tests/LocalStack.Client.Extensions.Tests/LocalStack.Client.Extensions.Tests.csproj index e3e2886..c6a3b13 100644 --- a/tests/LocalStack.Client.Extensions.Tests/LocalStack.Client.Extensions.Tests.csproj +++ b/tests/LocalStack.Client.Extensions.Tests/LocalStack.Client.Extensions.Tests.csproj @@ -1,7 +1,7 @@ ο»Ώ - net6.0;net7.0 + net8.0;net9.0 $(NoWarn);CA1707;MA0006 @@ -9,7 +9,7 @@ - + @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers - + 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/AWSProvisioningException.cs b/tests/LocalStack.Client.Functional.Tests/CloudFormation/AWSProvisioningException.cs new file mode 100644 index 0000000..e0230a0 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/CloudFormation/AWSProvisioningException.cs @@ -0,0 +1,16 @@ +ο»Ώnamespace LocalStack.Client.Functional.Tests.CloudFormation; + +public class AwsProvisioningException : Exception +{ + public AwsProvisioningException() : base() + { + } + + public AwsProvisioningException(string message) : base(message) + { + } + + public AwsProvisioningException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationProvisioner.cs b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationProvisioner.cs new file mode 100644 index 0000000..4798507 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationProvisioner.cs @@ -0,0 +1,91 @@ +ο»Ώ#pragma warning disable CA1031 + +namespace LocalStack.Client.Functional.Tests.CloudFormation; + +public sealed class CloudFormationProvisioner +{ + private readonly IAmazonCloudFormation _amazonCloudFormation; + private readonly ILogger _logger; + + public CloudFormationProvisioner(IAmazonCloudFormation amazonCloudFormation, ILogger logger) + { + _amazonCloudFormation = amazonCloudFormation; + _logger = logger; + } + + internal async Task ConfigureCloudFormationAsync(CloudFormationResource resource, CancellationToken cancellationToken = default) + { + await ProcessCloudFormationStackResourceAsync(resource, cancellationToken).ConfigureAwait(false); + await ProcessCloudFormationTemplateResourceAsync(resource, cancellationToken).ConfigureAwait(false); + } + + private async Task ProcessCloudFormationStackResourceAsync(CloudFormationResource resource, CancellationToken cancellationToken = default) + { + try + { + var request = new DescribeStacksRequest { StackName = resource.Name }; + DescribeStacksResponse? response = await _amazonCloudFormation.DescribeStacksAsync(request, cancellationToken).ConfigureAwait(false); + + // If the stack didn't exist then a StackNotFoundException would have been thrown. + Stack? stack = response.Stacks[0]; + + // Capture the CloudFormation stack output parameters on to the Aspire CloudFormation resource. This + // allows projects that have a reference to the stack have the output parameters applied to the + // projects IConfiguration. + resource.Outputs = stack!.Outputs; + + resource.ProvisioningTaskCompletionSource?.TrySetResult(); + } + catch (Exception e) + { + if (e is AmazonCloudFormationException ce && string.Equals(ce.ErrorCode, "ValidationError", StringComparison.Ordinal)) + { + _logger.LogError("Stack {StackName} does not exists to add as a resource.", resource.Name); + } + else + { + _logger.LogError(e, "Error reading {StackName}.", resource.Name); + } + + resource.ProvisioningTaskCompletionSource?.TrySetException(e); + } + } + + private async Task ProcessCloudFormationTemplateResourceAsync(CloudFormationResource resource, CancellationToken cancellationToken = default) + { + try + { + var executor = new CloudFormationStackExecutor(_amazonCloudFormation, resource, _logger); + Stack? stack = await executor.ExecuteTemplateAsync(cancellationToken).ConfigureAwait(false); + + if (stack != null) + { + _logger.LogInformation("CloudFormation stack has {Count} output parameters", stack.Outputs.Count); + + if (_logger.IsEnabled(LogLevel.Information)) + { + foreach (Output? output in stack.Outputs) + { + _logger.LogInformation("Output Name: {Name}, Value {Value}", output.OutputKey, output.OutputValue); + } + } + + _logger.LogInformation("CloudFormation provisioning complete"); + + resource.Outputs = stack.Outputs; + resource.ProvisioningTaskCompletionSource?.TrySetResult(); + } + else + { + _logger.LogError("CloudFormation provisioning failed"); + + resource.ProvisioningTaskCompletionSource?.TrySetException(new AwsProvisioningException("Failed to apply CloudFormation template")); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error provisioning {ResourceName} CloudFormation resource", resource.Name); + resource.ProvisioningTaskCompletionSource?.TrySetException(ex); + } + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationResource.cs b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationResource.cs new file mode 100644 index 0000000..10b8a1c --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationResource.cs @@ -0,0 +1,38 @@ +ο»Ώnamespace LocalStack.Client.Functional.Tests.CloudFormation; + +internal sealed class CloudFormationResource : ICloudFormationResource +{ + public CloudFormationResource(string name, string templatePath) + { + Name = name; + TemplatePath = templatePath; + CloudFormationParameters = new Dictionary(StringComparer.Ordinal); + StackPollingInterval = 3; + DisabledCapabilities = new List(); + } + + public IDictionary CloudFormationParameters { get; } + + public string Name { get; } + + public string TemplatePath { get; } + + public string? RoleArn { get; set; } + + public int StackPollingInterval { get; set; } + + public bool DisableDiffCheck { get; set; } + + public IList DisabledCapabilities { get; } + + public IAmazonCloudFormation? CloudFormationClient { get; set; } + + public IList? Outputs { get; set; } + + public TaskCompletionSource? ProvisioningTaskCompletionSource { get; set; } + + public void AddParameter(string parameterName, string parameterValue) + { + CloudFormationParameters[parameterName] = parameterValue; + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationStackExecutor.cs b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationStackExecutor.cs new file mode 100644 index 0000000..d17323e --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/CloudFormation/CloudFormationStackExecutor.cs @@ -0,0 +1,519 @@ +ο»Ώ#pragma warning disable CA2254 + +using Tag = Amazon.CloudFormation.Model.Tag; + +namespace LocalStack.Client.Functional.Tests.CloudFormation; + +internal sealed class CloudFormationStackExecutor(IAmazonCloudFormation cloudFormationClient, CloudFormationResource cloudFormationResource, ILogger logger) +{ + // Name of the Tag for the stack to store the SHA256 of the CloudFormation template + const string SHA256_TAG = "LocalStackAppHost_SHA256"; + + // CloudFormation statuses for when the stack is in transition all end with IN_PROGRESS + const string IN_PROGRESS_SUFFIX = "IN_PROGRESS"; + + // Polling interval for checking status of CloudFormation stack when creating or updating the stack. + TimeSpan StackPollingDelay { get; } = TimeSpan.FromSeconds(cloudFormationResource.StackPollingInterval); + + /// + /// Using the template and configuration from the CloudFormationResource create or update + /// the CloudFormation Stack. + /// + /// If a null is returned instead of the stack that implies the stack failed to be created or updated. + /// + /// A . + /// + internal async Task ExecuteTemplateAsync(CancellationToken cancellationToken = default) + { + Stack? existingStack = await FindStackAsync().ConfigureAwait(false); + ChangeSetType changeSetType = await DetermineChangeSetTypeAsync(existingStack, cancellationToken).ConfigureAwait(false); + + string templateBody = await File.ReadAllTextAsync(cloudFormationResource.TemplatePath, cancellationToken); + string computedSha256 = ComputeSha256(templateBody, cloudFormationResource.CloudFormationParameters); + + (List tags, string? existingSha256) = SetupTags(existingStack, changeSetType, computedSha256); + + // Check to see if the template hasn't change. If it hasn't short circuit out. + if (!cloudFormationResource.DisableDiffCheck && string.Equals(computedSha256, existingSha256, StringComparison.Ordinal)) + { + logger.LogInformation("CloudFormation Template for CloudFormation stack {StackName} has not changed", cloudFormationResource.Name); + + return existingStack; + } + + List templateParameters = SetupTemplateParameters(); + + string? changeSetId = await CreateChangeSetAsync(changeSetType, templateParameters, templateBody, tags, cancellationToken).ConfigureAwait(false); + + if (changeSetId == null) + { + return existingStack; + } + + Stack updatedStack = await ExecuteChangeSetAsync(changeSetId, changeSetType, cancellationToken).ConfigureAwait(false); + + return IsStackInErrorState(updatedStack) ? null : updatedStack; + } + + /// + /// Setup the tags collection by coping the any tags for existing stacks and then updating/adding the tag recording + /// the SHA256 for template and parameters. + /// + /// + /// + /// + /// + private static (List tags, string? existingSha256) SetupTags(Stack? existingStack, ChangeSetType changeSetType, string computedSha256) + { + string? existingSha256 = null; + var tags = new List(); + + if (changeSetType == ChangeSetType.UPDATE && existingStack != null) + { + tags = existingStack.Tags ?? new List(); + } + + Tag? shaTag = tags.Find(x => string.Equals(x.Key, SHA256_TAG, StringComparison.Ordinal)); + + if (shaTag != null) + { + existingSha256 = shaTag.Value; + shaTag.Value = computedSha256; + } + else + { + tags.Add(new Tag { Key = SHA256_TAG, Value = computedSha256 }); + } + + return (tags, existingSha256); + } + + /// + /// Setup the template parameters from the CloudFormationResource to how the SDK expects parameters. + /// + /// + private List SetupTemplateParameters() + { + var templateParameters = new List(); + + foreach (KeyValuePair kvp in cloudFormationResource.CloudFormationParameters) + { + templateParameters.Add(new Parameter { ParameterKey = kvp.Key, ParameterValue = kvp.Value }); + } + + return templateParameters; + } + + /// + /// Create the CloudFormation change set. + /// + /// + /// + /// + /// + /// A . + /// + /// + private async Task CreateChangeSetAsync(ChangeSetType changeSetType, List templateParameters, string templateBody, List tags, + CancellationToken cancellationToken) + { + CreateChangeSetResponse changeSetResponse; + + try + { + logger.LogInformation("Creating CloudFormation change set."); + var capabilities = new List(); + + if (cloudFormationResource.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_IAM", StringComparison.OrdinalIgnoreCase)) == null) + { + capabilities.Add("CAPABILITY_IAM"); + } + + if (cloudFormationResource.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_NAMED_IAM", StringComparison.OrdinalIgnoreCase)) == null) + { + capabilities.Add("CAPABILITY_NAMED_IAM"); + } + + if (cloudFormationResource.DisabledCapabilities.FirstOrDefault(x => string.Equals(x, "CAPABILITY_AUTO_EXPAND", StringComparison.OrdinalIgnoreCase)) == null) + { + capabilities.Add("CAPABILITY_AUTO_EXPAND"); + } + + var changeSetRequest = new CreateChangeSetRequest + { + StackName = cloudFormationResource.Name, + Parameters = templateParameters, + + // Change set name needs to be unique. Since the changeset isn't be created directly by the user the name isn't really important. + ChangeSetName = "LocalStack-AppHost-" + DateTime.UtcNow.Ticks, + ChangeSetType = changeSetType, + Capabilities = capabilities, + RoleARN = cloudFormationResource.RoleArn, + TemplateBody = templateBody, + Tags = tags + }; + + changeSetResponse = await cloudFormationClient.CreateChangeSetAsync(changeSetRequest, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + throw new AwsProvisioningException($"Error creating change set: {e.Message}", e); + } + + // The change set can take a few seconds to be reviewed and be ready to be executed. + if (await WaitForChangeSetBeingAvailableAsync(changeSetResponse.Id, cancellationToken).ConfigureAwait(false)) + { + return changeSetResponse.Id; + } + + return null; + } + + /// + /// Once the change set is created execute the change set to apply the changes to the stack. + /// + /// + /// + /// A . + /// + /// + private async Task ExecuteChangeSetAsync(string changeSetId, ChangeSetType changeSetType, CancellationToken cancellationToken) + { + var executeChangeSetRequest = new ExecuteChangeSetRequest { StackName = cloudFormationResource.Name, ChangeSetName = changeSetId }; + + DateTimeOffset timeChangeSetExecuted = DateTimeOffset.UtcNow; + + try + { + logger.LogInformation("Executing CloudFormation change set"); + + // Execute the change set. + await cloudFormationClient.ExecuteChangeSetAsync(executeChangeSetRequest, cancellationToken).ConfigureAwait(false); + + if (changeSetType == ChangeSetType.CREATE) + { + logger.LogInformation("Initiated CloudFormation stack creation for {Name}", cloudFormationResource.Name); + } + else + { + logger.LogInformation("Initiated CloudFormation stack update on {Name}", cloudFormationResource.Name); + } + } + catch (Exception e) + { + throw new AwsProvisioningException($"Error executing CloudFormation change set: {e.Message}", e); + } + + return await WaitStackToCompleteAsync(timeChangeSetExecuted, cancellationToken).ConfigureAwait(false); + } + + /// + /// Determine the type of change set to create (CREATE or UPDATE). If the stack is in an incomplete state + /// wait or delete the stack till it is in a ready state. + /// + /// + /// A . + /// + /// + private async Task DetermineChangeSetTypeAsync(Stack? stack, CancellationToken cancellationToken) + { + ChangeSetType changeSetType; + + if (stack == null || stack.StackStatus == StackStatus.REVIEW_IN_PROGRESS || stack.StackStatus == StackStatus.DELETE_COMPLETE) + { + changeSetType = ChangeSetType.CREATE; + } + + // If the status was ROLLBACK_COMPLETE that means the stack failed on initial creation + // and the resources were cleaned up. It is safe to delete the stack so we can recreate it. + else if (stack.StackStatus == StackStatus.ROLLBACK_COMPLETE) + { + await DeleteRollbackCompleteStackAsync(stack, cancellationToken).ConfigureAwait(false); + changeSetType = ChangeSetType.CREATE; + } + + // If the status was ROLLBACK_IN_PROGRESS that means the initial creation is failing. + // Wait to see if it goes into ROLLBACK_COMPLETE status meaning everything got cleaned up and then delete it. + else if (stack.StackStatus == StackStatus.ROLLBACK_IN_PROGRESS) + { + stack = await WaitForNoLongerInProgressAsync(cancellationToken).ConfigureAwait(false); + + if (stack != null && stack.StackStatus == StackStatus.ROLLBACK_COMPLETE) + { + await DeleteRollbackCompleteStackAsync(stack, cancellationToken).ConfigureAwait(false); + } + + changeSetType = ChangeSetType.CREATE; + } + + // 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); + changeSetType = ChangeSetType.CREATE; + } + + // The Stack state is in a normal state and ready to be updated. + else if (stack.StackStatus == StackStatus.CREATE_COMPLETE || + stack.StackStatus == StackStatus.UPDATE_COMPLETE || + stack.StackStatus == StackStatus.UPDATE_ROLLBACK_COMPLETE) + { + changeSetType = ChangeSetType.UPDATE; + } + + // All other states means the Stack is in an inconsistent state. + else + { + throw new AwsProvisioningException($"The stack's current state of {stack.StackStatus} is invalid for updating"); + } + + return changeSetType; + } + + /// + /// Check to see if the state of the stack indicates the executed change set failed. + /// + /// + /// + private static bool IsStackInErrorState(Stack stack) + { + if (stack.StackStatus.ToString(CultureInfo.InvariantCulture).EndsWith("FAILED", StringComparison.OrdinalIgnoreCase) || + stack.StackStatus.ToString(CultureInfo.InvariantCulture).EndsWith("ROLLBACK_COMPLETE", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + /// + /// If a stack is in the ROLLBACK_COMPLETE if failed during initial creation. There are no resources + /// left in the stack and it is safe to delete. If the stack is not deleted the recreation of the stack will fail. + /// + /// + /// A . + /// + /// + private async Task DeleteRollbackCompleteStackAsync(Stack stack, CancellationToken cancellation) + { + try + { + if (stack.StackStatus == StackStatus.ROLLBACK_COMPLETE) + { + await cloudFormationClient.DeleteStackAsync(new DeleteStackRequest { StackName = stack.StackName }, cancellation).ConfigureAwait(false); + } + + await WaitForNoLongerInProgressAsync(cancellation).ConfigureAwait(false); + } + catch (Exception e) + { + throw new AwsProvisioningException($"Error removing previous failed stack creation {stack.StackName}: {e.Message}", e); + } + } + + /// + /// Wait till the stack transitions from an in progress state to a stable state. + /// + /// A . + /// + /// + private async Task WaitForNoLongerInProgressAsync(CancellationToken cancellation) + { + try + { + long start = DateTime.UtcNow.Ticks; + Stack? currentStack = null; + + do + { + if (currentStack != null) + { + logger.LogInformation( + "... 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); + currentStack = await FindStackAsync().ConfigureAwait(false); + } while (currentStack != null && currentStack.StackStatus.ToString(CultureInfo.InvariantCulture).EndsWith(IN_PROGRESS_SUFFIX, StringComparison.Ordinal)); + + return currentStack; + } + catch (Exception e) + { + throw new AwsProvisioningException($"Error waiting for stack state change: {e.Message}", e); + } + } + + /// + /// Wait for the change set to be created and in success state to begin executing. + /// + /// + /// A . + /// + /// + private async Task WaitForChangeSetBeingAvailableAsync(string changeSetId, CancellationToken cancellation) + { + try + { + var request = new DescribeChangeSetRequest { ChangeSetName = changeSetId }; + + logger.LogInformation("... Waiting for change set to be reviewed"); + DescribeChangeSetResponse response; + + do + { + await Task.Delay(this.StackPollingDelay, cancellation).ConfigureAwait(false); + response = await cloudFormationClient.DescribeChangeSetAsync(request, cancellation).ConfigureAwait(false); + } while (response.Status == ChangeSetStatus.CREATE_IN_PROGRESS || response.Status == ChangeSetStatus.CREATE_PENDING); + + if (response.Status != ChangeSetStatus.FAILED) + { + return true; + } + + // There is no code returned from CloudFormation to tell if failed because there is no changes so + // the status reason has to be check for the string. + if (response.StatusReason?.Contains("The submitted information didn't contain changes", StringComparison.InvariantCultureIgnoreCase) != true) + { + throw new AwsProvisioningException($"Failed to create CloudFormation change set: {response.StatusReason}"); + } + + logger.LogInformation("No changes detected for change set"); + + return false; + } + catch (Exception e) + { + throw new AwsProvisioningException($"Error getting status of change set: {e.Message}", e); + } + } + + /// + /// Wait for the CloudFormation stack to get to a stable state after creating or updating the stack. + /// + /// Minimum timestamp for events. + /// A . + /// + private async Task WaitStackToCompleteAsync(DateTimeOffset minTimeStampForEvents, CancellationToken cancellationToken) + { + const int TIMESTAMP_WIDTH = 20; + const int LOGICAL_RESOURCE_WIDTH = 40; + const int RESOURCE_STATUS = 40; + var mostRecentEventId = string.Empty; + + var waitingMessage = $"... Waiting for CloudFormation stack {cloudFormationResource.Name} to be ready"; + logger.LogInformation(waitingMessage); + logger.LogInformation(new string('-', waitingMessage.Length)); + + Stack stack; + + do + { + await Task.Delay(StackPollingDelay, cancellationToken).ConfigureAwait(false); + + // If we are in the WaitStackToCompleteAsync then we already know the stack exists. + stack = (await FindStackAsync().ConfigureAwait(false))!; + + List events = await GetLatestEventsAsync(minTimeStampForEvents, mostRecentEventId, cancellationToken).ConfigureAwait(false); + + if (events.Count > 0) + { + mostRecentEventId = events[0].EventId; + } + + 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(' '); + line.Append(events[i].LogicalResourceId.PadRight(LOGICAL_RESOURCE_WIDTH)); + line.Append(' '); + line.Append(events[i].ResourceStatus.ToString(CultureInfo.InvariantCulture).PadRight(RESOURCE_STATUS)); + + if (!events[i].ResourceStatus.ToString(CultureInfo.InvariantCulture).EndsWith(IN_PROGRESS_SUFFIX, StringComparison.Ordinal) && + !string.IsNullOrEmpty(events[i].ResourceStatusReason)) + { + line.Append(' '); + line.Append(events[i].ResourceStatusReason); + } + + if (minTimeStampForEvents < events[i].Timestamp && events[i].Timestamp != null) + { + minTimeStampForEvents = (DateTimeOffset)events[i].Timestamp!; + } + + logger.LogInformation(line.ToString()); + } + } while (!cancellationToken.IsCancellationRequested && + stack.StackStatus.ToString(CultureInfo.InvariantCulture).EndsWith(IN_PROGRESS_SUFFIX, StringComparison.Ordinal)); + + return stack; + } + + private async Task> GetLatestEventsAsync(DateTimeOffset minTimeStampForEvents, string mostRecentEventId, CancellationToken cancellationToken) + { + var noNewEvents = false; + var events = new List(); + DescribeStackEventsResponse? response = null; + + do + { + var request = new DescribeStackEventsRequest() { StackName = cloudFormationResource.Name }; + + if (response != null) + { + request.NextToken = response.NextToken; + } + + try + { + response = await cloudFormationClient.DescribeStackEventsAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + throw new AwsProvisioningException($"Error getting events for CloudFormation stack: {e.Message}", e); + } + + foreach (StackEvent? evnt in response.StackEvents) + { + if (string.Equals(evnt.EventId, mostRecentEventId, StringComparison.Ordinal) || evnt.Timestamp < minTimeStampForEvents) + { + noNewEvents = true; + + break; + } + + events.Add(evnt); + } + } while (!noNewEvents && !string.IsNullOrEmpty(response.NextToken)); + + return events; + } + + private static string ComputeSha256(string templateBody, IDictionary parameters) + { + string content = templateBody; + + if (parameters != null) + { + content += string.Join(';', parameters.Select(x => x.Key + "=" + x.Value).ToArray()); + } + + byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content)); + + return Convert.ToHexString(bytes).ToUpperInvariant(); + } + + private async Task FindStackAsync() + { + await foreach (Stack? stack in cloudFormationClient.Paginators.DescribeStacks(new DescribeStacksRequest()).Stacks.ConfigureAwait(false)) + { + if (string.Equals(cloudFormationResource.Name, stack.StackName, StringComparison.Ordinal)) + { + return stack; + } + } + + return null; + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/CloudFormation/Constants.cs b/tests/LocalStack.Client.Functional.Tests/CloudFormation/Constants.cs new file mode 100644 index 0000000..378935b --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/CloudFormation/Constants.cs @@ -0,0 +1,19 @@ +ο»Ώnamespace LocalStack.Client.Functional.Tests.CloudFormation; + +internal static class Constants +{ + /// + /// Error state for Aspire resource dashboard + /// + public const string ResourceStateFailedToStart = "FailedToStart"; + + /// + /// In progress state for Aspire resource dashboard + /// + public const string ResourceStateStarting = "Starting"; + + /// + /// Success state for Aspire resource dashboard + /// + public const string ResourceStateRunning = "Running"; +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/CloudFormation/ICloudFormationResource.cs b/tests/LocalStack.Client.Functional.Tests/CloudFormation/ICloudFormationResource.cs new file mode 100644 index 0000000..5d03be3 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/CloudFormation/ICloudFormationResource.cs @@ -0,0 +1,22 @@ +ο»Ώnamespace LocalStack.Client.Functional.Tests.CloudFormation; + +public interface ICloudFormationResource +{ + string TemplatePath { get; } + + void AddParameter(string parameterName, string parameterValue); + + string? RoleArn { get; set; } + + int StackPollingInterval { get; set; } + + bool DisableDiffCheck { get; set; } + + IList DisabledCapabilities { get; } + + IAmazonCloudFormation? CloudFormationClient { get; set; } + + IList? Outputs { get; } + + TaskCompletionSource? ProvisioningTaskCompletionSource { get; set; } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackCollections.cs b/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackCollections.cs index 7fecfe1..94888ef 100644 --- a/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackCollections.cs +++ b/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackCollections.cs @@ -2,22 +2,8 @@ namespace LocalStack.Client.Functional.Tests.Fixtures; -[CollectionDefinition(nameof(LocalStackCollectionV131))] -public class LocalStackCollectionV131 : ICollectionFixture, ICollectionFixture -{ -} +[CollectionDefinition(nameof(LocalStackCollectionV37))] +public class LocalStackCollectionV37 : ICollectionFixture, ICollectionFixture; -[CollectionDefinition(nameof(LocalStackCollectionV20))] -public class LocalStackCollectionV20 : ICollectionFixture, ICollectionFixture -{ -} - -[CollectionDefinition(nameof(LocalStackCollectionV22))] -public class LocalStackCollectionV22 : ICollectionFixture, ICollectionFixture -{ -} - -[CollectionDefinition(nameof(LocalStackLegacyCollection))] -public class LocalStackLegacyCollection : ICollectionFixture, ICollectionFixture -{ -} \ No newline at end of file +[CollectionDefinition(nameof(LocalStackCollectionV46))] +public class LocalStackCollectionV46 : ICollectionFixture, ICollectionFixture; \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackFixtures.cs b/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackFixtures.cs index 736d6d7..56959a4 100644 --- a/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackFixtures.cs +++ b/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackFixtures.cs @@ -15,39 +15,25 @@ protected LocalStackFixtureBase(LocalStackBuilder localStackBuilder) public async Task InitializeAsync() { - await LocalStackContainer.StartAsync().ConfigureAwait(false); + await LocalStackContainer.StartAsync(); } public async Task DisposeAsync() { - await LocalStackContainer.StopAsync().ConfigureAwait(false); + await LocalStackContainer.StopAsync(); } } -public sealed class LocalStackFixtureV131 : LocalStackFixtureBase +public sealed class LocalStackFixtureV37 : LocalStackFixtureBase { - public LocalStackFixtureV131() : base(TestContainers.LocalStackBuilder(TestConstants.LocalStackV13)) + public LocalStackFixtureV37() : base(TestContainers.LocalStackBuilder(TestConstants.LocalStackV37)) { } } -public sealed class LocalStackFixtureV20 : LocalStackFixtureBase +public sealed class LocalStackFixtureV46 : LocalStackFixtureBase { - public LocalStackFixtureV20() : base(TestContainers.LocalStackBuilder(TestConstants.LocalStackV20)) - { - } -} - -public sealed class LocalStackFixtureV22 : LocalStackFixtureBase -{ - public LocalStackFixtureV22() : base(TestContainers.LocalStackBuilder(TestConstants.LocalStackV22)) - { - } -} - -public class LocalStackLegacyFixture : LocalStackFixtureBase -{ - public LocalStackLegacyFixture() : base(TestContainers.LocalStackLegacyBuilder) + public LocalStackFixtureV46() : base(TestContainers.LocalStackBuilder(TestConstants.LocalStackV46)) { } } diff --git a/tests/LocalStack.Client.Functional.Tests/Fixtures/TestFixture.cs b/tests/LocalStack.Client.Functional.Tests/Fixtures/TestFixture.cs index 696feda..88e646d 100644 --- a/tests/LocalStack.Client.Functional.Tests/Fixtures/TestFixture.cs +++ b/tests/LocalStack.Client.Functional.Tests/Fixtures/TestFixture.cs @@ -1,4 +1,5 @@ ο»Ώ#pragma warning disable CA1822 // Mark members as static - disabled because of readability +#pragma warning disable S2325 // Methods and properties that don't access instance data should be static - disabled because of readability namespace LocalStack.Client.Functional.Tests.Fixtures; public class TestFixture diff --git a/tests/LocalStack.Client.Functional.Tests/GlobalUsings.cs b/tests/LocalStack.Client.Functional.Tests/GlobalUsings.cs index b229817..337ce58 100644 --- a/tests/LocalStack.Client.Functional.Tests/GlobalUsings.cs +++ b/tests/LocalStack.Client.Functional.Tests/GlobalUsings.cs @@ -6,12 +6,17 @@ global using System.IO; global using System.Linq; global using System.Net; +global using System.Security.Cryptography; +global using System.Text; +global using System.Threading; global using System.Threading.Tasks; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Amazon; +global using Amazon.CloudFormation; +global using Amazon.CloudFormation.Model; global using Amazon.DynamoDBv2; global using Amazon.DynamoDBv2.DataModel; global using Amazon.DynamoDBv2.DocumentModel; @@ -35,11 +40,14 @@ global using LocalStack.Client.Enums; global using LocalStack.Client.Contracts; global using LocalStack.Client.Extensions.Tests.Extensions; +global using LocalStack.Client.Functional.Tests.CloudFormation; global using LocalStack.Client.Functional.Tests.Fixtures; global using LocalStack.Client.Functional.Tests.Scenarios.DynamoDb.Entities; global using LocalStack.Client.Functional.Tests.Scenarios.SQS.Models; global using LocalStack.Client.Functional.Tests.Scenarios.SNS.Models; +global using Microsoft.Extensions.Logging; + global using Testcontainers.LocalStack; global using Xunit; diff --git a/tests/LocalStack.Client.Functional.Tests/LocalStack.Client.Functional.Tests.csproj b/tests/LocalStack.Client.Functional.Tests/LocalStack.Client.Functional.Tests.csproj index 09bbbf1..171edcb 100644 --- a/tests/LocalStack.Client.Functional.Tests/LocalStack.Client.Functional.Tests.csproj +++ b/tests/LocalStack.Client.Functional.Tests/LocalStack.Client.Functional.Tests.csproj @@ -1,69 +1,78 @@ ο»Ώ - - net6.0;net7.0 - latest - $(NoWarn);CA1707;MA0006;CA1711 - + + net8.0;net9.0 + latest + $(NoWarn);CA1707;MA0006;MA0004;CA1711;CA2007;MA0132;CA1848;CA2254;S4144;CA1515 + + + + - - - PreserveNewest - - - PreserveNewest - appsettings.json - - + + + PreserveNewest + + + PreserveNewest + appsettings.json + + + Always + + - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - + + + + + + + + + + + - - - - - + + - - - Always - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + - - NETCOREAPP - + - + + + + + + + + + Always + + + + + NETCOREAPP + + + \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/BaseScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/BaseScenario.cs index ff749f0..0f45c02 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/BaseScenario.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/BaseScenario.cs @@ -16,11 +16,17 @@ protected BaseScenario(TestFixture testFixture, ILocalStackFixture localStackFix serviceCollection.AddAwsService(useServiceUrl: useServiceUrl) .AddAwsService(useServiceUrl: useServiceUrl) .AddAwsService(useServiceUrl: useServiceUrl) - .AddAwsService(useServiceUrl: useServiceUrl); + .AddAwsService(useServiceUrl: useServiceUrl) + .AddAwsService(useServiceUrl: useServiceUrl); + + serviceCollection.AddLogging(); ServiceProvider = serviceCollection.BuildServiceProvider(); + LocalStackFixture = localStackFixture; } + protected ILocalStackFixture LocalStackFixture { get; set; } + protected IConfiguration Configuration { get; set; } protected ServiceProvider ServiceProvider { get; private set; } diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/BaseCloudFormationScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/BaseCloudFormationScenario.cs new file mode 100644 index 0000000..ecc3f22 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/BaseCloudFormationScenario.cs @@ -0,0 +1,60 @@ +ο»Ώnamespace LocalStack.Client.Functional.Tests.Scenarios.CloudFormation; + +public abstract class BaseCloudFormationScenario : BaseScenario +{ + protected BaseCloudFormationScenario(TestFixture testFixture, ILocalStackFixture localStackFixture, string configFile = TestConstants.LocalStackConfig, + bool useServiceUrl = false) : base(testFixture, localStackFixture, configFile, useServiceUrl) + { + AmazonCloudFormation = ServiceProvider.GetRequiredService(); + AmazonSqs = ServiceProvider.GetRequiredService(); + AmazonSns = ServiceProvider.GetRequiredService(); + + var logger = ServiceProvider.GetRequiredService>(); + CloudFormationProvisioner = new CloudFormationProvisioner(AmazonCloudFormation, logger); + } + + protected IAmazonCloudFormation AmazonCloudFormation { get; private set; } + + protected IAmazonSQS AmazonSqs { get; private set; } + + protected IAmazonSimpleNotificationService AmazonSns { get; private set; } + + protected CloudFormationProvisioner CloudFormationProvisioner { get; private set; } + + [Fact] + public virtual async Task CloudFormationService_Should_Create_A_CloudFormation_Stack_Async() + { + var stackName = Guid.NewGuid().ToString(); + const string templatePath = "./Scenarios/CloudFormation/app-resources.template"; + + var cloudFormationResource = new CloudFormationResource(stackName, templatePath); + cloudFormationResource.AddParameter("DefaultVisibilityTimeout", "30"); + + await CloudFormationProvisioner.ConfigureCloudFormationAsync(cloudFormationResource); + + DescribeStacksResponse response = await AmazonCloudFormation.DescribeStacksAsync(new DescribeStacksRequest() { StackName = stackName }); + Stack? stack = response.Stacks[0]; + + Assert.NotNull(stack); + Assert.NotNull(cloudFormationResource.Outputs); + Assert.NotEmpty(cloudFormationResource.Outputs); + Assert.Equal(2, cloudFormationResource.Outputs.Count); + + string queueUrl = cloudFormationResource.Outputs.Single(output => output.OutputKey == "ChatMessagesQueueUrl").OutputValue; + string snsArn = cloudFormationResource.Outputs.Single(output => output.OutputKey == "ChatTopicArn").OutputValue; + + GetTopicAttributesResponse topicAttResponse = await AmazonSns.GetTopicAttributesAsync(snsArn); + + if (topicAttResponse.HttpStatusCode == HttpStatusCode.OK) + { + Assert.Equal(snsArn, topicAttResponse.Attributes["TopicArn"]); + } + + GetQueueAttributesResponse queueAttResponse = await AmazonSqs.GetQueueAttributesAsync(queueUrl, ["QueueArn"]); + + if (queueAttResponse.HttpStatusCode == HttpStatusCode.OK) + { + Assert.NotNull(queueAttResponse.Attributes["QueueArn"]); + } + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/CloudFormationScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/CloudFormationScenario.cs new file mode 100644 index 0000000..72c8d63 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/CloudFormationScenario.cs @@ -0,0 +1,19 @@ +ο»Ώ#pragma warning disable MA0048 // File name must match type name - disabled because of readability + +namespace LocalStack.Client.Functional.Tests.Scenarios.CloudFormation; + +[Collection(nameof(LocalStackCollectionV37))] +public sealed class CloudFormationScenarioV37 : BaseCloudFormationScenario +{ + public CloudFormationScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) + { + } +} + +[Collection(nameof(LocalStackCollectionV46))] +public sealed class CloudFormationScenarioV46 : BaseCloudFormationScenario +{ + public CloudFormationScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) + { + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/app-resources.template b/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/app-resources.template new file mode 100644 index 0000000..06a0b93 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/CloudFormation/app-resources.template @@ -0,0 +1,59 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Parameters" : { + "DefaultVisibilityTimeout" : { + "Type" : "Number", + "Description" : "The default visiblity timeout for messages in SQS queue." + } + }, + "Resources" : { + "ChatMessagesQueue" : { + "Type" : "AWS::SQS::Queue", + "Properties" : { + "VisibilityTimeout" : { "Ref" : "DefaultVisibilityTimeout" } + } + }, + "ChatTopic" : { + "Type" : "AWS::SNS::Topic", + "Properties" : { + "Subscription" : [ + {"Protocol" : "sqs", "Endpoint" : {"Fn::GetAtt" : [ "ChatMessagesQueue", "Arn"]}} + ] + } + }, + "ChatMessagesQueuePolicy": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "Queues": [ + { "Ref": "ChatMessagesQueue" } + ], + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sqs:SendMessage", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { "Fn::GetAtt": [ "ChatMessagesQueue", "Arn" ] }, + "Condition": { + "ArnEquals": { + "aws:SourceArn": { "Ref": "ChatTopic" } + } + } + } + ] + } + } + } + }, + "Outputs" : { + "ChatMessagesQueueUrl" : { + "Value" : { "Ref" : "ChatMessagesQueue" } + }, + "ChatTopicArn" : { + "Value" : { "Ref" : "ChatTopic" } + } + } +} diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/BaseDynamoDbScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/BaseDynamoDbScenario.cs index 05ce7dc..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; } @@ -21,7 +21,7 @@ protected BaseDynamoDbScenario(TestFixture testFixture, ILocalStackFixture local public virtual async Task DynamoDbService_Should_Create_A_DynamoDb_Table_Async() { var tableName = Guid.NewGuid().ToString(); - CreateTableResponse createTableResponse = await CreateTestTableAsync(tableName).ConfigureAwait(false); + CreateTableResponse createTableResponse = await CreateTestTableAsync(tableName); Assert.Equal(HttpStatusCode.OK, createTableResponse.HttpStatusCode); } @@ -29,9 +29,9 @@ public virtual async Task DynamoDbService_Should_Create_A_DynamoDb_Table_Async() public virtual async Task DynamoDbService_Should_Delete_A_DynamoDb_Table_Async() { var tableName = Guid.NewGuid().ToString(); - await CreateTestTableAsync(tableName).ConfigureAwait(false); + await CreateTestTableAsync(tableName); - DeleteTableResponse deleteTableResponse = await DeleteTestTableAsync(tableName).ConfigureAwait(false); + DeleteTableResponse deleteTableResponse = await DeleteTestTableAsync(tableName); Assert.Equal(HttpStatusCode.OK, deleteTableResponse.HttpStatusCode); } @@ -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 }; - await CreateTestTableAsync(tableName).ConfigureAwait(false); - Table targetTable = DynamoDbContext.GetTargetTable(dynamoDbOperationConfig); + // Fix: Use GetTargetTableConfig instead of DynamoDBOperationConfig + var getTargetTableConfig = new GetTargetTableConfig() { OverrideTableName = tableName }; + + await CreateTestTableAsync(tableName); + + 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).ConfigureAwait(false); - dynamoDbOperationConfig.IndexName = TestConstants.MovieTableMovieIdGsi; - List movieEntities = - await DynamoDbContext.QueryAsync(movieEntity.MovieId, dynamoDbOperationConfig).GetRemainingAsync().ConfigureAwait(false); + await targetTable.PutItemAsync(item); + + // 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 }; - await CreateTestTableAsync(tableName).ConfigureAwait(false); + // Fix: Use GetTargetTableConfig instead of DynamoDBOperationConfig + var getTargetTableConfig = new GetTargetTableConfig() { OverrideTableName = tableName }; + await CreateTestTableAsync(tableName); - Table targetTable = DynamoDbContext.GetTargetTable(dynamoDbOperationConfig); - IList 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).ConfigureAwait(false); + await targetTable.PutItemAsync(document); } - dynamoDbOperationConfig.IndexName = TestConstants.MovieTableMovieIdGsi; - List returnedMovieEntities = - await DynamoDbContext.ScanAsync(new List(), dynamoDbOperationConfig).GetRemainingAsync().ConfigureAwait(false); + // 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/DynamoDbScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/DynamoDbScenario.cs index be7b3bc..15a3e8c 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/DynamoDbScenario.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/DynamoDbScenario.cs @@ -2,35 +2,18 @@ namespace LocalStack.Client.Functional.Tests.Scenarios.DynamoDb; -[Collection(nameof(LocalStackCollectionV131))] -public class DynamoDbScenarioV131 : BaseDynamoDbScenario +[Collection(nameof(LocalStackCollectionV37))] +public sealed class DynamoDbScenarioV37 : BaseDynamoDbScenario { - public DynamoDbScenarioV131(TestFixture testFixture, LocalStackFixtureV131 localStackFixtureV131) : base(testFixture, localStackFixtureV131) + public DynamoDbScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) { } } -[Collection(nameof(LocalStackCollectionV20))] -public sealed class DynamoDbScenarioV20 : BaseDynamoDbScenario +[Collection(nameof(LocalStackCollectionV46))] +public sealed class DynamoDbScenarioV46 : BaseDynamoDbScenario { - public DynamoDbScenarioV20(TestFixture testFixture, LocalStackFixtureV20 localStackFixtureV20) : base(testFixture, localStackFixtureV20) - { - } -} - -[Collection(nameof(LocalStackCollectionV22))] -public sealed class DynamoDbScenarioV22 : BaseDynamoDbScenario -{ - public DynamoDbScenarioV22(TestFixture testFixture, LocalStackFixtureV22 localStackFixtureV22) : base(testFixture, localStackFixtureV22) - { - } -} - -[Collection(nameof(LocalStackLegacyCollection))] -public sealed class DynamoDbLegacyScenario : BaseDynamoDbScenario -{ - public DynamoDbLegacyScenario(TestFixture testFixture, LocalStackLegacyFixture localStackLegacyFixture) : base( - testFixture, localStackLegacyFixture, TestConstants.LegacyLocalStackConfig, true) + public DynamoDbScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) { } } \ No newline at end of file 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 462aa41..11af890 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/BaseRealLife.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/BaseRealLife.cs @@ -24,25 +24,25 @@ public virtual async Task var jobCreatedEvent = new JobCreatedEvent(423565221, 191, 125522, "Painting Service"); var createTopicRequest = new CreateTopicRequest(topicName); - CreateTopicResponse createTopicResponse = await AmazonSimpleNotificationService.CreateTopicAsync(createTopicRequest).ConfigureAwait(false); + CreateTopicResponse createTopicResponse = await AmazonSimpleNotificationService.CreateTopicAsync(createTopicRequest); Assert.Equal(HttpStatusCode.OK, createTopicResponse.HttpStatusCode); var createQueueRequest = new CreateQueueRequest(queueName); - CreateQueueResponse createQueueResponse = await AmazonSqs.CreateQueueAsync(createQueueRequest).ConfigureAwait(false); + CreateQueueResponse createQueueResponse = await AmazonSqs.CreateQueueAsync(createQueueRequest); Assert.Equal(HttpStatusCode.OK, createQueueResponse.HttpStatusCode); const string queueArnAttribute = "QueueArn"; var getQueueAttributesRequest = new GetQueueAttributesRequest(createQueueResponse.QueueUrl, new List { queueArnAttribute }); - GetQueueAttributesResponse getQueueAttributesResponse = await AmazonSqs.GetQueueAttributesAsync(getQueueAttributesRequest).ConfigureAwait(false); + GetQueueAttributesResponse getQueueAttributesResponse = await AmazonSqs.GetQueueAttributesAsync(getQueueAttributesRequest); Assert.Equal(HttpStatusCode.OK, getQueueAttributesResponse.HttpStatusCode); string queueArn = getQueueAttributesResponse.Attributes[queueArnAttribute]; var subscribeRequest = new SubscribeRequest(createTopicResponse.TopicArn, "sqs", queueArn); - SubscribeResponse subscribeResponse = await AmazonSimpleNotificationService.SubscribeAsync(subscribeRequest).ConfigureAwait(false); + SubscribeResponse subscribeResponse = await AmazonSimpleNotificationService.SubscribeAsync(subscribeRequest); Assert.Equal(HttpStatusCode.OK, subscribeResponse.HttpStatusCode); @@ -57,25 +57,25 @@ public virtual async Task Message = serializedObject, TopicArn = createTopicResponse.TopicArn, Subject = jobCreatedEvent.EventName, MessageAttributes = messageAttributes }; - PublishResponse publishResponse = await AmazonSimpleNotificationService.PublishAsync(publishRequest).ConfigureAwait(false); + PublishResponse publishResponse = await AmazonSimpleNotificationService.PublishAsync(publishRequest); Assert.Equal(HttpStatusCode.OK, publishResponse.HttpStatusCode); var receiveMessageRequest = new ReceiveMessageRequest(createQueueResponse.QueueUrl); - ReceiveMessageResponse receiveMessageResponse = await AmazonSqs.ReceiveMessageAsync(receiveMessageRequest).ConfigureAwait(false); + ReceiveMessageResponse receiveMessageResponse = await AmazonSqs.ReceiveMessageAsync(receiveMessageRequest); Assert.Equal(HttpStatusCode.OK, receiveMessageResponse.HttpStatusCode); - if (receiveMessageResponse.Messages.Count == 0) + if ((receiveMessageResponse.Messages?.Count ?? 0) == 0) { - await Task.Delay(2000).ConfigureAwait(false); - receiveMessageResponse = await AmazonSqs.ReceiveMessageAsync(receiveMessageRequest).ConfigureAwait(false); + await Task.Delay(2000); + receiveMessageResponse = await AmazonSqs.ReceiveMessageAsync(receiveMessageRequest); Assert.Equal(HttpStatusCode.OK, receiveMessageResponse.HttpStatusCode); } 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/RealLife/SnsToSqsScenarios.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/SnsToSqsScenarios.cs index d60cf02..78ea378 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/SnsToSqsScenarios.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/SnsToSqsScenarios.cs @@ -2,40 +2,18 @@ namespace LocalStack.Client.Functional.Tests.Scenarios.RealLife; -[Collection(nameof(LocalStackCollectionV131))] -public sealed class SnsToSqsScenarioV131 : BaseRealLife +[Collection(nameof(LocalStackCollectionV37))] +public sealed class SnsToSqsScenarioV37 : BaseRealLife { - public SnsToSqsScenarioV131(TestFixture testFixture, LocalStackFixtureV131 localStackFixtureV131) : base(testFixture, localStackFixtureV131) + public SnsToSqsScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) { } } -[Collection(nameof(LocalStackCollectionV20))] -public sealed class SnsToSqsScenarioV20 : BaseRealLife +[Collection(nameof(LocalStackCollectionV46))] +public sealed class SnsToSqsScenarioV46 : BaseRealLife { - public SnsToSqsScenarioV20(TestFixture testFixture, LocalStackFixtureV20 localStackFixtureV20) : base(testFixture, localStackFixtureV20) + public SnsToSqsScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) { } -} - -[Collection(nameof(LocalStackCollectionV22))] -public sealed class SnsToSqsScenarioV22 : BaseRealLife -{ - public SnsToSqsScenarioV22(TestFixture testFixture, LocalStackFixtureV22 localStackFixtureV22) : base(testFixture, localStackFixtureV22) - { - } -} - -[Collection(nameof(LocalStackLegacyCollection))] -public sealed class SnsToSqsLegacyScenario : BaseRealLife -{ - public SnsToSqsLegacyScenario(TestFixture testFixture, LocalStackLegacyFixture localStackFixtureV22) : base( - testFixture, localStackFixtureV22, TestConstants.LegacyLocalStackConfig, true) - { - } - - public override Task Should_Create_A_SNS_Topic_And_SQS_Queue_Then_Subscribe_To_The_Topic_Using_SQS_Then_Publish_A_Message_To_Topic_And_Read_It_From_The_Queue_Async() - { - return Task.CompletedTask; - } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/BaseS3Scenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/BaseS3Scenario.cs index 700c455..bc60c53 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/BaseS3Scenario.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/BaseS3Scenario.cs @@ -18,7 +18,7 @@ protected BaseS3Scenario(TestFixture testFixture, ILocalStackFixture localStackF public async Task S3Service_Should_Create_A_Bucket_Async() { var bucketName = Guid.NewGuid().ToString(); - PutBucketResponse putBucketResponse = await CreateTestBucketAsync(bucketName).ConfigureAwait(false); + PutBucketResponse putBucketResponse = await CreateTestBucketAsync(bucketName); Assert.Equal(HttpStatusCode.OK, putBucketResponse.HttpStatusCode); } @@ -27,11 +27,11 @@ public async Task S3Service_Should_Create_A_Bucket_Async() public async Task S3Service_Should_Delete_A_Bucket_Async() { var bucketName = Guid.NewGuid().ToString(); - PutBucketResponse putBucketResponse = await CreateTestBucketAsync(bucketName).ConfigureAwait(false); + PutBucketResponse putBucketResponse = await CreateTestBucketAsync(bucketName); Assert.Equal(HttpStatusCode.OK, putBucketResponse.HttpStatusCode); - DeleteBucketResponse deleteBucketResponse = await DeleteTestBucketAsync(bucketName).ConfigureAwait(false); + DeleteBucketResponse deleteBucketResponse = await DeleteTestBucketAsync(bucketName); Assert.Equal(HttpStatusCode.NoContent, deleteBucketResponse.HttpStatusCode); } @@ -39,10 +39,10 @@ public async Task S3Service_Should_Delete_A_Bucket_Async() public async Task S3Service_Should_Upload_A_File_To_A_Bucket_Async() { var bucketName = Guid.NewGuid().ToString(); - await CreateTestBucketAsync(bucketName).ConfigureAwait(false); - await UploadTestFileAsync(key: Key, bucketName: bucketName).ConfigureAwait(false); + await CreateTestBucketAsync(bucketName); + await UploadTestFileAsync(key: Key, bucketName: bucketName); - GetObjectResponse getObjectResponse = await AmazonS3.GetObjectAsync(bucketName, Key).ConfigureAwait(false); + GetObjectResponse getObjectResponse = await AmazonS3.GetObjectAsync(bucketName, Key); Assert.Equal(HttpStatusCode.OK, getObjectResponse.HttpStatusCode); } @@ -51,10 +51,10 @@ public async Task S3Service_Should_Upload_A_File_To_A_Bucket_Async() public async Task S3Service_Should_Delete_A_File_To_A_Bucket_Async() { var bucketName = Guid.NewGuid().ToString(); - await CreateTestBucketAsync(bucketName).ConfigureAwait(false); - await UploadTestFileAsync(key: Key, bucketName: bucketName).ConfigureAwait(false); + await CreateTestBucketAsync(bucketName); + await UploadTestFileAsync(key: Key, bucketName: bucketName); - DeleteObjectResponse deleteObjectResponse = await AmazonS3.DeleteObjectAsync(bucketName, Key).ConfigureAwait(false); + DeleteObjectResponse deleteObjectResponse = await AmazonS3.DeleteObjectAsync(bucketName, Key); Assert.Equal(HttpStatusCode.NoContent, deleteObjectResponse.HttpStatusCode); } @@ -63,7 +63,7 @@ public async Task S3Service_Should_Delete_A_File_To_A_Bucket_Async() public async Task S3Service_Should_List_Files_In_A_Bucket_Async() { var bucketName = Guid.NewGuid().ToString(); - await CreateTestBucketAsync(bucketName).ConfigureAwait(false); + await CreateTestBucketAsync(bucketName); const int uploadCount = 4; var fileNames = new string[uploadCount]; @@ -72,11 +72,11 @@ public async Task S3Service_Should_List_Files_In_A_Bucket_Async() { var fileName = $"SampleData{i}.txt"; - await UploadTestFileAsync(fileName, bucketName).ConfigureAwait(false); + await UploadTestFileAsync(fileName, bucketName); fileNames[i] = fileName; } - ListObjectsResponse listObjectsResponse = await AmazonS3.ListObjectsAsync(bucketName).ConfigureAwait(false); + ListObjectsResponse listObjectsResponse = await AmazonS3.ListObjectsAsync(bucketName); List s3Objects = listObjectsResponse.S3Objects; Assert.Equal(uploadCount, s3Objects.Count); @@ -101,6 +101,6 @@ protected async Task UploadTestFileAsync(string? key = null, string? bucketName { using var fileTransferUtility = new TransferUtility(AmazonS3); - await fileTransferUtility.UploadAsync(FilePath, bucketName ?? BucketName, key ?? Key).ConfigureAwait(false); + await fileTransferUtility.UploadAsync(FilePath, bucketName ?? BucketName, key ?? Key); } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/S3Scenarios.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/S3Scenarios.cs index a973b4d..dd1ce9c 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/S3Scenarios.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/S3Scenarios.cs @@ -2,35 +2,18 @@ namespace LocalStack.Client.Functional.Tests.Scenarios.S3; -[Collection(nameof(LocalStackCollectionV131))] -public sealed class S3ScenarioV131 : BaseS3Scenario +[Collection(nameof(LocalStackCollectionV37))] +public sealed class S3ScenarioV37 : BaseS3Scenario { - public S3ScenarioV131(TestFixture testFixture, LocalStackFixtureV131 localStackFixtureV131) : base(testFixture, localStackFixtureV131) + public S3ScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) { } } -[Collection(nameof(LocalStackCollectionV20))] -public sealed class S3ScenarioV20 : BaseS3Scenario +[Collection(nameof(LocalStackCollectionV46))] +public sealed class S3ScenarioV46 : BaseS3Scenario { - public S3ScenarioV20(TestFixture testFixture, LocalStackFixtureV20 localStackFixtureV20) : base(testFixture, localStackFixtureV20) - { - } -} - -[Collection(nameof(LocalStackCollectionV22))] -public sealed class S3ScenarioV22 : BaseS3Scenario -{ - public S3ScenarioV22(TestFixture testFixture, LocalStackFixtureV22 localStackFixtureV22) : base(testFixture, localStackFixtureV22) - { - } -} - -[Collection(nameof(LocalStackLegacyCollection))] -public sealed class S3LegacyScenario : BaseS3Scenario -{ - public S3LegacyScenario(TestFixture testFixture, LocalStackLegacyFixture localStackLegacyFixture) : base( - testFixture, localStackLegacyFixture, TestConstants.LegacyLocalStackConfig, true) + public S3ScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) { } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs index f314c47..97f2912 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs @@ -17,17 +17,17 @@ protected BaseSnsScenario(TestFixture testFixture, ILocalStackFixture localStack public async Task SnsService_Should_Create_A_Sns_Topic_Async() { var topicName = Guid.NewGuid().ToString(); - CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName).ConfigureAwait(false); + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); Assert.Equal(HttpStatusCode.OK, createTopicResponse.HttpStatusCode); - ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync().ConfigureAwait(false); - Topic? snsTopic = listTopicsResponse.Topics.SingleOrDefault(topic => topic.TopicArn == createTopicResponse.TopicArn); + ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync(); + Topic? snsTopic = listTopicsResponse.Topics?.SingleOrDefault(topic => topic.TopicArn == createTopicResponse.TopicArn); Assert.NotNull(snsTopic); Assert.EndsWith(topicName, snsTopic.TopicArn, StringComparison.Ordinal); - await DeleteSnsTopicAsync(createTopicResponse.TopicArn).ConfigureAwait(false); //Cleanup + await DeleteSnsTopicAsync(createTopicResponse.TopicArn); //Cleanup } [Fact] @@ -35,13 +35,13 @@ public async Task SnsService_Should_Delete_A_Sns_Topic_Async() { var topicName = Guid.NewGuid().ToString(); - CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName).ConfigureAwait(false); - DeleteTopicResponse deleteTopicResponse = await DeleteSnsTopicAsync(createTopicResponse.TopicArn).ConfigureAwait(false); + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); + DeleteTopicResponse deleteTopicResponse = await DeleteSnsTopicAsync(createTopicResponse.TopicArn); Assert.Equal(HttpStatusCode.OK, deleteTopicResponse.HttpStatusCode); - ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync().ConfigureAwait(false); - bool hasAny = listTopicsResponse.Topics.Exists(topic => topic.TopicArn == createTopicResponse.TopicArn); + ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync(); + bool hasAny = listTopicsResponse.Topics?.Exists(topic => topic.TopicArn == createTopicResponse.TopicArn) ?? false; Assert.False(hasAny); } @@ -50,7 +50,7 @@ public async Task SnsService_Should_Delete_A_Sns_Topic_Async() public async Task SnsService_Should_Send_Publish_A_Message_Async() { var topicName = Guid.NewGuid().ToString(); - CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName).ConfigureAwait(false); + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); var jobCreatedEvent = new JobCreatedEvent(423565221, 191, 125522, "Painting Service"); string serializedObject = JsonSerializer.Serialize(jobCreatedEvent); @@ -65,11 +65,11 @@ public async Task SnsService_Should_Send_Publish_A_Message_Async() Message = serializedObject, TopicArn = createTopicResponse.TopicArn, Subject = jobCreatedEvent.EventName, MessageAttributes = messageAttributes }; - PublishResponse publishResponse = await AmazonSimpleNotificationService.PublishAsync(publishRequest).ConfigureAwait(false); + PublishResponse publishResponse = await AmazonSimpleNotificationService.PublishAsync(publishRequest); Assert.Equal(HttpStatusCode.OK, publishResponse.HttpStatusCode); - await DeleteSnsTopicAsync(createTopicResponse.TopicArn).ConfigureAwait(false); //Cleanup + await DeleteSnsTopicAsync(createTopicResponse.TopicArn); //Cleanup } [Theory, InlineData("eu-central-1"), InlineData("us-west-1"), InlineData("af-south-1"), InlineData("ap-southeast-1"), InlineData("ca-central-1"), @@ -84,26 +84,27 @@ public virtual async Task Multi_Region_Tests_Async(string systemName) Assert.Equal(RegionEndpoint.GetBySystemName(systemName), amazonSimpleNotificationService.Config.RegionEndpoint); var topicName = Guid.NewGuid().ToString(); - CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName).ConfigureAwait(false); + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); Assert.Equal(HttpStatusCode.OK, createTopicResponse.HttpStatusCode); var topicArn = $"arn:aws:sns:{systemName}:000000000000:{topicName}"; - ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync().ConfigureAwait(false); - Topic? snsTopic = listTopicsResponse.Topics.SingleOrDefault(topic => topic.TopicArn == topicArn); + ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync(); + Topic? snsTopic = listTopicsResponse.Topics?.SingleOrDefault(topic => topic.TopicArn == topicArn); Assert.NotNull(snsTopic); + Assert.NotNull(listTopicsResponse.Topics); Assert.Single(listTopicsResponse.Topics); - await DeleteSnsTopicAsync(topicArn).ConfigureAwait(false); //Cleanup + await DeleteSnsTopicAsync(topicArn); //Cleanup } protected async Task CreateSnsTopicAsync(string topic) { var createTopicRequest = new CreateTopicRequest(topic); - CreateTopicResponse createTopicResponse = await AmazonSimpleNotificationService.CreateTopicAsync(createTopicRequest).ConfigureAwait(false); + CreateTopicResponse createTopicResponse = await AmazonSimpleNotificationService.CreateTopicAsync(createTopicRequest); return createTopicResponse; } @@ -112,7 +113,7 @@ protected async Task DeleteSnsTopicAsync(string topic) { var deleteTopicRequest = new DeleteTopicRequest(topic); - DeleteTopicResponse deleteTopicResponse = await AmazonSimpleNotificationService.DeleteTopicAsync(deleteTopicRequest).ConfigureAwait(false); + DeleteTopicResponse deleteTopicResponse = await AmazonSimpleNotificationService.DeleteTopicAsync(deleteTopicRequest); return deleteTopicResponse; } diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/SnsScenarios.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/SnsScenarios.cs index 96fd4c4..8576088 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/SnsScenarios.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/SnsScenarios.cs @@ -2,40 +2,18 @@ namespace LocalStack.Client.Functional.Tests.Scenarios.SNS; -[Collection(nameof(LocalStackCollectionV131))] -public sealed class SnsScenarioV131 : BaseSnsScenario +[Collection(nameof(LocalStackCollectionV37))] +public sealed class SnsScenarioV37 : BaseSnsScenario { - public SnsScenarioV131(TestFixture testFixture, LocalStackFixtureV131 localStackFixtureV131) : base(testFixture, localStackFixtureV131) + public SnsScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) { } } -[Collection(nameof(LocalStackCollectionV20))] -public sealed class SnsScenarioV20 : BaseSnsScenario +[Collection(nameof(LocalStackCollectionV46))] +public sealed class SnsScenarioV46 : BaseSnsScenario { - public SnsScenarioV20(TestFixture testFixture, LocalStackFixtureV20 localStackFixtureV20) : base(testFixture, localStackFixtureV20) + public SnsScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) { } -} - -[Collection(nameof(LocalStackCollectionV22))] -public sealed class SnsScenarioV22 : BaseSnsScenario -{ - public SnsScenarioV22(TestFixture testFixture, LocalStackFixtureV22 localStackFixtureV22) : base(testFixture, localStackFixtureV22) - { - } -} - -[Collection(nameof(LocalStackLegacyCollection))] -public sealed class SnsLegacyScenario : BaseSnsScenario -{ - public SnsLegacyScenario(TestFixture testFixture, LocalStackLegacyFixture localStackLegacyFixture) : base( - testFixture, localStackLegacyFixture, TestConstants.LegacyLocalStackConfig, true) - { - } - - public override Task Multi_Region_Tests_Async(string systemName) - { - return Task.CompletedTask; - } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/BaseSqsScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/BaseSqsScenario.cs index f0bff9c..a5020bd 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/BaseSqsScenario.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/BaseSqsScenario.cs @@ -16,38 +16,38 @@ protected BaseSqsScenario(TestFixture testFixture, ILocalStackFixture localStack protected IAmazonSQS AmazonSqs { get; set; } [Fact] - public async Task AmazonSqsService_Should_Create_A_Queue_Async() + public virtual async Task AmazonSqsService_Should_Create_A_Queue_Async() { var guid = Guid.NewGuid(); var queueName = $"{guid}.fifo"; var dlQueueName = $"{guid}-DLQ.fifo"; - CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName).ConfigureAwait(false); + CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName); Assert.Equal(HttpStatusCode.OK, createQueueResponse.HttpStatusCode); } [Fact] - public async Task AmazonSqsService_Should_Delete_A_Queue_Async() + public virtual async Task AmazonSqsService_Should_Delete_A_Queue_Async() { var guid = Guid.NewGuid(); var queueName = $"{guid}.fifo"; var dlQueueName = $"{guid}-DLQ.fifo"; - CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName).ConfigureAwait(false); - DeleteQueueResponse deleteQueueResponse = await DeleteQueueAsync(createQueueResponse.QueueUrl).ConfigureAwait(false); + CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName); + DeleteQueueResponse deleteQueueResponse = await DeleteQueueAsync(createQueueResponse.QueueUrl); Assert.Equal(HttpStatusCode.OK, deleteQueueResponse.HttpStatusCode); } [Fact] - public async Task AmazonSqsService_Should_Send_A_Message_To_A_Queue_Async() + public virtual async Task AmazonSqsService_Should_Send_A_Message_To_A_Queue_Async() { var guid = Guid.NewGuid(); var queueName = $"{guid}.fifo"; var dlQueueName = $"{guid}-DLQ.fifo"; - CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName).ConfigureAwait(false); + CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName); var commentModel = new Fixture().Create(); string serializedModel = JsonSerializer.Serialize(commentModel); @@ -60,19 +60,19 @@ public async Task AmazonSqsService_Should_Send_A_Message_To_A_Queue_Async() MessageBody = serializedModel, }; - SendMessageResponse messageResponse = await AmazonSqs.SendMessageAsync(sendMessageRequest).ConfigureAwait(false); + SendMessageResponse messageResponse = await AmazonSqs.SendMessageAsync(sendMessageRequest); Assert.Equal(HttpStatusCode.OK, messageResponse.HttpStatusCode); } [Fact] - public async Task AmazonSqsService_Should_Receive_Messages_From_A_Queue_Async() + public virtual async Task AmazonSqsService_Should_Receive_Messages_From_A_Queue_Async() { var guid = Guid.NewGuid(); var queueName = $"{guid}.fifo"; var dlQueueName = $"{guid}-DLQ.fifo"; - CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName).ConfigureAwait(false); + CreateQueueResponse createQueueResponse = await CreateFifoQueueWithRedriveAsync(queueName, dlQueueName); var commentModel = new Fixture().Create(); string serializedModel = JsonSerializer.Serialize(commentModel); @@ -85,11 +85,11 @@ public async Task AmazonSqsService_Should_Receive_Messages_From_A_Queue_Async() MessageBody = serializedModel }; - await AmazonSqs.SendMessageAsync(sendMessageRequest).ConfigureAwait(false); + await AmazonSqs.SendMessageAsync(sendMessageRequest); var req = new ReceiveMessageRequest { MaxNumberOfMessages = 1, QueueUrl = createQueueResponse.QueueUrl }; - ReceiveMessageResponse receiveMessages = await AmazonSqs.ReceiveMessageAsync(req).ConfigureAwait(false); + ReceiveMessageResponse receiveMessages = await AmazonSqs.ReceiveMessageAsync(req); Assert.Equal(HttpStatusCode.OK, receiveMessages.HttpStatusCode); Message? currentMessage = receiveMessages.Messages.FirstOrDefault(); @@ -107,14 +107,14 @@ protected async Task CreateFifoQueueWithRedriveAsync(string QueueName = dlQueueName ?? TestDlQueueName, Attributes = new Dictionary(StringComparer.Ordinal) { { "FifoQueue", "true" }, }, }; - CreateQueueResponse createDlqResult = await AmazonSqs.CreateQueueAsync(createDlqRequest).ConfigureAwait(false); + CreateQueueResponse createDlqResult = await AmazonSqs.CreateQueueAsync(createDlqRequest); GetQueueAttributesResponse attributes = await AmazonSqs.GetQueueAttributesAsync(new GetQueueAttributesRequest { QueueUrl = createDlqResult.QueueUrl, AttributeNames = new List { "QueueArn" }, }) - .ConfigureAwait(false); + ; var redrivePolicy = new { maxReceiveCount = "1", deadLetterTargetArn = attributes.Attributes["QueueArn"] }; @@ -127,14 +127,14 @@ protected async Task CreateFifoQueueWithRedriveAsync(string }, }; - return await AmazonSqs.CreateQueueAsync(createQueueRequest).ConfigureAwait(false); + return await AmazonSqs.CreateQueueAsync(createQueueRequest); } protected async Task CreateQueueAsync(string? queueName = null) { var createQueueRequest = new CreateQueueRequest(queueName ?? TestQueueName); - return await AmazonSqs.CreateQueueAsync(createQueueRequest).ConfigureAwait(false); + return await AmazonSqs.CreateQueueAsync(createQueueRequest); } [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings")] @@ -142,6 +142,6 @@ protected async Task DeleteQueueAsync(string queueUrl) { var deleteQueueRequest = new DeleteQueueRequest(queueUrl); - return await AmazonSqs.DeleteQueueAsync(deleteQueueRequest).ConfigureAwait(false); + return await AmazonSqs.DeleteQueueAsync(deleteQueueRequest); } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/SqsScenarios.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/SqsScenarios.cs index 02971e4..ecdb77e 100644 --- a/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/SqsScenarios.cs +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/SqsScenarios.cs @@ -2,35 +2,18 @@ namespace LocalStack.Client.Functional.Tests.Scenarios.SQS; -[Collection(nameof(LocalStackCollectionV131))] -public sealed class SqsScenarioV131 : BaseSqsScenario +[Collection(nameof(LocalStackCollectionV37))] +public sealed class SqsScenarioV37 : BaseSqsScenario { - public SqsScenarioV131(TestFixture testFixture, LocalStackFixtureV131 localStackFixtureV131) : base(testFixture, localStackFixtureV131) + public SqsScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) { } } -[Collection(nameof(LocalStackCollectionV20))] -public sealed class SqsScenarioV20 : BaseSqsScenario +[Collection(nameof(LocalStackCollectionV46))] +public sealed class SqsScenarioV46 : BaseSqsScenario { - public SqsScenarioV20(TestFixture testFixture, LocalStackFixtureV20 localStackFixtureV20) : base(testFixture, localStackFixtureV20) - { - } -} - -[Collection(nameof(LocalStackCollectionV22))] -public sealed class SqsScenarioV22 : BaseSqsScenario -{ - public SqsScenarioV22(TestFixture testFixture, LocalStackFixtureV22 localStackFixtureV22) : base(testFixture, localStackFixtureV22) - { - } -} - -[Collection(nameof(LocalStackLegacyCollection))] -public sealed class SqsLegacyScenario : BaseSqsScenario -{ - public SqsLegacyScenario(TestFixture testFixture, LocalStackLegacyFixture localStackLegacyFixture) : base( - testFixture, localStackLegacyFixture, TestConstants.LegacyLocalStackConfig, true) + public SqsScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) { } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/TestConstants.cs b/tests/LocalStack.Client.Functional.Tests/TestConstants.cs index bd8fddc..a623dc3 100644 --- a/tests/LocalStack.Client.Functional.Tests/TestConstants.cs +++ b/tests/LocalStack.Client.Functional.Tests/TestConstants.cs @@ -2,13 +2,10 @@ public static class TestConstants { - public const string LegacyLocalStackConfig = "appsettings.LocalStackLegacy.json"; public const string LocalStackConfig = "appsettings.LocalStack.json"; - public const string LocalStackHttpsConfig = "appsettings.LocalStack.Https.json"; - public const string LocalStackV13 = "1.3.1"; - public const string LocalStackV20 = "2.0"; - public const string LocalStackV22 = "latest"; - - public const string MovieTableMovieIdGsi = "MoiveTableMovie-Index"; -} + public const string LocalStackV37 = "3.7.1"; + public const string LocalStackV46 = "4.6.0"; + + public const string MovieTableMovieIdGsi = "MovieTableMovie-Index"; +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/TestContainers.cs b/tests/LocalStack.Client.Functional.Tests/TestContainers.cs index 918e392..9e9fc4f 100644 --- a/tests/LocalStack.Client.Functional.Tests/TestContainers.cs +++ b/tests/LocalStack.Client.Functional.Tests/TestContainers.cs @@ -4,19 +4,6 @@ namespace LocalStack.Client.Functional.Tests; internal static class TestContainers { - public static readonly LocalStackBuilder LocalStackLegacyBuilder = new LocalStackBuilder().WithImage($"localstack/localstack:0.11.4") - .WithName($"localStack-0.11.4-{Guid.NewGuid().ToString().ToLower()}") - .WithEnvironment("DEFAULT_REGION", "eu-central-1") - .WithEnvironment("SERVICES", "s3,dynamodb,sqs,sns") - .WithEnvironment("DOCKER_HOST", "unix:///var/run/docker.sock") - .WithEnvironment("DEBUG", "1") - .WithPortBinding(AwsServiceEndpointMetadata.DynamoDb.Port, AwsServiceEndpointMetadata.DynamoDb.Port) - .WithPortBinding(AwsServiceEndpointMetadata.Sqs.Port, AwsServiceEndpointMetadata.Sqs.Port) - .WithPortBinding(AwsServiceEndpointMetadata.S3.Port, AwsServiceEndpointMetadata.S3.Port) - .WithPortBinding(AwsServiceEndpointMetadata.Sns.Port, AwsServiceEndpointMetadata.Sns.Port) - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(AwsServiceEndpointMetadata.DynamoDb.Port)) - .WithCleanUp(true); - public static LocalStackBuilder LocalStackBuilder(string version) { return new LocalStackBuilder().WithImage($"localstack/localstack:{version}") @@ -24,7 +11,7 @@ public static LocalStackBuilder LocalStackBuilder(string version) .WithEnvironment("DOCKER_HOST", "unix:///var/run/docker.sock") .WithEnvironment("DEBUG", "1") .WithEnvironment("LS_LOG", "trace-internal") - .WithPortBinding(4566, true) + .WithPortBinding(4566, assignRandomHostPort: true) .WithCleanUp(true); } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Integration.Tests/AssertAmazonClient.cs b/tests/LocalStack.Client.Integration.Tests/AssertAmazonClient.cs index e5e357d..0f645a7 100644 --- a/tests/LocalStack.Client.Integration.Tests/AssertAmazonClient.cs +++ b/tests/LocalStack.Client.Integration.Tests/AssertAmazonClient.cs @@ -1,6 +1,6 @@ ο»Ώnamespace LocalStack.Client.Integration.Tests; -public static class AssertAmazonClient +internal static class AssertAmazonClient { public const string TestAwsRegion = "eu-central-1"; public const bool UseSsl = true; diff --git a/tests/LocalStack.Client.Integration.Tests/CreateClientByImplementationTests.cs b/tests/LocalStack.Client.Integration.Tests/CreateClientByImplementationTests.cs index 6e45c86..bbba6ec 100644 --- a/tests/LocalStack.Client.Integration.Tests/CreateClientByImplementationTests.cs +++ b/tests/LocalStack.Client.Integration.Tests/CreateClientByImplementationTests.cs @@ -1000,4 +1000,148 @@ public void Should_Able_To_Create_AmazonSchedulerClient() Assert.NotNull(amazonSchedulerClient); AssertAmazonClient.AssertClientConfiguration(amazonSchedulerClient); } + + [Fact] + public void Should_Able_To_Create_AmazonRAM() + { + var amazonRamClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRamClient); + AssertAmazonClient.AssertClientConfiguration(amazonRamClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppConfigData() + { + var amazonAppConfigDataClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAppConfigDataClient); + AssertAmazonClient.AssertClientConfiguration(amazonAppConfigDataClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonPinpoint() + { + var amazonPinpointClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonPinpointClient); + AssertAmazonClient.AssertClientConfiguration(amazonPinpointClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonPipes() + { + var amazonPipesClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonPipesClient); + AssertAmazonClient.AssertClientConfiguration(amazonPipesClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAccount() + { + var amazonAccountClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAccountClient); + AssertAmazonClient.AssertClientConfiguration(amazonAccountClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonACMPCAClient() + { + var amazonAcmpcaClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAcmpcaClient); + AssertAmazonClient.AssertClientConfiguration(amazonAcmpcaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonBedrockClient() + { + var amazonBedrockClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonBedrockClient); + AssertAmazonClient.AssertClientConfiguration(amazonBedrockClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudControlApiClient() + { + var amazonCloudControlApiClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudControlApiClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudControlApiClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodeBuildClient() + { + var amazonCodeBuildClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCodeBuildClient); + AssertAmazonClient.AssertClientConfiguration(amazonCodeBuildClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodeConnectionsClient() + { + var amazonCodeConnectionsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCodeConnectionsClient); + AssertAmazonClient.AssertClientConfiguration(amazonCodeConnectionsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodeDeployClient() + { + var amazonCodeDeployClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCodeDeployClient); + AssertAmazonClient.AssertClientConfiguration(amazonCodeDeployClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodePipelineClient() + { + var amazonCodePipelineClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCodePipelineClient); + AssertAmazonClient.AssertClientConfiguration(amazonCodePipelineClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticTranscoderClient() + { + var amazonElasticTranscoderClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElasticTranscoderClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticTranscoderClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMemoryDBClient() + { + var amazonMemoryDbClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonMemoryDbClient); + AssertAmazonClient.AssertClientConfiguration(amazonMemoryDbClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonShieldClient() + { + var amazonShieldClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonShieldClient); + AssertAmazonClient.AssertClientConfiguration(amazonShieldClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonVerifiedPermissionsClient() + { + var amazonVerifiedPermissionsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonVerifiedPermissionsClient); + AssertAmazonClient.AssertClientConfiguration(amazonVerifiedPermissionsClient); + } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs b/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs index ba2523e..fbfdaf4 100644 --- a/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs +++ b/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs @@ -1000,4 +1000,139 @@ public void Should_Able_To_Create_AmazonSchedulerClient() Assert.NotNull(amazonSchedulerClient); AssertAmazonClient.AssertClientConfiguration(amazonSchedulerClient); } + + [Fact] + public void Should_Able_To_Create_AmazonRAM() + { + AmazonServiceClient amazonRamClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRamClient); + AssertAmazonClient.AssertClientConfiguration(amazonRamClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppConfigData() + { + AmazonServiceClient amazonAppConfigData = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAppConfigData); + AssertAmazonClient.AssertClientConfiguration(amazonAppConfigData); + } + + [Fact] + public void Should_Able_To_Create_AmazonPinpoint() + { + AmazonServiceClient amazonPinpoint = Session.CreateClientByInterface(); + + Assert.NotNull(amazonPinpoint); + AssertAmazonClient.AssertClientConfiguration(amazonPinpoint); + } + + [Fact] + public void Should_Able_To_Create_AmazonPipes() + { + AmazonServiceClient amazonPipes = Session.CreateClientByInterface(); + + Assert.NotNull(amazonPipes); + AssertAmazonClient.AssertClientConfiguration(amazonPipes); + } + + [Fact] + public void Should_Able_To_Create_AmazonAccount() + { + AmazonServiceClient amazonAccount = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAccount); + AssertAmazonClient.AssertClientConfiguration(amazonAccount); + } + + [Fact] + public void Should_Able_To_Create_AmazonACMPCA() + { + AmazonServiceClient amazonAcmpca = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAcmpca); + AssertAmazonClient.AssertClientConfiguration(amazonAcmpca); + } + + [Fact] + public void Should_Able_To_Create_AmazonBedrock() + { + AmazonServiceClient amazonBedrock = Session.CreateClientByInterface(); + + Assert.NotNull(amazonBedrock); + AssertAmazonClient.AssertClientConfiguration(amazonBedrock); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudControlApi() + { + AmazonServiceClient amazonCloudControlApi = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudControlApi); + AssertAmazonClient.AssertClientConfiguration(amazonCloudControlApi); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodeConnections() + { + AmazonServiceClient amazonCodeConnections = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCodeConnections); + AssertAmazonClient.AssertClientConfiguration(amazonCodeConnections); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodeDeploy() + { + AmazonServiceClient amazonCodeDeploy = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCodeDeploy); + AssertAmazonClient.AssertClientConfiguration(amazonCodeDeploy); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodePipeline() + { + AmazonServiceClient amazonCodePipeline = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCodePipeline); + AssertAmazonClient.AssertClientConfiguration(amazonCodePipeline); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticTranscoder() + { + AmazonServiceClient amazonElasticTranscoder = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElasticTranscoder); + AssertAmazonClient.AssertClientConfiguration(amazonElasticTranscoder); + } + + [Fact] + public void Should_Able_To_Create_AmazonMemoryDB() + { + AmazonServiceClient amazonMemoryDb = Session.CreateClientByInterface(); + + Assert.NotNull(amazonMemoryDb); + AssertAmazonClient.AssertClientConfiguration(amazonMemoryDb); + } + + [Fact] + public void Should_Able_To_Create_AmazonShield() + { + AmazonServiceClient amazonShield = Session.CreateClientByInterface(); + + Assert.NotNull(amazonShield); + AssertAmazonClient.AssertClientConfiguration(amazonShield); + } + + [Fact] + public void Should_Able_To_Create_AmazonVerifiedPermissions() + { + AmazonServiceClient amazonVerifiedPermissions = Session.CreateClientByInterface(); + + Assert.NotNull(amazonVerifiedPermissions); + AssertAmazonClient.AssertClientConfiguration(amazonVerifiedPermissions); + } } \ No newline at end of file diff --git a/tests/LocalStack.Client.Integration.Tests/GlobalUsings.cs b/tests/LocalStack.Client.Integration.Tests/GlobalUsings.cs index 1e93c58..8bc1d15 100644 --- a/tests/LocalStack.Client.Integration.Tests/GlobalUsings.cs +++ b/tests/LocalStack.Client.Integration.Tests/GlobalUsings.cs @@ -1,17 +1,26 @@ -ο»Ώglobal using Amazon; +ο»Ώglobal using System; +global using System.Diagnostics.CodeAnalysis; +global using System.Reflection; +global using Amazon; +global using Amazon.Account; +global using Amazon.ACMPCA; global using Amazon.Amplify; global using Amazon.APIGateway; global using Amazon.ApiGatewayManagementApi; global using Amazon.ApiGatewayV2; global using Amazon.AppConfig; +global using Amazon.AppConfigData; +global using Amazon.Appflow; global using Amazon.AppSync; global using Amazon.Athena; global using Amazon.AutoScaling; -global using Amazon.AWSSupport; global using Amazon.AWSMarketplaceMetering; +global using Amazon.AWSSupport; global using Amazon.Backup; global using Amazon.Batch; +global using Amazon.Bedrock; global using Amazon.CertificateManager; +global using Amazon.CloudControlApi; global using Amazon.CloudFormation; global using Amazon.CloudFront; global using Amazon.CloudSearch; @@ -19,7 +28,11 @@ global using Amazon.CloudWatch; global using Amazon.CloudWatchEvents; global using Amazon.CloudWatchLogs; +global using Amazon.CodeBuild; global using Amazon.CodeCommit; +global using Amazon.CodeConnections; +global using Amazon.CodeDeploy; +global using Amazon.CodePipeline; global using Amazon.CognitoIdentity; global using Amazon.CognitoIdentityProvider; global using Amazon.ConfigService; @@ -37,6 +50,8 @@ global using Amazon.ElasticLoadBalancingV2; global using Amazon.ElasticMapReduce; global using Amazon.Elasticsearch; +global using Amazon.ElasticTranscoder; +global using Amazon.EMRServerless; global using Amazon.EventBridge; global using Amazon.FIS; global using Amazon.Glue; @@ -50,6 +65,7 @@ global using Amazon.IoTWireless; global using Amazon.Kafka; global using Amazon.KeyManagementService; +global using Amazon.Keyspaces; global using Amazon.KinesisAnalytics; global using Amazon.KinesisAnalyticsV2; global using Amazon.KinesisFirehose; @@ -58,11 +74,17 @@ global using Amazon.MediaConvert; global using Amazon.MediaStore; global using Amazon.MediaStoreData; +global using Amazon.MemoryDB; +global using Amazon.MQ; global using Amazon.MWAA; global using Amazon.Neptune; global using Amazon.OpenSearchService; global using Amazon.Organizations; +global using Amazon.Pinpoint; +global using Amazon.Pipes; global using Amazon.QLDB; +global using Amazon.QLDBSession; +global using Amazon.RAM; global using Amazon.RDS; global using Amazon.RDSDataService; global using Amazon.Redshift; @@ -70,16 +92,19 @@ global using Amazon.ResourceGroups; global using Amazon.ResourceGroupsTaggingAPI; global using Amazon.Route53; +global using Amazon.Route53Domains; global using Amazon.Route53Resolver; global using Amazon.Runtime; global using Amazon.S3; global using Amazon.S3Control; global using Amazon.SageMaker; global using Amazon.SageMakerRuntime; +global using Amazon.Scheduler; global using Amazon.SecretsManager; global using Amazon.SecurityToken; global using Amazon.ServerlessApplicationRepository; global using Amazon.ServiceDiscovery; +global using Amazon.Shield; global using Amazon.SimpleEmail; global using Amazon.SimpleEmailV2; global using Amazon.SimpleNotificationService; @@ -89,27 +114,16 @@ global using Amazon.StepFunctions; global using Amazon.TimestreamQuery; global using Amazon.TimestreamWrite; +global using Amazon.TranscribeService; global using Amazon.Transfer; +global using Amazon.VerifiedPermissions; global using Amazon.WAF; global using Amazon.WAFV2; global using Amazon.XRay; -global using Amazon.MQ; -global using Amazon.TranscribeService; global using LocalStack.Client.Contracts; global using LocalStack.Client.Exceptions; global using LocalStack.Client.Models; global using LocalStack.Client.Options; -global using System; -global using System.Diagnostics.CodeAnalysis; -global using System.Reflection; - -global using Amazon.Appflow; -global using Amazon.EMRServerless; -global using Amazon.Keyspaces; -global using Amazon.QLDBSession; -global using Amazon.Route53Domains; -global using Amazon.Scheduler; - global using Xunit; \ 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 ee72cbd..3dd364a 100644 --- a/tests/LocalStack.Client.Integration.Tests/LocalStack.Client.Integration.Tests.csproj +++ b/tests/LocalStack.Client.Integration.Tests/LocalStack.Client.Integration.Tests.csproj @@ -1,17 +1,20 @@ ο»Ώ - net461;net6.0;net7.0 - $(NoWarn);CA1707;MA0006 + net472;net8.0;net9.0 + $(NoWarn);CA1707;MA0006;CA1510 + + + @@ -20,7 +23,9 @@ + + @@ -28,7 +33,11 @@ + + + + @@ -49,6 +58,7 @@ + @@ -72,11 +82,15 @@ + + + + @@ -100,12 +114,14 @@ + + @@ -124,7 +140,7 @@ - + diff --git a/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj b/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj index 2d0a3b6..e59c51e 100644 --- a/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj +++ b/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj @@ -1,7 +1,7 @@ ο»Ώ - net461;net6.0;net7.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 7c4c22a..b28ba87 100644 --- a/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs +++ b/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs @@ -1,4 +1,6 @@ -ο»Ώnamespace LocalStack.Client.Tests.SessionTests; +ο»Ώ#pragma warning disable CA2263 // Prefer generic overload when type is known + +namespace LocalStack.Client.Tests.SessionTests; public class SessionLocalStackTests { @@ -6,7 +8,7 @@ public class SessionLocalStackTests public void CreateClientByImplementation_Should_Throw_NotSupportedClientException_If_Given_ServiceId_Is_Not_Supported() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); mockSession.SessionOptionsMock.SetupDefault(); mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockServiceMetadata); @@ -33,9 +35,9 @@ public void CreateClientByImplementation_Should_Throw_MisconfiguredClientExcepti public void CreateClientByImplementation_Should_Create_SessionAWSCredentials_With_AwsAccessKeyId_And_AwsAccessKey_And_AwsSessionToken() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + 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(); @@ -67,9 +69,9 @@ public void CreateClientByImplementation_Should_Create_SessionAWSCredentials_Wit public void CreateClientByImplementation_Should_Create_ClientConfig_With_UseHttp_Set_Bey_ConfigOptions_UseSsl(bool useSsl) { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -94,9 +96,9 @@ public void CreateClientByImplementation_Should_Create_ClientConfig_With_UseHttp public void CreateClientByImplementation_Should_Create_ClientConfig_With_UseHttp_And_ProxyHost_And_ProxyPort_By_ServiceEndpoint_Configuration() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); var configOptions = new ConfigOptions(); mockSession.SessionOptionsMock.SetupDefault(); @@ -130,9 +132,9 @@ public void CreateClientByImplementation_Should_Set_RegionEndpoint_By_RegionName { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); @@ -153,15 +155,17 @@ public void CreateClientByImplementation_Should_Set_RegionEndpoint_By_RegionName [Theory, InlineData("sa-east-1"), InlineData(null)] - public void CreateClientByImplementation_Should_Set_ServiceUrl_By_ServiceEndpoint_Configuration_And_RegionEndpoint_To_Null_If_Given_UseServiceUrl_Parameter_Is_True_Regardless_Of_Use_RegionName_Property_Of_SessionOptions_Has_Value_Or_Not(string systemName) + public void CreateClientByImplementation_Should_Set_ServiceUrl_By_ServiceEndpoint_Configuration_And_RegionEndpoint_To_Null_If_Given_UseServiceUrl_Parameter_Is_True_Regardless_Of_Use_RegionName_Property_Of_SessionOptions_Has_Value_Or_Not(string? systemName) { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + 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); +#pragma warning restore CS8604 // Possible null reference argument. mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockServiceMetadata); mockSession.SessionReflectionMock.Setup(reflection => reflection.CreateClientConfig(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockClientConfig); @@ -181,9 +185,9 @@ public void CreateClientByImplementation_Should_Set_ServiceUrl_By_ServiceEndpoin public void CreateClientByImplementation_Should_Pass_The_ClientConfig_To_SetForcePathStyle() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -205,9 +209,9 @@ public void CreateClientByImplementation_Should_Pass_The_ClientConfig_To_SetForc public void CreateClientByImplementation_Should_Create_AmazonServiceClient_By_Given_Generic_Type_And_Configure_ServiceUrl_Or_RegionEndpoint_By_Given_UseServiceUrl_Parameter(bool useServiceUrl) { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(); @@ -253,7 +257,7 @@ public void CreateClientByInterface_Should_Throw_AmazonClientException_If_Given_ public void CreateClientByInterface_Should_Throw_NotSupportedClientException_If_Given_ServiceId_Is_Not_Supported() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); mockSession.SessionOptionsMock.SetupDefault(); mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockServiceMetadata); @@ -282,9 +286,9 @@ public void CreateClientByInterface_Should_Throw_MisconfiguredClientException_If public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_Set_Bey_ConfigOptions_UseSsl(bool useSsl) { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -301,7 +305,7 @@ public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_Set_ IClientConfig clientConfig = mockAmazonServiceClient.Config; Assert.Equal(useSsl, !clientConfig.UseHttp); - + mockSession.ConfigMock.Verify(config => config.GetConfigOptions(), Times.Once); } @@ -309,9 +313,9 @@ public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_Set_ public void CreateClientByInterface_Should_Create_SessionAWSCredentials_With_AwsAccessKeyId_And_AwsAccessKey_And_AwsSessionToken() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + 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(); @@ -341,9 +345,9 @@ public void CreateClientByInterface_Should_Create_SessionAWSCredentials_With_Aws public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_And_ProxyHost_And_ProxyPort_By_ServiceEndpoint_Configuration() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); var configOptions = new ConfigOptions(); mockSession.SessionOptionsMock.SetupDefault(); @@ -379,9 +383,9 @@ public void CreateClientByInterface_Should_Set_RegionEndpoint_By_RegionName_Prop { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); @@ -403,15 +407,17 @@ public void CreateClientByInterface_Should_Set_RegionEndpoint_By_RegionName_Prop [Theory, InlineData("sa-east-1"), InlineData(null)] - public void CreateClientByInterface_Should_Set_ServiceUrl_By_ServiceEndpoint_Configuration_And_RegionEndpoint_To_Null_If_Given_UseServiceUrl_Parameter_Is_True_Regardless_Of_Use_RegionName_Property_Of_SessionOptions_Has_Value_Or_Not(string systemName) + public void CreateClientByInterface_Should_Set_ServiceUrl_By_ServiceEndpoint_Configuration_And_RegionEndpoint_To_Null_If_Given_UseServiceUrl_Parameter_Is_True_Regardless_Of_Use_RegionName_Property_Of_SessionOptions_Has_Value_Or_Not(string? systemName) { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + 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); +#pragma warning restore CS8604 // Possible null reference argument. mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockServiceMetadata); mockSession.SessionReflectionMock.Setup(reflection => reflection.CreateClientConfig(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockClientConfig); @@ -431,9 +437,9 @@ public void CreateClientByInterface_Should_Set_ServiceUrl_By_ServiceEndpoint_Con public void CreateClientByInterface_Should_Pass_The_ClientConfig_To_SetForcePathStyle() { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + var mockServiceMetadata = new MockServiceMetadata(); var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); mockSession.SessionOptionsMock.SetupDefault(); @@ -456,9 +462,9 @@ public void CreateClientByInterface_Should_Pass_The_ClientConfig_To_SetForcePath public void CreateClientByInterface_Should_Create_AmazonServiceClient_By_Given_Generic_Type_And_Configure_ServiceUrl_Or_RegionEndpoint_By_Given_UseServiceUrl_Parameter(bool useServiceUrl) { var mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); + 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(); @@ -493,4 +499,4 @@ public void CreateClientByInterface_Should_Create_AmazonServiceClient_By_Given_G mockSession.SessionReflectionMock.Verify(reflection => reflection.CreateClientConfig(It.Is(type => type == typeof(MockAmazonServiceClient))), Times.Once); mockSession.SessionReflectionMock.Verify(reflection => reflection.SetForcePathStyle(It.Is(config => config == mockClientConfig), true), Times.Once); } -} +} \ No newline at end of file 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 1c250f2..d7d39e1 100644 --- a/tests/common/LocalStack.Tests.Common/LocalStack.Tests.Common.csproj +++ b/tests/common/LocalStack.Tests.Common/LocalStack.Tests.Common.csproj @@ -1,8 +1,8 @@ - net461;net6.0;net7.0 - $(NoWarn);CA1707;MA0006 + net472;net8.0;net9.0 + $(NoWarn);CA1707;MA0006;CA1510 @@ -10,6 +10,10 @@ + + + + @@ -17,4 +21,4 @@ - + \ No newline at end of file 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.DependencyInjection/LocalStack.Client.Sandbox.DependencyInjection.csproj b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/LocalStack.Client.Sandbox.DependencyInjection.csproj index b7fe3e8..2d6360a 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/LocalStack.Client.Sandbox.DependencyInjection.csproj +++ b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/LocalStack.Client.Sandbox.DependencyInjection.csproj @@ -1,44 +1,44 @@ ο»Ώ - - Exe - net6.0;net7.0 - $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848 - + + Exe + net8.0;net9.0 + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 + - - - - + + + + - - - PreserveNewest - appsettings.json - - - PreserveNewest - - + + + PreserveNewest + appsettings.json + + + PreserveNewest + + - - - - - - - - - + + + + + + + + + - - - + + + - - - Always - - + + + Always + + - + \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/Program.cs b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/Program.cs index f35ab5a..81d8dfa 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/Program.cs +++ b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/Program.cs @@ -65,25 +65,25 @@ Console.WriteLine("Press any key to start Sandbox application"); Console.ReadLine(); -await CreateBucketAndUploadFileAsync(amazonS3Client, bucketName, filePath, key).ConfigureAwait(false); +await CreateBucketAndUploadFileAsync(amazonS3Client, bucketName, filePath, key); static async Task CreateBucketAndUploadFileAsync(IAmazonS3 s3Client, string bucketName, string path, string key) { try { var putBucketRequest = new PutBucketRequest { BucketName = bucketName, UseClientRegion = true }; - await s3Client.PutBucketAsync(putBucketRequest).ConfigureAwait(false); + await s3Client.PutBucketAsync(putBucketRequest); Console.WriteLine("The bucket {0} created", bucketName); // Retrieve the bucket location. - string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName).ConfigureAwait(false); + string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName); Console.WriteLine("The bucket's location: {0}", bucketLocation); using var fileTransferUtility = new TransferUtility(s3Client); Console.WriteLine("Uploading the file {0}...", path); - await fileTransferUtility.UploadAsync(path, bucketName, key).ConfigureAwait(false); + await fileTransferUtility.UploadAsync(path, bucketName, key); Console.WriteLine("The file {0} created", path); } catch (AmazonS3Exception e) @@ -99,7 +99,7 @@ static async Task CreateBucketAndUploadFileAsync(IAmazonS3 s3Client, string buck static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) { var request = new GetBucketLocationRequest() { BucketName = bucketName }; - GetBucketLocationResponse response = await client.GetBucketLocationAsync(request).ConfigureAwait(false); + GetBucketLocationResponse response = await client.GetBucketLocationAsync(request); var bucketLocation = response.Location.ToString(); return bucketLocation; diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/LocalStack.Client.Sandbox.WithGenericHost.csproj b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/LocalStack.Client.Sandbox.WithGenericHost.csproj index 9864e3d..a6a6439 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/LocalStack.Client.Sandbox.WithGenericHost.csproj +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/LocalStack.Client.Sandbox.WithGenericHost.csproj @@ -1,40 +1,40 @@ ο»Ώ - - Exe - net6.0;net7.0 - latest - $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848 - + + Exe + net8.0;net9.0 + latest + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 + - - - PreserveNewest - - - PreserveNewest - appsettings.json - - + + + PreserveNewest + + + PreserveNewest + appsettings.json + + - - - - - - - - + + + + + + + + - - - - + + + + - - - Always - - + + + Always + + - + \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Program.cs b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Program.cs index a3e0243..49609d2 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Program.cs +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Program.cs @@ -22,13 +22,13 @@ }) .ConfigureLogging((_, configLogging) => { configLogging.AddConsole(); }) .UseConsoleLifetime() - .RunConsoleAsync() - .ConfigureAwait(false); + .RunConsoleAsync(); static string? GetNetCoreVersion() { Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - string[] assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + var separator = new[] { '/', '\\' }; + string[] assemblyPath = assembly.Location.Split(separator, StringSplitOptions.RemoveEmptyEntries); int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) { diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleS3Service.cs b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleS3Service.cs index 3b1504a..e77f03f 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleS3Service.cs +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleS3Service.cs @@ -1,6 +1,8 @@ -ο»Ώnamespace LocalStack.Client.Sandbox.WithGenericHost; +ο»Ώ#pragma warning disable CA1812 // Avoid uninstantiated internal classes -public class SampleS3Service : IHostedService +namespace LocalStack.Client.Sandbox.WithGenericHost; + +internal sealed class SampleS3Service : IHostedService { private const string BucketName = "localstack-sandbox-with-host"; private const string FilePath = "SampleData.txt"; @@ -22,18 +24,18 @@ public async Task StartAsync(CancellationToken cancellationToken) try { var putBucketRequest = new PutBucketRequest { BucketName = BucketName }; - await _amazonS3.PutBucketAsync(putBucketRequest, cancellationToken).ConfigureAwait(false); + await _amazonS3.PutBucketAsync(putBucketRequest, cancellationToken); _logger.LogInformation("The bucket {BucketName} created", BucketName); // Retrieve the bucket location. - string bucketLocation = await FindBucketLocationAsync(_amazonS3, BucketName).ConfigureAwait(false); + string bucketLocation = await FindBucketLocationAsync(_amazonS3, BucketName); _logger.LogInformation("The bucket's location: {BucketLocation}", bucketLocation); using var fileTransferUtility = new TransferUtility(_amazonS3); _logger.LogInformation("Uploading the file {FilePath}...", FilePath); - await fileTransferUtility.UploadAsync(FilePath, BucketName, Key, cancellationToken).ConfigureAwait(false); + await fileTransferUtility.UploadAsync(FilePath, BucketName, Key, cancellationToken); _logger.LogInformation("The file {FilePath} created", FilePath); } catch (AmazonS3Exception e) @@ -59,9 +61,9 @@ public Task StopAsync(CancellationToken cancellationToken) private static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) { var request = new GetBucketLocationRequest() { BucketName = bucketName }; - GetBucketLocationResponse response = await client.GetBucketLocationAsync(request).ConfigureAwait(false); + GetBucketLocationResponse response = await client.GetBucketLocationAsync(request); var bucketLocation = response.Location.ToString(); return bucketLocation; } -} +} \ 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 6b6d7e8..a35f787 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj +++ b/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj @@ -2,8 +2,8 @@ Exe - net461;net6.0;net7.0 - $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848 + net472;net8.0;net9.0 + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 @@ -20,4 +20,4 @@ - + \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox/Program.cs b/tests/sandboxes/LocalStack.Client.Sandbox/Program.cs index dd055fc..96e4bf2 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox/Program.cs +++ b/tests/sandboxes/LocalStack.Client.Sandbox/Program.cs @@ -32,26 +32,25 @@ Console.WriteLine("Press any key to start Sandbox application"); Console.ReadLine(); -await CreateBucketAndUploadFileAsync(amazonS3Client, bucketName, filePath, key).ConfigureAwait(false); - +await CreateBucketAndUploadFileAsync(amazonS3Client, bucketName, filePath, key); static async Task CreateBucketAndUploadFileAsync(IAmazonS3 s3Client, string bucketName, string path, string key) { try { var putBucketRequest = new PutBucketRequest { BucketName = bucketName, UseClientRegion = true }; - await s3Client.PutBucketAsync(putBucketRequest).ConfigureAwait(false); + await s3Client.PutBucketAsync(putBucketRequest); Console.WriteLine("The bucket {0} created", bucketName); // Retrieve the bucket location. - string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName).ConfigureAwait(false); + string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName); Console.WriteLine("The bucket's location: {0}", bucketLocation); using var fileTransferUtility = new TransferUtility(s3Client); Console.WriteLine("Uploading the file {0}...", path); - await fileTransferUtility.UploadAsync(path, bucketName, key).ConfigureAwait(false); + await fileTransferUtility.UploadAsync(path, bucketName, key); Console.WriteLine("The file {0} created", path); } catch (AmazonS3Exception e) @@ -67,7 +66,7 @@ static async Task CreateBucketAndUploadFileAsync(IAmazonS3 s3Client, string buck static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) { var request = new GetBucketLocationRequest() { BucketName = bucketName }; - GetBucketLocationResponse response = await client.GetBucketLocationAsync(request).ConfigureAwait(false); + GetBucketLocationResponse response = await client.GetBucketLocationAsync(request); var bucketLocation = response.Location.ToString(); return bucketLocation; diff --git a/tests/sandboxes/LocalStack.Container/LocalStack.Container.csproj b/tests/sandboxes/LocalStack.Container/LocalStack.Container.csproj index b3bd0b5..8e92cd2 100644 --- a/tests/sandboxes/LocalStack.Container/LocalStack.Container.csproj +++ b/tests/sandboxes/LocalStack.Container/LocalStack.Container.csproj @@ -2,9 +2,9 @@ Exe - net7.0 + net9.0 latest - $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848 + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 @@ -12,4 +12,4 @@ - + \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Container/Program.cs b/tests/sandboxes/LocalStack.Container/Program.cs index fca686a..0359e91 100644 --- a/tests/sandboxes/LocalStack.Container/Program.cs +++ b/tests/sandboxes/LocalStack.Container/Program.cs @@ -2,7 +2,7 @@ Console.ReadLine(); string containerId = Guid.NewGuid().ToString().ToUpperInvariant(); -LocalStackBuilder localStackBuilder = new LocalStackBuilder().WithImage($"localstack/localstack:latest") +LocalStackBuilder localStackBuilder = new LocalStackBuilder().WithImage($"localstack/localstack:4.6.0") .WithName($"localStack-latest-{containerId}") .WithEnvironment("DOCKER_HOST", "unix:///var/run/docker.sock") .WithEnvironment("DEBUG", "1") @@ -12,14 +12,13 @@ LocalStackContainer container = localStackBuilder.Build(); - Console.WriteLine("Starting LocalStack Container"); -await container.StartAsync().ConfigureAwait(false); +await container.StartAsync(); Console.WriteLine("LocalStack Container started"); Console.WriteLine("Press any key to stop LocalStack container"); Console.ReadLine(); Console.WriteLine("Stopping LocalStack Container"); -await container.DisposeAsync().ConfigureAwait(false); +await container.DisposeAsync(); Console.WriteLine("LocalStack Container stopped"); \ No newline at end of file