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 new file mode 100644 index 0000000..f580012 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,206 @@ +# Contributing to LocalStack .NET Client + +🎉 **Thank you for your interest in 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. + +## 📋 Quick Reference + +- 🐛 **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) + +## 🤝 Code of Conduct + +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). + +## 📝 Contributor License Agreement (CLA) + +**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. + +## 🎯 Version Strategy + +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) + +When contributing, please specify which version track your contribution targets. + +## 🚀 Getting Started + +### Prerequisites + +- [.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/) + +### Development Environment Setup + +1. **Fork and Clone** + + ```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 + ``` + +2. **Build the Project** + + ```bash + # Windows + .\build.ps1 + + # Linux/macOS + ./build.sh + ``` + +3. **Run Tests** + + ```bash + # All tests (requires Docker for functional tests) + .\build.ps1 --target tests + + # Unit/Integration tests only + .\build.ps1 --target tests --skipFunctionalTest true + ``` + +## 🐛 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 feature/your-feature-name + # or + git checkout -b fix/issue-number-description + ``` + +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 + +3. **Test thoroughly** + + ```bash + # Run all tests + ./build.sh --target tests + + # Test specific scenarios with LocalStack + # (see sandbox projects for examples) + ``` + +4. **Commit with conventional commits** + + ```bash + 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" + ``` + +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.** + +Thank you for making LocalStack .NET Client better! 🚀 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/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 new file mode 100644 index 0000000..11a75aa --- /dev/null +++ b/.github/workflows/publish-nuget.yml @@ -0,0 +1,133 @@ +name: "Manual Package Publishing" + +on: + workflow_dispatch: + inputs: + 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: "github" + options: + - nuget + - github + package-id: + type: choice + description: Package Id + required: true + default: "LocalStack.Client" + options: + - LocalStack.Client + - LocalStack.Client.Extensions + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_NOLOGO: true + +jobs: + 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@v4 + + - name: "Setup .NET SDK" + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + + - name: "Cache NuGet packages" + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj', '**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: "Make build script executable" + run: chmod +x ./build.sh + + - name: "Build & Test" + run: ./build.sh --target tests --skipFunctionalTest true + + - name: "Print Package Information" + run: | + echo "📦 Package: ${{ github.event.inputs.package-id }}" + echo "🏷️ Version: ${{ github.event.inputs.package-version }}" + echo "🎯 Target: ${{ github.event.inputs.package-source }}" + echo "🔗 Repository: ${{ github.repository }}" + + - name: "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: | + ./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: "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: "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: "Publish to NuGet.org" + if: ${{ github.event.inputs.package-source == 'nuget' }} + 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/.gitignore b/.gitignore index bd2b3f0..a961f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ +artifacts-extensions/ **/Properties/launchSettings.json # StyleCop @@ -333,4 +334,10 @@ testrunner # Cake [Tt]ools/ -.ionide \ No newline at end of file +.ionide + +!tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Properties/launchSettings.json + + +# Visual Studio Code options directory +.vscode/ \ No newline at end of file diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..345f41d --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System; + +[assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/BannedSymbols.txt b/BannedSymbols.txt new file mode 100644 index 0000000..ed211ba --- /dev/null +++ b/BannedSymbols.txt @@ -0,0 +1,4 @@ +# https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md +P:System.DateTime.Now;Use System.DateTime.UtcNow instead +P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead +P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..92632c8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,77 @@ +# LocalStack .NET Client v2.x Change Log + +This document outlines the changes, updates, and important notes for the LocalStack .NET Client v2.x series, including the latest preview release. + +See v1.x change log for previous versions: [CHANGELOG.md](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/sdkv3-lts/CHANGELOG.md) + +## [v2.0.0](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v2.0.0) + +> **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. + +### ✨ New features (since `v2.0.0‑preview1`) + +- **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** + +### 🛠️ General + +- **Testing Compatibility:** + - Successfully tested against LocalStack versions: + - **v3.7.1** + - **v4.6.0** + +*See [`v2.0.0-preview1`](#v200-preview1) for the complete migration from v1.x and the AWS SDK v4 overhaul.* + +--- + +## [v2.0.0-preview1](https://github.com/localstack-dotnet/localstack-dotnet-client/releases/tag/v2.0.0-preview1) + +### 1. Breaking Changes + +- **Framework Support Updates:** + - **Deprecated** support for **.NET Framework 4.6.2**. + - **Added** support for **.NET Framework 4.7.2** (required for AWS SDK v4 compatibility). + +### 2. General + +- **AWS SDK v4 Migration:** + - **Complete migration** from AWS SDK for .NET v3 to v4. + - **AWSSDK.Core** minimum version set to **4.0.0.15**. + - **AWSSDK.Extensions.NETCore.Setup** updated to **4.0.2**. + - All 70+ AWS SDK service packages updated to v4.x series. + +- **Framework Support:** + - **.NET 9** + - **.NET 8** + - **.NET Standard 2.0** + - **.NET Framework 4.7.2** + +- **Testing Validation:** + - **1,099 total tests** passing across all target frameworks. + - Successfully tested with AWS SDK v4 across all supported .NET versions. + - Tested against following LocalStack versions: + - **v3.7.1** + - **v4.3.0** + +### 3. Important Notes + +- **Preview Release**: This is a preview release for early adopters and testing. See the [v2.0.0 Roadmap & Migration Guide](https://github.com/localstack-dotnet/localstack-dotnet-client/discussions/45) for the complete migration plan. +- **No API Changes**: LocalStack.NET public APIs remain unchanged. All changes are internal to support AWS SDK v4 compatibility. +- **Feedback Welcome**: Please report issues or feedback on [GitHub Issues](https://github.com/localstack-dotnet/localstack-dotnet-client/issues). +- **v2.x series requires AWS SDK v4**: This version is only compatible with AWS SDK for .NET v4.x packages. +- **Migration from v1.x**: Users upgrading from v1.x should ensure their projects reference AWS SDK v4 packages. +- **Framework Requirement**: .NET Framework 4.7.2 or higher is now required (upgrade from 4.6.2). + +--- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 87c7171..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,113 +0,0 @@ -# Contributing to [LocalStack.NET Client](https://github.com/localstack-dotnet/localstack-dotnet-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. - -Please read these guidelines before contributing to LocalStack.NET Client: - - - [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) - -## Got a Question or Problem? - -If you have questions about how to use LocalStack.NET Client, you can ask by submitting an issue to the [GitHub Repository][github] - -## Found an Issue? - -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. - -When submitting an issue please include the following information: - -- 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. - -The more information you include about the issue, the more likely it is to be fixed! - - -## Want a Feature? - -You can request a new feature by submitting an issue to the [GitHub Repository][github] - -## Submitting a Pull Request - -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: - -- 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 - -Read [GitHub Help][pullrequesthelp] for more details about creating pull requests. - -### 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: - -1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, - and configure the remotes: - - ```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 - ``` - -2. If you cloned a while ago, get the latest changes from upstream: - - ```bash - git checkout master - git pull upstream master - ``` - -3. Create a new topic branch (off the main project development branch) to - contain your feature, change, or fix: - - ```bash - git checkout -b - ``` - -4. Commit your changes in logical chunks. Use Git's - [interactive rebase](https://help.github.com/articles/interactive-rebase) - feature to tidy up your commits before making them public. - -5. Locally merge (or rebase) the upstream development branch into your topic branch: - - ```bash - git pull [--rebase] upstream master - ``` - -6. Push your topic branch up to your fork: - - ```bash - git push origin - ``` - -7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) - with a clear title and description against the `master` branch. - - -[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 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..2c1d5d2 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,94 @@ + + + LocalStack.NET, Deniz İrgin + LocalStack.NET + LocalStack.NET + https://github.com/localstack-dotnet/localstack-dotnet-client + localstack-dotnet-square.png + 2.0.0 + 2.0.0 + true + snupkg + 13.0 + true + latest + true + true + true + true + true + enable + + $(NoWarn); + + + $(NoError); + + true + All + strict + true + + + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..2ebfc00 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +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/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index f8b8ed9..0000000 --- a/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/LICENSE b/LICENSE index f83905e..f9478d1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 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 b/LocalStack.sln new file mode 100644 index 0000000..51ed6dc --- /dev/null +++ b/LocalStack.sln @@ -0,0 +1,123 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client", "src\LocalStack.Client\LocalStack.Client.csproj", "{22C080D7-929C-44F1-909C-831EF9D2810F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandboxes", "sandboxes", "{962A750E-8FE2-461F-B3FC-2B401309B5FD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3F0F4BAA-02EF-4008-9CF8-E73AA92D4664}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Sandbox", "tests\sandboxes\LocalStack.Client.Sandbox\LocalStack.Client.Sandbox.csproj", "{E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Tests", "tests\LocalStack.Client.Tests\LocalStack.Client.Tests.csproj", "{9FC6CABE-ED38-4048-B511-69D76870ABF8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Integration.Tests", "tests\LocalStack.Client.Integration.Tests\LocalStack.Client.Integration.Tests.csproj", "{691A4094-2074-474A-81A3-E33B728AE54E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Sandbox.DependencyInjection", "tests\sandboxes\LocalStack.Client.Sandbox.DependencyInjection\LocalStack.Client.Sandbox.DependencyInjection.csproj", "{4E90D3D1-D570-4205-9C6E-B917B5508912}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{06034ACF-97AD-4266-8E46-42B1804C89B6}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build-macos.yml = .github\workflows\build-macos.yml + .github\workflows\build-ubuntu.yml = .github\workflows\build-ubuntu.yml + .github\workflows\build-windows.yml = .github\workflows\build-windows.yml + build.ps1 = build.ps1 + build.sh = build.sh + .github\workflows\publish-nuget.yml = .github\workflows\publish-nuget.yml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Extensions", "src\LocalStack.Client.Extensions\LocalStack.Client.Extensions.csproj", "{74035094-A726-44E2-9B88-42D6425D8548}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Sandbox.WithGenericHost", "tests\sandboxes\LocalStack.Client.Sandbox.WithGenericHost\LocalStack.Client.Sandbox.WithGenericHost.csproj", "{350EF226-D0CE-4C8C-83D1-22E638F46862}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Extensions.Tests", "tests\LocalStack.Client.Extensions.Tests\LocalStack.Client.Extensions.Tests.csproj", "{0F24D1F8-DB6B-439E-BD6D-23E8DA88615A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Functional.Tests", "tests\LocalStack.Client.Functional.Tests\LocalStack.Client.Functional.Tests.csproj", "{A697D9A2-4DF7-4B4D-A189-EEC7F64B5609}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Container", "tests\sandboxes\LocalStack.Container\LocalStack.Container.csproj", "{6CCFBCE0-C7C6-42A7-B39F-665B4C15C6FE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{152F3084-DC30-4A44-AEBC-E4C0EBFA0F4E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Build", "build\LocalStack.Build\LocalStack.Build.csproj", "{2CA18A71-CA83-4CC4-A777-AA4F56E4413F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{FF7B2686-CC4B-4B6C-B360-E487339DB210}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalStack.Tests.Common", "tests\common\LocalStack.Tests.Common\LocalStack.Tests.Common.csproj", "{7B896BF0-E9E1-44B7-9268-78A6B45CFE0D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {22C080D7-929C-44F1-909C-831EF9D2810F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22C080D7-929C-44F1-909C-831EF9D2810F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22C080D7-929C-44F1-909C-831EF9D2810F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22C080D7-929C-44F1-909C-831EF9D2810F}.Release|Any CPU.Build.0 = Release|Any CPU + {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Release|Any CPU.Build.0 = Release|Any CPU + {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Release|Any CPU.Build.0 = Release|Any CPU + {691A4094-2074-474A-81A3-E33B728AE54E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {691A4094-2074-474A-81A3-E33B728AE54E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {691A4094-2074-474A-81A3-E33B728AE54E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {691A4094-2074-474A-81A3-E33B728AE54E}.Release|Any CPU.Build.0 = Release|Any CPU + {4E90D3D1-D570-4205-9C6E-B917B5508912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E90D3D1-D570-4205-9C6E-B917B5508912}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E90D3D1-D570-4205-9C6E-B917B5508912}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E90D3D1-D570-4205-9C6E-B917B5508912}.Release|Any CPU.Build.0 = Release|Any CPU + {74035094-A726-44E2-9B88-42D6425D8548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74035094-A726-44E2-9B88-42D6425D8548}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74035094-A726-44E2-9B88-42D6425D8548}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74035094-A726-44E2-9B88-42D6425D8548}.Release|Any CPU.Build.0 = Release|Any CPU + {350EF226-D0CE-4C8C-83D1-22E638F46862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {350EF226-D0CE-4C8C-83D1-22E638F46862}.Debug|Any CPU.Build.0 = Debug|Any CPU + {350EF226-D0CE-4C8C-83D1-22E638F46862}.Release|Any CPU.ActiveCfg = Release|Any CPU + {350EF226-D0CE-4C8C-83D1-22E638F46862}.Release|Any CPU.Build.0 = Release|Any CPU + {0F24D1F8-DB6B-439E-BD6D-23E8DA88615A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F24D1F8-DB6B-439E-BD6D-23E8DA88615A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F24D1F8-DB6B-439E-BD6D-23E8DA88615A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F24D1F8-DB6B-439E-BD6D-23E8DA88615A}.Release|Any CPU.Build.0 = Release|Any CPU + {A697D9A2-4DF7-4B4D-A189-EEC7F64B5609}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A697D9A2-4DF7-4B4D-A189-EEC7F64B5609}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A697D9A2-4DF7-4B4D-A189-EEC7F64B5609}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A697D9A2-4DF7-4B4D-A189-EEC7F64B5609}.Release|Any CPU.Build.0 = Release|Any CPU + {6CCFBCE0-C7C6-42A7-B39F-665B4C15C6FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CCFBCE0-C7C6-42A7-B39F-665B4C15C6FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CCFBCE0-C7C6-42A7-B39F-665B4C15C6FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CCFBCE0-C7C6-42A7-B39F-665B4C15C6FE}.Release|Any CPU.Build.0 = Release|Any CPU + {2CA18A71-CA83-4CC4-A777-AA4F56E4413F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CA18A71-CA83-4CC4-A777-AA4F56E4413F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CA18A71-CA83-4CC4-A777-AA4F56E4413F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CA18A71-CA83-4CC4-A777-AA4F56E4413F}.Release|Any CPU.Build.0 = Release|Any CPU + {7B896BF0-E9E1-44B7-9268-78A6B45CFE0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B896BF0-E9E1-44B7-9268-78A6B45CFE0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B896BF0-E9E1-44B7-9268-78A6B45CFE0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B896BF0-E9E1-44B7-9268-78A6B45CFE0D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {962A750E-8FE2-461F-B3FC-2B401309B5FD} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} + {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D} = {962A750E-8FE2-461F-B3FC-2B401309B5FD} + {9FC6CABE-ED38-4048-B511-69D76870ABF8} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} + {691A4094-2074-474A-81A3-E33B728AE54E} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} + {4E90D3D1-D570-4205-9C6E-B917B5508912} = {962A750E-8FE2-461F-B3FC-2B401309B5FD} + {350EF226-D0CE-4C8C-83D1-22E638F46862} = {962A750E-8FE2-461F-B3FC-2B401309B5FD} + {0F24D1F8-DB6B-439E-BD6D-23E8DA88615A} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} + {A697D9A2-4DF7-4B4D-A189-EEC7F64B5609} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} + {6CCFBCE0-C7C6-42A7-B39F-665B4C15C6FE} = {962A750E-8FE2-461F-B3FC-2B401309B5FD} + {2CA18A71-CA83-4CC4-A777-AA4F56E4413F} = {152F3084-DC30-4A44-AEBC-E4C0EBFA0F4E} + {FF7B2686-CC4B-4B6C-B360-E487339DB210} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} + {7B896BF0-E9E1-44B7-9268-78A6B45CFE0D} = {FF7B2686-CC4B-4B6C-B360-E487339DB210} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E4925255-67AA-4095-816B-CC10A5490E71} + EndGlobalSection +EndGlobal diff --git a/src/LocalStack.sln.DotSettings b/LocalStack.sln.DotSettings similarity index 97% rename from src/LocalStack.sln.DotSettings rename to LocalStack.sln.DotSettings index 13520b7..974f95b 100644 --- a/src/LocalStack.sln.DotSettings +++ b/LocalStack.sln.DotSettings @@ -246,11 +246,14 @@ <Configurator><ConnectList /></Configurator> True 12 + True + True True True True True True + True True True True @@ -261,7 +264,7 @@ <data><AttributeFilter ClassMask="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" IsEnabled="True" /><AttributeFilter ClassMask="System.CodeDom.Compiler.GeneratedCodeAttribute" IsEnabled="True" /><AttributeFilter ClassMask="Microsoft.VisualStudio.TestPlatform.TestSDKAutoGeneratedCode*" IsEnabled="True" /></data> True True - anonymous + True 90,-320 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 d6d848b..8343b2b 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,300 @@ -# LocalStack .Net Core and .Net Framework Client +# LocalStack .NET Client -![LocalStack](https://github.com/localstack-dotnet/localstack-dotnet-client/blob/master/assets/localstack-dotnet.png?raw=true) +[![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 -This is an easy-to-use .NET client for [LocalStack](https://github.com/localstack/localstack). -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. +**v2.0.0** is live with complete AWS SDK v4 support - easier migration than expected! -## Continuous integration +- ✅ **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)** -| Build server | Platform | Build status | -|----------------- |---------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Azure Pipelines | Ubuntu | [![Build Status](https://denizirgindev.visualstudio.com/localstack-dotnet-client/_apis/build/status/Ubuntu?branchName=master)](https://denizirgindev.visualstudio.com/localstack-dotnet-client/_build/latest?definitionId=8&branchName=master) | -| Azure Pipelines | macOs | [![Build Status](https://denizirgindev.visualstudio.com/localstack-dotnet-client/_apis/build/status/macOS?branchName=master)](https://denizirgindev.visualstudio.com/localstack-dotnet-client/_build/latest?definitionId=10&branchName=master) | -| Azure Pipelines | Windows | [![Build Status](https://denizirgindev.visualstudio.com/localstack-dotnet-client/_apis/build/status/Windows?branchName=master)](https://denizirgindev.visualstudio.com/localstack-dotnet-client/_build/latest?definitionId=9&branchName=master) | +**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. + +## 🚀 Platform Compatibility & Quality Status + +### Supported Platforms + +- [.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 1. [Supported Platforms](#supported-platforms) -2. [Prerequisites](#prerequisites) -3. [Installation](#installation) -4. [Usage](#usage) - - [Standalone Initialization](#standalone-initialization) - - [Microsoft.Extensions.DependencyInjection Initialization](#di) - - [Create and use AWS SDK Clients](#create-client) -5. [Developing](#developing) -6. [License](#license) +2. [Why LocalStack.NET Client?](#why-localstacknet-client) +3. [Prerequisites](#prerequisites) +4. [Getting Started](#getting-started) + - [Setup](#setup) + - [Configuration](#configuration) +5. [Known Issues](#known-issues) +6. [Developing](#developing) + - [Building the Project](#building-the-project) + - [Sandbox Applications](#sandbox-applications) + - [Running Tests](#running-tests) +7. [Changelog](#changelog) +8. [License](#license) -## Supported Platforms +## Why LocalStack.NET Client? -* .NET 4.6.1 (Desktop / Server) -* [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) +- **Consistent Client Configuration:** LocalStack.NET eliminates the need for manual endpoint configuration, providing a standardized and familiar approach to initializing clients. -## Prerequisites +- **Adaptable Environment Transition:** Easily switch between LocalStack and actual AWS services with minimal configuration changes. -To make use of this library, you need to have [LocalStack](https://github.com/localstack/localstack) -installed on your local machine. In particular, the `localstack` command needs to be available. +- **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. -## Installation +- **Reduced Learning Curve:** Offers a familiar interface tailored for LocalStack, ideal for developers acquainted with the AWS SDK for .NET. -The easiest way to install *LocalStack .NET Client* is via `nuget`: +- **Enhanced Development Speed:** Reduces boilerplate and manual configurations, speeding up the development process. -``` -Install-Package LocalStack.Client -``` +## Prerequisites -Or use `dotnet cli` +To utilize this library, you need to have LocalStack running. While LocalStack can be installed directly on your machine and accessed via the localstack cli, the recommended approach is to run LocalStack using [Docker](https://docs.docker.com/get-docker/) or [docker-compose](https://docs.docker.com/compose/install/). -``` -dotnet add package LocalStack.Client +For detailed installation and setup instructions, please refer to the [official LocalStack installation guide](https://docs.localstack.cloud/getting-started/installation/). + +## Getting Started + +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 +# 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 ``` -| Stable | Nightly | -|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![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) | +#### Development Builds (GitHub Packages) -## Usage +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 +``` -This library provides a thin wrapper around [aws-sdk-net](https://github.com/aws/aws-sdk-net). -Therefore the usage of this library is same as using `AWS SDK for .NET`. +> **🔑 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. -See [Getting Started with the AWS SDK for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-setup.html) +Refer to [documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Getting-Started#installation) for more information on how to install LocalStack.NET. -This library can be used with any DI library, or it can be used as standalone. +`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). -### Standalone Initialization +### Setup -If you do not want to use any DI library, you have to instantiate `SessionStandalone` as follows. +Here's a basic example of how to setup `LocalStack.NET`: ```csharp -var awsAccessKeyId = "Key Id"; -var awsAccessKey = "Secret Key"; -var awsSessionToken = "Token"; -var regionName = "us-west-1"; -var localStackHost = "localhost"; - -ISession session = SessionStandalone - .Init() - .WithSessionOptions(awsAccessKeyId, awsAccessKey, awsSessionToken, regionName) - .WithConfig(localStackHost) - .Create(); +public void ConfigureServices(IServiceCollection services) +{ + // Add framework services. + services.AddMvc(); + + services.AddLocalStack(Configuration) + services.AddDefaultAWSOptions(Configuration.GetAWSOptions()); + services.AddAwsService(); + services.AddAwsService(); +} ``` -All parameters are optional and in case of passed `null`, default values will be setted. Since its workin on local machine, the real aws credantials are not needed for `awsAccessKeyId`, `awsAccessKey`, `awsSessionToken` parameters. +The `AddLocalStack` method integrates LocalStack.NET into your application, and the `AddAwsService` method allows you to specify which AWS services you want to use with LocalStack. + +(Alternatively, `AddAWSServiceLocalStack` method can be used to prevent mix-up with `AddAWSService`.) + +`AddLocalStack` extension method is responsible for both configurations and adding of `LocalStack.Client` dependencies to service collection. + +### Configuration -### Microsoft.Extensions.DependencyInjection Initialization +To configure LocalStack.NET, you can use entries in the appsettings.json files. Here's a basic example for different environments: -First, you need to install `Microsoft.Extensions.DependencyInjection` nuget package as follows +`appsettings.Development.json` +```json +"LocalStack": { + "UseLocalStack": true, + "Session": { + "RegionName": "eu-central-1" + }, + "Config": { + "LocalStackHost": "localhost.localstack.cloud", // or "localhost", + "EdgePort": 4566 + } +} ``` -dotnet add package Microsoft.Extensions.DependencyInjection + +`appsettings.Production.json` + +```json +"LocalStack": { + "UseLocalStack": false +}, +"AWS": { + "Profile": "", + "Region": "eu-central-1" +} ``` -Register necessary dependencies to `ServiceCollection` as follows +The `RegionName` is important as LocalStack creates resources based on the specified region. For more advanced configurations and understanding how LocalStack.NET operates with LocalStack, refer to [documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Setup#configuration). -```csharp -var collection = new ServiceCollection(); +## Known Issues -var awsAccessKeyId = "Key Id"; -var awsAccessKey = "Secret Key"; -var awsSessionToken = "Token"; -var regionName = "us-west-1"; -var localStackHost = "localhost"; +- **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. -collection - .AddScoped(provider => - new SessionOptions(awsAccessKeyId, awsAccessKey, awsSessionToken, regionName)) - .AddScoped(provider => new Config(localStackHost)) - .AddScoped() - .AddScoped(); +- **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. -ServiceProvider serviceProvider = collection.BuildServiceProvider(); +- **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. -var session = serviceProvider.GetRequiredService(); +```csharp +Environment.SetEnvironmentVariable("AWS_SERVICE_URL", string.Empty); ``` -All parameters are optional and in case of passed `null`, default values will be setted. Since its workin on local machine, the real aws credantials are not needed for `awsAccessKeyId`, `awsAccessKey`, `awsSessionToken` parameters. +- **IAmazonLambda Operations:** There's a general issue with `IAmazonLambda` operations. This matter is currently under investigation. -### Create and use AWS SDK Clients +- **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. -For all local services supported by [LocalStack](https://github.com/localstack/localstack#overview), the corresponding [AWSSDK packages](https://www.nuget.org/profiles/awsdotnet) can be use. +## Developing -The following example shows how to use [AWSSDK.S3](https://www.nuget.org/packages/AWSSDK.S3/) with `LocalStack.NET Client` +We appreciate contributions in the form of feedback, bug reports, and pull requests. -```csharp -var amazonS3Client = session.CreateClient(); +### Building the Project -ListBucketsResponse listBucketsResponse = await amazonS3Client.ListBucketsAsync(); +To build the project, use the following commands based on your operating system: -const string bucketName = "test-bucket-3"; +Windows -if (!await AmazonS3Util.DoesS3BucketExistAsync(amazonS3Client, bucketName)) -{ - PutBucketResponse putBucketResponse = await amazonS3Client.PutBucketAsync(bucketName); -} +```powershell +build.ps1 +``` -var fileTransferUtility = new TransferUtility(amazonS3Client); +Linux -await fileTransferUtility.UploadAsync("SampleData.txt", bucketName, "SampleData.txt"); -GetObjectResponse getObjectResponse = await amazonS3Client.GetObjectAsync(bucketName, "SampleData.txt"); +```bash +./build.sh ``` -See [sandbox projects](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master/tests/sandboxes) for more examples. +### Sandbox Applications -## Developing +The LocalStack .NET repository includes several sandbox console applications located in [tests/sandboxes](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master/tests/sandboxes). These applications serve both as testing tools and as examples. Refer to [the documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Developing#sandbox-applications) for more information -We welcome feedback, bug reports, and pull requests! +### Running Tests -Use these commands to get you started and test your code: +To execute the tests, use the commands below: Windows -``` -build.ps1 + +```powershell +build.ps1 --target=tests ``` Linux + +```bash +./build.sh --target=tests ``` -./build.sh -``` - +## License -## License Licensed under MIT, see [LICENSE](LICENSE) for the full text. diff --git a/build.cake b/build.cake deleted file mode 100644 index ae4430e..0000000 --- a/build.cake +++ /dev/null @@ -1,257 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using System.Diagnostics; - -var target = Argument("target", "default"); -var configuration = Argument("config", "Release"); -var buildNumber = Argument("buildnumber", 1); - -var artifactOutput = "./artifacts"; -var testResults = "results.trx"; -string projectPath = "./src/LocalStack.Client/LocalStack.Client.csproj"; - - -Task("Default") - .IsDependentOn("init") - .IsDependentOn("tests"); - -Task("init") - .Description("Initialize task prerequisites") - .Does(() => - { - StartProcess("dotnet", new ProcessSettings { - Arguments = "--info" - }); - - if(IsRunningOnUnix()) - { - StartProcess("git", new ProcessSettings { - Arguments = "config --global core.autocrlf true" - }); - - StartProcess("mono", new ProcessSettings { - Arguments = "--version" - }); - - InstallXUnitNugetPackage(); - } - }); - -Task("compile") - .Description("Builds all the projects in the solution") - .Does(() => - { - string slnPath = "./src/LocalStack.sln"; - - DotNetCoreBuildSettings settings = new DotNetCoreBuildSettings(); - settings.Configuration = configuration; - DotNetCoreBuild(slnPath, settings); - }); - -Task("tests") - .Description("Run Tests") - .IsDependentOn("compile") - .Does(() => - { - DotNetCoreTestSettings settings = new DotNetCoreTestSettings(); - settings.NoRestore = true; - settings.NoBuild = true; - settings.Configuration = configuration; - - IList testProjMetadatas = GetProjMetadata(); - - foreach (var testProj in testProjMetadatas) - { - string testProjectPath = testProj.CsProjPath; - - Warning($"Target Frameworks {string.Join(" ",testProj.TargetFrameworks)}"); - - foreach(string targetFramework in testProj.TargetFrameworks) - { - Warning($"Running {targetFramework.ToUpper()} tests for {testProj.AssemblyName}"); - settings.Framework = targetFramework; - - if(IsRunningOnUnix() && targetFramework == "net461") - { - RunXunitUsingMono(targetFramework, $"{testProj.DirectoryPath}/bin/{configuration}/{targetFramework}/{testProj.AssemblyName}.dll"); - } - else - { - string testFilePrefix = targetFramework.Replace(".","-"); - settings.ArgumentCustomization = args => args.Append($" --logger \"trx;LogFileName={testFilePrefix}_{testResults}\""); - DotNetCoreTest(testProjectPath, settings); - } - } - } - }); - -Task("nuget-pack") - .Does(() => - { - string outputDirectory = MakeAbsolute(Directory(artifactOutput)).FullPath; - string projectFullPath = MakeAbsolute(File(projectPath)).FullPath; - - if(!System.IO.Directory.Exists(outputDirectory)) - { - System.IO.Directory.CreateDirectory(outputDirectory); - } - - var settings = new DotNetCorePackSettings(); - settings.Configuration = configuration; - settings.OutputDirectory = artifactOutput; - settings.MSBuildSettings = new DotNetCoreMSBuildSettings(); - settings.MSBuildSettings.SetVersion(GetProjectVersion()); - - DotNetCorePack(projectFullPath, settings); - }); - -Task("get-version") - .Description("Get version") - .Does(() => - { - Warning(GetProjectVersion()); - }); - -RunTarget(target); - -/* -/ HELPER METHODS -*/ -private void InstallXUnitNugetPackage() -{ - NuGetInstallSettings nugetInstallSettings = new NuGetInstallSettings(); - nugetInstallSettings.Version = "2.4.1"; - nugetInstallSettings.Verbosity = NuGetVerbosity.Normal; - nugetInstallSettings.OutputDirectory = "testrunner"; - nugetInstallSettings.WorkingDirectory = "."; - - NuGetInstall("xunit.runner.console", nugetInstallSettings); -} - -private void RunXunitUsingMono(string targetFramework, string assemblyPath) -{ - int exitCode = StartProcess("mono", new ProcessSettings { - Arguments = $"./testrunner/xunit.runner.console.2.4.1/tools/{targetFramework}/xunit.console.exe {assemblyPath}" - }); - - if(exitCode != 0) - { - throw new InvalidOperationException($"Exit code: {exitCode}"); - } -} - -private IList GetProjMetadata() -{ - var testsRoot = MakeAbsolute(Directory("./tests/")); - var csProjs = GetFiles($"{testsRoot}/**/*.csproj").Where(fp => fp.FullPath.EndsWith("Tests.csproj")).ToList(); - - IList testProjMetadatas = new List(); - - foreach (var csProj in csProjs) - { - string csProjPath = csProj.FullPath; - - string[] targetFrameworks = GetProjectTargetFrameworks(csProjPath); - string directoryPath = csProj.GetDirectory().FullPath; - string assemblyName = GetAssemblyName(csProjPath); - - var testProjMetadata = new TestProjMetadata(directoryPath, csProjPath, targetFrameworks, assemblyName); - testProjMetadatas.Add(testProjMetadata); - } - - return testProjMetadatas; -} - -private string[] GetProjectTargetFrameworks(string csprojPath) -{ - var file = MakeAbsolute(File(csprojPath)); - var project = System.IO.File.ReadAllText(file.FullPath, Encoding.UTF8); - - bool multipleFrameworks = project.Contains(""); - string startElement = multipleFrameworks ? "" : ""; - string endElement = multipleFrameworks ? "" : ""; - - int startIndex = project.IndexOf(startElement) + startElement.Length; - int endIndex = project.IndexOf(endElement, startIndex); - - string targetFrameworks = project.Substring(startIndex, endIndex - startIndex); - return targetFrameworks.Split(';'); -} - -private string GetAssemblyName(string csprojPath) -{ - var file = MakeAbsolute(File(csprojPath)); - var project = System.IO.File.ReadAllText(file.FullPath, Encoding.UTF8); - - bool assemblyNameElementExists = project.Contains(""); - - string assemblyName = string.Empty; - - if(assemblyNameElementExists) - { - int startIndex = project.IndexOf("") + "".Length; - int endIndex = project.IndexOf("", startIndex); - - assemblyName = project.Substring(startIndex, endIndex - startIndex); - } - else - { - int startIndex = csprojPath.LastIndexOf("/") + 1; - int endIndex = csprojPath.IndexOf(".csproj", startIndex); - - assemblyName = csprojPath.Substring(startIndex, endIndex - startIndex); - } - - return assemblyName; -} - -private void UpdateProjectVersion(string version) -{ - Information("Setting version to " + version); - - if(string.IsNullOrWhiteSpace(version)) - { - throw new CakeException("No version specified! You need to pass in --targetversion=\"x.y.z\""); - } - - var file = MakeAbsolute(File("./src/Directory.Build.props")); - - Information(file.FullPath); - - var project = System.IO.File.ReadAllText(file.FullPath, Encoding.UTF8); - - var projectVersion = new Regex(@".+<\/Version>"); - project = projectVersion.Replace(project, string.Concat("", version, "")); - - System.IO.File.WriteAllText(file.FullPath, project, Encoding.UTF8); -} - -private string GetProjectVersion() -{ - var file = MakeAbsolute(File("./src/Directory.Build.props")); - - Information(file.FullPath); - - var project = System.IO.File.ReadAllText(file.FullPath, Encoding.UTF8); - int startIndex = project.IndexOf("") + "".Length; - int endIndex = project.IndexOf("", startIndex); - - string version = project.Substring(startIndex, endIndex - startIndex); - version = $"{version}.{buildNumber}"; - - return version; -} - -/* -/ MODELS -*/ -public class TestProjMetadata -{ - public TestProjMetadata(string directoryPath, string csProjPath, string[] targetFrameworks, string assemblyName) - => (DirectoryPath, CsProjPath, TargetFrameworks, AssemblyName) = (directoryPath, csProjPath, targetFrameworks, assemblyName); - - public string DirectoryPath { get; } - public string CsProjPath { get; } - public string AssemblyName { get; set; } - public string[] TargetFrameworks { get; } -} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index dc223b7..455c631 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,128 +1,3 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file is based on https://github.com/cake-build/resources modified for Cake.CoreCLR -# Feel free to change this file to fit your needs. -########################################################################## - -<# -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER ShowDescription -Shows description about tasks. -.PARAMETER DryRun -Performs a dry run. -.PARAMETER Experimental -Uses the nightly builds of the Roslyn script engine. -.PARAMETER Mono -Uses the Mono Compiler rather than the Roslyn script engine. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. -.LINK -https://cakebuild.net -#> - -[CmdletBinding()] -Param( - [string]$Script = "build.cake", - [string]$Target, - [string]$Configuration, - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity, - [switch]$ShowDescription, - [Alias("WhatIf", "Noop")] - [switch]$DryRun, - [switch]$Experimental, - [switch]$Mono, - [switch]$SkipToolPackageRestore, - [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)] - [string[]]$ScriptArgs -) - -Write-Host "Preparing to run build script..." - -if (!$PSScriptRoot) { - $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -} - -$CAKE_VERSION = "0.37.0" - -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$CAKE_ROOT = Join-Path $TOOLS_DIR "/cake.coreclr/" -$CAKE_EXE = Join-Path $CAKE_ROOT "/Cake.dll" - -# Make sure that dotnet core installed. -try { - dotnet --version -} -catch { - Throw "Error: dotnet is not installed." -} - -# Install cake if its not installed -if (!(Test-Path $CAKE_EXE)) { - # Make sure tools folder exists - if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null - } - - # Make sure that tools.csproj exist. - if (!(Test-Path "$TOOLS_DIR/tools.csproj")) { - Write-Verbose -Message "Creating tools.csproj..." - try { - New-Item "$TOOLS_DIR/tools.csproj" -ItemType file - "Exenetcoreapp3.1" | Out-File -FilePath "$TOOLS_DIR/tools.csproj" -Append - } - catch { - Throw "Could not download packages.config." - } - } - - $CAKE_NFW = Join-Path $TOOLS_DIR "/cake/" - - # Add dependencies - dotnet add $TOOLS_DIR/tools.csproj package Cake.CoreCLR -v $CAKE_VERSION --package-directory $TOOLS_DIR - # Add Cake.exe for VsCode intellisense support - dotnet add $TOOLS_DIR/tools.csproj package Cake -v $CAKE_VERSION --package-directory $TOOLS_DIR - - # Clean up - Move-Item -Path $CAKE_ROOT/$CAKE_VERSION/* -Destination $CAKE_ROOT - Remove-Item $CAKE_ROOT/$CAKE_VERSION/ -Force -Recurse - Move-Item -Path $CAKE_NFW/$CAKE_VERSION/* -Destination $CAKE_NFW - Remove-Item $CAKE_NFW/$CAKE_VERSION/ -Force -Recurse - Remove-Item $TOOLS_DIR/tools.csproj -} - -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.dll at $CAKE_EXE" -} - -# Build Cake arguments -$cakeArguments = @("$Script"); -if ($Target) { $cakeArguments += "-target=$Target" } -if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } -if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } -if ($ShowDescription) { $cakeArguments += "-showdescription" } -if ($DryRun) { $cakeArguments += "-dryrun" } -if ($Experimental) { $cakeArguments += "-experimental" } -if ($Mono) { $cakeArguments += "-mono" } -$cakeArguments += $ScriptArgs - -# Start Cake -Write-Host "Running build script..." -&dotnet $TOOLS_DIR/cake.coreclr/Cake.dll $cakeArguments -exit $LASTEXITCODE \ No newline at end of file +dotnet build ./build/LocalStack.Build/LocalStack.Build.csproj >$null 2>&1 +dotnet run --project ./build/LocalStack.Build/LocalStack.Build.csproj --no-launch-profile --no-build -- $args +exit $LASTEXITCODE; \ No newline at end of file diff --git a/build.sh b/build.sh index 739b42e..2b2c46b 100644 --- a/build.sh +++ b/build.sh @@ -1,78 +1,2 @@ -#!/usr/bin/env bash - -########################################################################## -# This is the Cake bootstrapper script for Linux and OS X. -# This file is based on https://github.com/cake-build/resources modified for Cake.CoreCLR -# Feel free to change this file to fit your needs. -########################################################################## - -CAKE_VERSION=0.37.0 - -# Define directories. -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -TOOLS_DIR=$SCRIPT_DIR/tools -CAKE_ROOT= $TOOLS_DIR/cake.coreclr/ -CAKE_EXE=$TOOLS_DIR/cake.coreclr/Cake.dll - -# Define default arguments. -SCRIPT="build.cake" -TARGET="Default" -CONFIGURATION="Release" -VERBOSITY="verbose" -DRYRUN= -SHOW_VERSION=false -CAKE_ARGUMENTS=() - -# Parse arguments. -for i in "$@"; do - case $1 in - -s|--script) SCRIPT="$2"; shift ;; - --) shift; CAKE_ARGUMENTS+=("$@"); break ;; - *) CAKE_ARGUMENTS+=("$1") ;; - esac - shift -done - -# Make sure that dotnet core installed. -if ! [ -x "$(command -v dotnet)" ]; then - echo 'Error: dotnet is not installed.' >&2 - exit 1 -fi - -# Install cake if its not installed -if [ ! -f "$CAKE_EXE" ]; then - - # Make sure the tools folder exist. - if [ ! -d "$TOOLS_DIR" ]; then - mkdir "$TOOLS_DIR" - fi - - # Make sure that tools.csproj exist. - if [ ! -f "$TOOLS_DIR/tools.csproj" ]; then - echo "Creating tools.csproj..." - echo "Exenetcoreapp3.1" > $TOOLS_DIR/tools.csproj - if [ $? -ne 0 ]; then - echo "An error occurred while creating tools.csproj." - exit 1 - fi - fi - - # Add dependencies - dotnet add $TOOLS_DIR/tools.csproj package Cake.CoreCLR -v $CAKE_VERSION --package-directory $TOOLS_DIR - mv $TOOLS_DIR/cake.coreclr/$CAKE_VERSION/* $TOOLS_DIR/cake.coreclr/ - rm -rf $TOOLS_DIR/cake.coreclr/$CAKE_VERSION/ - rm -f $TOOLS_DIR/tools.csproj -fi - -# Make sure that Cake has been installed. -if [ ! -f "$CAKE_EXE" ]; then - echo "Could not find Cake.exe at '$CAKE_EXE'." - exit 1 -fi - -# Start Cake -if $SHOW_VERSION; then - dotnet $TOOLS_DIR/cake.coreclr/Cake.dll -version -else - dotnet $TOOLS_DIR/cake.coreclr/Cake.dll $SCRIPT "${CAKE_ARGUMENTS[@]}" -fi \ No newline at end of file +dotnet build ./build/LocalStack.Build/LocalStack.Build.csproj > /dev/null 2>&1 +dotnet run --project ./build/LocalStack.Build/LocalStack.Build.csproj --no-launch-profile --no-build -- "$@" diff --git a/build/LocalStack.Build/BuildContext.cs b/build/LocalStack.Build/BuildContext.cs new file mode 100644 index 0000000..e41badd --- /dev/null +++ b/build/LocalStack.Build/BuildContext.cs @@ -0,0 +1,376 @@ +#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", 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", 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 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("../../"); + SrcPath = SolutionRoot + context.Directory("src"); + TestsPath = SolutionRoot + context.Directory("tests"); + BuildPath = SolutionRoot + context.Directory("build"); + ArtifactOutput = SolutionRoot + context.Directory("artifacts"); + LocalStackClientFolder = SrcPath + context.Directory(LocalStackClientProjName); + LocalStackClientExtFolder = SrcPath + context.Directory(LocalStackClientExtensionsProjName); + SlnFilePath = SolutionRoot + context.File("LocalStack.sln"); + LocalStackClientProjFile = LocalStackClientFolder + context.File($"{LocalStackClientProjName}.csproj"); + LocalStackClientExtProjFile = LocalStackClientExtFolder + context.File($"{LocalStackClientExtensionsProjName}.csproj"); + + var packIdBuilder = ImmutableDictionary.CreateBuilder(); + packIdBuilder.AddRange( + [ + new KeyValuePair(LocalStackClientProjName, LocalStackClientProjFile), + new KeyValuePair(LocalStackClientExtensionsProjName, LocalStackClientExtProjFile), + ]); + PackageIdProjMap = packIdBuilder.ToImmutable(); + } + + public string BuildConfiguration { get; } + + public bool ForceBuild { get; } + + public bool ForceRestore { get; } + + public bool SkipFunctionalTest { get; } + + 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; } + + public ConvertableFilePath SlnFilePath { get; } + + public ConvertableDirectoryPath SolutionRoot { get; } + + public ConvertableDirectoryPath SrcPath { get; } + + public ConvertableDirectoryPath TestsPath { get; } + + public ConvertableDirectoryPath BuildPath { get; } + + public ConvertableDirectoryPath ArtifactOutput { get; } + + public ConvertableDirectoryPath LocalStackClientFolder { get; } + + public ConvertableDirectoryPath LocalStackClientExtFolder { get; } + + public ConvertableFilePath LocalStackClientProjFile { get; } + + public ConvertableFilePath LocalStackClientExtProjFile { get; } + + /// + /// Gets the effective package version for LocalStack.Client package. + /// This value is cached to ensure consistency across pack and publish operations. + /// + public string GetClientPackageVersion() + { + return _clientPackageVersion ??= UseDirectoryPropsVersion + ? GetDynamicVersionFromProps("PackageMainVersion") + : PackageVersion; + } + + /// + /// 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() + { + return _extensionsPackageVersion ??= UseDirectoryPropsVersion + ? GetDynamicVersionFromProps("PackageExtensionVersion") + : PackageVersion; + } + + /// + /// 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 + { + LocalStackClientProjName => GetClientPackageVersion(), + LocalStackClientExtensionsProjName => GetExtensionsPackageVersion(), + _ => throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId)), + }; + } + + 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))]; + + var projMetadata = new List(); + + foreach (FilePath csProj in csProjFile) + { + string csProjPath = csProj.FullPath; + + IEnumerable targetFrameworks = GetProjectTargetFrameworks(csProjPath); + string directoryPath = csProj.GetDirectory().FullPath; + string assemblyName = GetAssemblyName(csProjPath); + + var testProjMetadata = new ProjMetadata(directoryPath, csProjPath, targetFrameworks, assemblyName); + projMetadata.Add(testProjMetadata); + } + + return projMetadata; + } + + public void InstallMonoOnLinux() + { + int result = this.StartProcess("mono", new ProcessSettings + { + Arguments = "--version", + RedirectStandardOutput = true, + NoWorkingDirectory = true, + }); + + if (result == 0) + { + this.Information("✅ Mono is already installed. Skipping installation."); + return; + } + + this.Information("Mono not found. Starting installation on Linux for .NET Framework test platform support..."); + + // Add Mono repository key + int exitCode1 = this.StartProcess("sudo", new ProcessSettings + { + Arguments = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF", + }); + + if (exitCode1 != 0) + { + this.Warning($"⚠️ Failed to add Mono repository key (exit code: {exitCode1})"); + return; + } + + // Add Mono repository + int exitCode2 = this.StartProcess("bash", new ProcessSettings + { + Arguments = "-c \"echo 'deb https://download.mono-project.com/repo/ubuntu focal main' | sudo tee /etc/apt/sources.list.d/mono-official-stable.list\"", + }); + + if (exitCode2 != 0) + { + this.Warning($"⚠️ Failed to add Mono repository (exit code: {exitCode2})"); + return; + } + + // Update package list + int exitCode3 = this.StartProcess("sudo", new ProcessSettings { Arguments = "apt update" }); + + if (exitCode3 != 0) + { + this.Warning($"⚠️ Failed to update package list (exit code: {exitCode3})"); + return; + } + + // Install Mono + int exitCode4 = this.StartProcess("sudo", new ProcessSettings { Arguments = "apt install -y mono-complete" }); + + if (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"); + } + + /// + /// Gets the target frameworks for a specific package using the existing proven method + /// + /// The package identifier + /// Comma-separated target frameworks + public string GetPackageTargetFrameworks(string packageId) + { + if (!PackageIdProjMap.TryGetValue(packageId, out FilePath? projectFile) || projectFile == null) + { + throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId)); + } + + string[] frameworks = GetProjectTargetFrameworks(projectFile.FullPath); + return string.Join(", ", frameworks); + } + + /// + /// Generates dynamic version from Directory.Build.props with build metadata + /// + /// The property name to extract (PackageMainVersion or PackageExtensionVersion) + /// Version with build metadata (e.g., 2.0.0-preview1.20240715.a1b2c3d) + private string GetDynamicVersionFromProps(string versionPropertyName) + { + // Extract base version from Directory.Build.props + FilePath propsFile = this.File("../../Directory.Build.props"); + string content = File.ReadAllText(propsFile.FullPath, Encoding.UTF8); + + string startElement = $"<{versionPropertyName}>"; + string endElement = $""; + + int startIndex = content.IndexOf(startElement, StringComparison.Ordinal) + startElement.Length; + int endIndex = content.IndexOf(endElement, startIndex, StringComparison.Ordinal); + + if (startIndex < startElement.Length || endIndex < 0) + { + throw new InvalidOperationException($"Could not find {versionPropertyName} in Directory.Build.props"); + } + + string baseVersion = content[startIndex..endIndex]; + + // Generate build metadata + string buildDate = DateTime.UtcNow.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture); + string commitSha = GetGitCommitSha(); + string safeBranchName = BranchName.Replace('/', '-').Replace('_', '-'); + + // 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}"); + } + + // Fallback to timestamp-based identifier + return DateTime.UtcNow.ToString("HHmmss", System.Globalization.CultureInfo.InvariantCulture); + } + + private string[] GetProjectTargetFrameworks(string csprojPath) + { + FilePath file = this.File(csprojPath); + string project = File.ReadAllText(file.FullPath, Encoding.UTF8); + + bool multipleFrameworks = project.Contains(""); + string startElement = multipleFrameworks ? "" : ""; + string endElement = multipleFrameworks ? "" : ""; + + int startIndex = project.IndexOf(startElement, StringComparison.Ordinal) + startElement.Length; + int endIndex = project.IndexOf(endElement, startIndex, StringComparison.Ordinal); + + string targetFrameworks = project[startIndex..endIndex]; + + return targetFrameworks.Split(';'); + } + + private string GetAssemblyName(string csprojPath) + { + FilePath file = this.File(csprojPath); + string project = File.ReadAllText(file.FullPath, Encoding.UTF8); + + bool assemblyNameElementExists = project.Contains(""); + + string assemblyName; + + if (assemblyNameElementExists) + { + int startIndex = project.IndexOf("", StringComparison.Ordinal) + "".Length; + int endIndex = project.IndexOf("", startIndex, StringComparison.Ordinal); + + assemblyName = project[startIndex..endIndex]; + } + else + { + int startIndex = csprojPath.LastIndexOf('/') + 1; + int endIndex = csprojPath.IndexOf(".csproj", startIndex, StringComparison.Ordinal); + + assemblyName = csprojPath[startIndex..endIndex]; + } + + return assemblyName; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..7ce57ab --- /dev/null +++ b/build/LocalStack.Build/GlobalUsings.cs @@ -0,0 +1,31 @@ +global using Cake.Common; +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.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; + +global using System; +global using System.Collections.Generic; +global using System.Collections.Immutable; +global using System.IO; +global using System.Linq; +global using System.Text; +global using System.Text.RegularExpressions; + +global using Cake.Common.Tools.DotNet.Build; +global using Cake.Common.Tools.DotNet.NuGet.Push; +global using Cake.Common.Tools.DotNet.Pack; +global using Cake.Common.Tools.DotNet.Test; \ No newline at end of file diff --git a/build/LocalStack.Build/LocalStack.Build.csproj b/build/LocalStack.Build/LocalStack.Build.csproj new file mode 100644 index 0000000..4b263e5 --- /dev/null +++ b/build/LocalStack.Build/LocalStack.Build.csproj @@ -0,0 +1,15 @@ + + + Exe + 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;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 new file mode 100644 index 0000000..38783f2 --- /dev/null +++ b/build/LocalStack.Build/Models/ProjMetadata.cs @@ -0,0 +1,5 @@ +#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 new file mode 100644 index 0000000..388a455 --- /dev/null +++ b/build/LocalStack.Build/Program.cs @@ -0,0 +1,6 @@ +#pragma warning disable CA1515 // Consider making public types internal + +return new CakeHost().UseContext().Run(args); + +[TaskName("Default"), IsDependentOn(typeof(TestTask))] +public class DefaultTask : FrostingTask; \ No newline at end of file diff --git a/build/LocalStack.Build/Properties/launchSettings-example.json b/build/LocalStack.Build/Properties/launchSettings-example.json new file mode 100644 index 0000000..e58c2de --- /dev/null +++ b/build/LocalStack.Build/Properties/launchSettings-example.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "LocalStack.Build": { + "commandName": "Project", + "commandLineArgs": "--target nuget-push --package-version 1.2.3 --package-source myget --package-id LocalStack.Client --package-secret ************************" + }, + "WSL 2": { + "commandName": "WSL2", + "commandLineArgs": "--target get-version", + "distributionName": "Ubuntu-18.04" + } + } +} \ 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/build/azure-pipelines.artifact.yml b/build/azure-pipelines.artifact.yml deleted file mode 100644 index d0a0196..0000000 --- a/build/azure-pipelines.artifact.yml +++ /dev/null @@ -1,54 +0,0 @@ -trigger: - branches: - include: - - master - - releases/* - - feature/* - paths: - exclude: - - README.md - - CONTRIBUTING.md - - LICENSE -pr: none -pool: - vmImage: ubuntu-18.04 -variables: - Version.MajorMinor: 1.0 - Version.Revision: $[counter(variables['Version.MajorMinor'], 0)] -steps: - - bash: "sudo apt install nuget && mkdir ./testrunner && sudo chmod -R 777 ./testrunner && ls" - displayName: "Init Task" - - - task: UseDotNet@2 - displayName: ".NET Core 3.1.x" - inputs: - version: "3.1.x" - packageType: sdk - - - task: UseDotNet@2 - displayName: ".NET Core 2.1.x" - inputs: - version: "2.1.x" - packageType: sdk - - - task: Bash@3 - displayName: "Compile & Tests" - inputs: - targetType: filePath - filePath: ./build.sh - - - bash: echo $(Version.Revision) && chmod +x ./build.sh && sudo ./build.sh --target=get-version --buildnumber=$BUILD_ID - displayName: "Package Version" - env: - BUILD_ID: $(Version.Revision) - - - bash: chmod +x ./build.sh && sudo ./build.sh --target=nuget-pack --buildnumber=$BUILD_ID - displayName: "Nuget Pack" - env: - BUILD_ID: $(Version.Revision) - - - task: PublishBuildArtifacts@1 - displayName: "Publish Artifact: LocalStack.Client" - inputs: - PathtoPublish: artifacts/ - ArtifactName: LocalStack.Client diff --git a/build/azure-pipelines.macos.yml b/build/azure-pipelines.macos.yml deleted file mode 100644 index b41fb57..0000000 --- a/build/azure-pipelines.macos.yml +++ /dev/null @@ -1,61 +0,0 @@ -trigger: - branches: - include: - - "*" - paths: - exclude: - - README.md - - CONTRIBUTING.md - - LICENSE -pr: - branches: - include: - - "*" -schedules: - - cron: "0 12 * * 0" - displayName: Weekly Sunday build - branches: - include: - - master - always: true -pool: - vmImage: macOS-10.14 -steps: - - bash: "sudo curl -o /usr/local/bin/nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" - displayName: "Install Nuget" - - - bash: "alias nuget='mono /usr/local/bin/nuget.exe'" - displayName: "Set nuget alias" - - - bash: "mkdir ./testrunner && sudo chmod -R 777 ./testrunner && ls" - displayName: "Create test runner folder" - - - task: UseDotNet@2 - displayName: ".NET Core 3.1.x" - inputs: - version: "3.1.x" - packageType: sdk - - - task: UseDotNet@2 - displayName: ".NET Core 2.1.x" - inputs: - version: "2.1.x" - packageType: sdk - - - task: Bash@3 - displayName: "Compile & Tests" - inputs: - targetType: filePath - filePath: ./build.sh - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "VSTest" - testResultsFiles: "**/*.trx" - testRunTitle: "LocalStack.Client.Tests" - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "VSTest" - testResultsFiles: "**/*.trx" - testRunTitle: "LocalStack.Client.Integration.Tests" diff --git a/build/azure-pipelines.ubuntu.yml b/build/azure-pipelines.ubuntu.yml deleted file mode 100644 index b813717..0000000 --- a/build/azure-pipelines.ubuntu.yml +++ /dev/null @@ -1,55 +0,0 @@ -trigger: - branches: - include: - - "*" - paths: - exclude: - - README.md - - CONTRIBUTING.md - - LICENSE -pr: - branches: - include: - - "*" -schedules: - - cron: "0 12 * * 0" - displayName: Weekly Sunday build - branches: - include: - - master - always: true -pool: - vmImage: ubuntu-18.04 -steps: - - bash: "sudo apt install nuget && mkdir ./testrunner && sudo chmod -R 777 ./testrunner && ls" - displayName: "Init Task" - - - task: UseDotNet@2 - displayName: ".NET Core 3.1.x" - inputs: - version: "3.1.x" - packageType: sdk - - - task: UseDotNet@2 - displayName: ".NET Core 2.1.x" - inputs: - version: "2.1.x" - packageType: sdk - - - task: Bash@3 - displayName: "Compile & Tests" - inputs: - targetType: filePath - filePath: ./build.sh - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "VSTest" - testResultsFiles: "**/*.trx" - testRunTitle: "LocalStack.Client.Tests" - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "VSTest" - testResultsFiles: "**/*.trx" - testRunTitle: "LocalStack.Client.Integration.Tests" diff --git a/build/azure-pipelines.windows.yml b/build/azure-pipelines.windows.yml deleted file mode 100644 index a4f2d74..0000000 --- a/build/azure-pipelines.windows.yml +++ /dev/null @@ -1,40 +0,0 @@ -trigger: - branches: - include: - - "*" - paths: - exclude: - - README.md - - CONTRIBUTING.md - - LICENSE -pr: - branches: - include: - - "*" -schedules: - - cron: "0 12 * * 0" - displayName: Weekly Sunday build - branches: - include: - - master - always: true -pool: - vmImage: windows-2019 -steps: - - task: PowerShell@2 - displayName: "Compile & Tests" - inputs: - targetType: filePath - filePath: ./build.ps1 - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "VSTest" - testResultsFiles: "**/*.trx" - testRunTitle: "LocalStack.Client.Tests" - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "VSTest" - testResultsFiles: "**/*.trx" - testRunTitle: "LocalStack.Client.Integration.Tests" diff --git a/build/scripts/packageUpdate.ps1 b/build/scripts/packageUpdate.ps1 new file mode 100644 index 0000000..48318fe --- /dev/null +++ b/build/scripts/packageUpdate.ps1 @@ -0,0 +1,17 @@ +$regex = 'PackageReference Include="([^"]*)" Version="([^"]*)"' + +ForEach ($file in get-childitem . -recurse | where {$_.extension -like "*proj"}) +{ + $packages = Get-Content $file.FullName | + select-string -pattern $regex -AllMatches | + ForEach-Object {$_.Matches} | + ForEach-Object {$_.Groups[1].Value.ToString()}| + sort -Unique + + ForEach ($package in $packages) + { + write-host "Update $file package :$package" -foreground 'magenta' + $fullName = $file.FullName + iex "dotnet add $fullName package $package" + } +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..2bb272e --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.200", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..318a6f7 --- /dev/null +++ b/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props deleted file mode 100644 index 5970def..0000000 --- a/src/Directory.Build.props +++ /dev/null @@ -1,10 +0,0 @@ - - - LocalStack.NET, Deniz İrgin - LocalStack.NET - LocalStack.NET - https://github.com/localstack-dotnet/localstack-dotnet-client - localstack-dotnet-square.png - 1.0.0 - - \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs b/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs new file mode 100644 index 0000000..ffe3977 --- /dev/null +++ b/src/LocalStack.Client.Extensions/AwsClientFactoryWrapper.cs @@ -0,0 +1,44 @@ +#pragma warning disable S3011 // We need to use reflection to access private fields for service metadata +#pragma warning disable CS8600,CS8603 // Not possible to get null value from this private field +#pragma warning disable CA1802 // We need to use reflection to access private fields for service metadata +namespace LocalStack.Client.Extensions; + +public sealed class AwsClientFactoryWrapper : IAwsClientFactoryWrapper +{ + 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? genericFactoryType = typeof(ConfigurationException).Assembly.GetType(ClientFactoryGenericTypeName); + + if (genericFactoryType == null) + { + throw new LocalStackClientConfigurationException($"Failed to find internal ClientFactory in {ClientFactoryGenericTypeName}"); + } + + // Create ClientFactory + Type concreteFactoryType = genericFactoryType.MakeGenericType(typeof(TClient)); + ConstructorInfo? constructor = concreteFactoryType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(AWSOptions) }, null); + + if (constructor == null) + { + throw new LocalStackClientConfigurationException("ClientFactory missing constructor with AWSOptions parameter."); + } + + object factory = constructor.Invoke(new object[] { awsOptions! }); + MethodInfo? createMethod = factory.GetType().GetMethod(CreateServiceClientMethodName, BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(IServiceProvider) }, null); + + if (createMethod == null) + { + throw new LocalStackClientConfigurationException($"ClientFactory missing {CreateServiceClientMethodName}(IServiceProvider) method."); + } + + object serviceInstance = createMethod.Invoke(factory, new object[] { provider }); + return (AmazonServiceClient)serviceInstance; + } +} \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/Contracts/IAwsClientFactoryWrapper.cs b/src/LocalStack.Client.Extensions/Contracts/IAwsClientFactoryWrapper.cs new file mode 100644 index 0000000..0a2b8a5 --- /dev/null +++ b/src/LocalStack.Client.Extensions/Contracts/IAwsClientFactoryWrapper.cs @@ -0,0 +1,6 @@ +namespace LocalStack.Client.Extensions.Contracts; + +public interface IAwsClientFactoryWrapper +{ + AmazonServiceClient CreateServiceClient(IServiceProvider provider, AWSOptions? awsOptions) where TClient : IAmazonService; +} \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/Exceptions/LocalStackClientConfigurationException.cs b/src/LocalStack.Client.Extensions/Exceptions/LocalStackClientConfigurationException.cs new file mode 100644 index 0000000..ed4e0ba --- /dev/null +++ b/src/LocalStack.Client.Extensions/Exceptions/LocalStackClientConfigurationException.cs @@ -0,0 +1,40 @@ +namespace LocalStack.Client.Extensions.Exceptions; + +[Serializable] +public class LocalStackClientConfigurationException : Exception +{ + /// + /// Construct instance of ConfigurationException + /// + /// The error message. + public LocalStackClientConfigurationException(string message) : base(message) + { + } + + /// + /// Construct instance of ConfigurationException + /// + /// The error message. + /// Original exception. + public LocalStackClientConfigurationException(string message, Exception exception) : base(message, exception) + { + } + + public LocalStackClientConfigurationException() + { + } + + /// + /// Serialization constructor. + /// + /// 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) + { + } +} \ No newline at end of file diff --git a/src/LocalStack.Client.Extensions/GlobalUsings.cs b/src/LocalStack.Client.Extensions/GlobalUsings.cs new file mode 100644 index 0000000..de1f8e2 --- /dev/null +++ b/src/LocalStack.Client.Extensions/GlobalUsings.cs @@ -0,0 +1,19 @@ +global using System; +global using System.Diagnostics.CodeAnalysis; +global using System.Reflection; +global using System.Runtime.Serialization; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Options; + +global using Amazon; +global using Amazon.Extensions.NETCore.Setup; +global using Amazon.Runtime; + +global using LocalStack.Client.Contracts; +global using LocalStack.Client.Extensions.Contracts; +global using LocalStack.Client.Options; +global using LocalStack.Client.Utils; +global using LocalStack.Client.Extensions.Exceptions; \ No newline at end of file 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 new file mode 100644 index 0000000..f9478d1 --- /dev/null +++ b/src/LocalStack.Client.Extensions/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +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. \ 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 new file mode 100644 index 0000000..d23ee89 --- /dev/null +++ b/src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj @@ -0,0 +1,78 @@ + + + + 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 new file mode 100644 index 0000000..88d6fb0 --- /dev/null +++ b/src/LocalStack.Client.Extensions/README.md @@ -0,0 +1,294 @@ +# 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. + +## 🚀 Platform Compatibility & Quality Status + +### Supported Platforms + +- [.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 + +1. [Supported Platforms](#supported-platforms) +2. [Why LocalStack.NET Client?](#why-localstacknet-client) +3. [Prerequisites](#prerequisites) +4. [Getting Started](#getting-started) + - [Setup](#setup) + - [Configuration](#configuration) +5. [Known Issues](#known-issues) +6. [Developing](#developing) + - [Building the Project](#building-the-project) + - [Sandbox Applications](#sandbox-applications) + - [Running Tests](#running-tests) +7. [Changelog](#changelog) +8. [License](#license) + +## 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 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. + +- **Enhanced Development Speed:** Reduces boilerplate and manual configurations, speeding up the development process. + +## Prerequisites + +To utilize this library, you need to have LocalStack running. While LocalStack can be installed directly on your machine and accessed via the localstack cli, the recommended approach is to run LocalStack using [Docker](https://docs.docker.com/get-docker/) or [docker-compose](https://docs.docker.com/compose/install/). + +For detailed installation and setup instructions, please refer to the [official LocalStack installation guide](https://docs.localstack.cloud/getting-started/installation/). + +## Getting Started + +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 +# 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). + +### Setup + +Here's a basic example of how to setup `LocalStack.NET`: + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + // Add framework services. + services.AddMvc(); + + services.AddLocalStack(Configuration) + services.AddDefaultAWSOptions(Configuration.GetAWSOptions()); + services.AddAwsService(); + services.AddAwsService(); +} +``` + +The `AddLocalStack` method integrates LocalStack.NET into your application, and the `AddAwsService` method allows you to specify which AWS services you want to use with LocalStack. + +(Alternatively, `AddAWSServiceLocalStack` method can be used to prevent mix-up with `AddAWSService`.) + +`AddLocalStack` extension method is responsible for both configurations and adding of `LocalStack.Client` dependencies to service collection. + +### Configuration + +To configure LocalStack.NET, you can use entries in the appsettings.json files. Here's a basic example for different environments: + +`appsettings.Development.json` + +```json +"LocalStack": { + "UseLocalStack": true, + "Session": { + "RegionName": "eu-central-1" + }, + "Config": { + "LocalStackHost": "localhost.localstack.cloud", // or "localhost", + "EdgePort": 4566 + } +} +``` + +`appsettings.Production.json` + +```json +"LocalStack": { + "UseLocalStack": false +}, +"AWS": { + "Profile": "", + "Region": "eu-central-1" +} +``` + +The `RegionName` is important as LocalStack creates resources based on the specified region. For more advanced configurations and understanding how LocalStack.NET operates with LocalStack, refer to [documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Setup#configuration). + +## Known Issues + +- **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. + +```csharp +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. + +### Building the Project + +To build the project, use the following commands based on your operating system: + +Windows + +```powershell +build.ps1 +``` + +Linux + +```bash +./build.sh +``` + +### Sandbox Applications + +The LocalStack .NET repository includes several sandbox console applications located in [tests/sandboxes](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master/tests/sandboxes). These applications serve both as testing tools and as examples. Refer to [the documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Developing#sandbox-applications) for more information + +### Running Tests + +To execute the tests, use the commands below: + +Windows + +```powershell +build.ps1 --target=tests +``` + +Linux + +```bash +./build.sh --target=tests +``` + +## Changelog + +Please refer to [`CHANGELOG.md`](CHANGELOG.md) to see the complete list of changes for each release. + +## License + +Licensed under MIT, see [LICENSE](LICENSE) for the full text. diff --git a/src/LocalStack.Client.Extensions/ServiceCollectionExtensions.cs b/src/LocalStack.Client.Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..6afe80b --- /dev/null +++ b/src/LocalStack.Client.Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,234 @@ +namespace LocalStack.Client.Extensions; + +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)); + + collection.Configure(options => configuration.GetSection(LocalStackSectionName) + .GetSection(nameof(LocalStackOptions.Session)) + .Bind(options, c => c.BindNonPublicProperties = true)); + + collection.Configure(options => configuration.GetSection(LocalStackSectionName) + .GetSection(nameof(LocalStackOptions.Config)) + .Bind(options, c => c.BindNonPublicProperties = true)); + + return collection.AddLocalStackServices(); + } + + /// + /// Adds the AWSOptions object to the dependency injection framework providing information + /// that will be used to construct Amazon service clients. + /// + /// + /// The default AWS options used to construct AWS service clients with. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection AddDefaultAwsOptions(this IServiceCollection collection, AWSOptions options) + { + collection.AddDefaultAWSOptions(options); + + return collection; + } + + /// + /// Adds the Amazon service client to the dependency injection framework. The Amazon service client is not + /// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection AddAwsService(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool useServiceUrl = false) where TService : IAmazonService + { + return AddAwsService(collection, null, lifetime, useServiceUrl); + } + + /// + /// Adds the Amazon service client to the dependency injection framework. The Amazon service client is not + /// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection AddAwsService(this IServiceCollection collection, AWSOptions? options, + ServiceLifetime lifetime = ServiceLifetime.Singleton, bool useServiceUrl = false) + where TService : IAmazonService + { + ServiceDescriptor descriptor = GetServiceFactoryDescriptor(options, lifetime, useServiceUrl); + + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + collection.Add(descriptor); + + return collection; + } + + /// + /// Adds the Amazon service client to the dependency injection framework. The Amazon service client is not + /// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection AddAWSServiceLocalStack(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool useServiceUrl = false) where TService : IAmazonService + { + return AddAWSServiceLocalStack(collection, null, lifetime, useServiceUrl); + } + + /// + /// Adds the Amazon service client to the dependency injection framework. The Amazon service client is not + /// created until it is requested. If the ServiceLifetime property is set to Singleton, the default, then the same + /// instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection AddAWSServiceLocalStack(this IServiceCollection collection, AWSOptions? options, + ServiceLifetime lifetime = ServiceLifetime.Singleton, bool useServiceUrl = false) + where TService : IAmazonService + { + return AddAwsService(collection, options, lifetime, useServiceUrl); + } + + /// + /// Adds the Amazon service client to the dependency injection framework if the service type hasn't already been registered. + /// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, + /// the default, then the same instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection TryAddAwsService(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool useServiceUrl = false) where TService : IAmazonService + { + return TryAddAwsService(collection, null, lifetime); + } + + /// + /// Adds the Amazon service client to the dependency injection framework if the service type hasn't already been registered. + /// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, + /// the default, then the same instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection TryAddAwsService(this IServiceCollection collection, AWSOptions? options, + ServiceLifetime lifetime = ServiceLifetime.Singleton, bool useServiceUrl = false) + where TService : IAmazonService + { + ServiceDescriptor descriptor = GetServiceFactoryDescriptor(options, lifetime, useServiceUrl); + collection.TryAdd(descriptor); + + return collection; + } + + /// + /// Adds the Amazon service client to the dependency injection framework if the service type hasn't already been registered. + /// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, + /// the default, then the same instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection TryAddAWSServiceLocalStack(this IServiceCollection collection, ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool useServiceUrl = false) where TService : IAmazonService + { + return TryAddAWSServiceLocalStack(collection, null, lifetime, useServiceUrl); + } + + /// + /// Adds the Amazon service client to the dependency injection framework if the service type hasn't already been registered. + /// The Amazon service client is not created until it is requested. If the ServiceLifetime property is set to Singleton, + /// the default, then the same instance will be reused for the lifetime of the process and the object should not be disposed. + /// + /// The AWS service interface, like IAmazonS3. + /// + /// The AWS options used to create the service client overriding the default AWS options added using AddDefaultAWSOptions. + /// The lifetime of the service client created. The default is Singleton. + /// (LocalStack) Determines whether the service client will use RegionEndpoint or ServiceUrl. The default is false. + /// Returns back the IServiceCollection to continue the fluent system of IServiceCollection. + public static IServiceCollection TryAddAWSServiceLocalStack(this IServiceCollection collection, AWSOptions? options, + ServiceLifetime lifetime = ServiceLifetime.Singleton, bool useServiceUrl = false) + where TService : IAmazonService + { + return TryAddAwsService(collection, options, lifetime, useServiceUrl); + } + + private static IServiceCollection AddLocalStackServices(this IServiceCollection services) + { + services.AddTransient(provider => + { + ConfigOptions options = provider.GetRequiredService>().Value; + + return new Config(options); + }) + .AddSingleton() + .AddSingleton(provider => + { + SessionOptions sessionOptions = provider.GetRequiredService>().Value; + var config = provider.GetRequiredService(); + var sessionReflection = provider.GetRequiredService(); + + return new Session(sessionOptions, config, sessionReflection); + }) + .AddSingleton(); + + return services; + } + + private static ServiceDescriptor GetServiceFactoryDescriptor(AWSOptions? options, ServiceLifetime lifetime, bool useServiceUrl = false) + where TService : IAmazonService + { + var descriptor = new ServiceDescriptor(typeof(TService), provider => + { + LocalStackOptions localStackOptions = provider.GetRequiredService>().Value; + + AmazonServiceClient serviceInstance; + + if (localStackOptions.UseLocalStack) + { + var session = provider.GetRequiredService(); + serviceInstance = session.CreateClientByInterface(useServiceUrl); + } + else + { + var clientFactory = provider.GetRequiredService(); + serviceInstance = clientFactory.CreateServiceClient(provider, options!); + } + + return serviceInstance; + }, lifetime); + + return descriptor; + } +} \ No newline at end of file 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/Config.cs b/src/LocalStack.Client/Config.cs index a693b8a..0bf18b8 100644 --- a/src/LocalStack.Client/Config.cs +++ b/src/LocalStack.Client/Config.cs @@ -1,56 +1,64 @@ -using LocalStack.Client.Contracts; -using LocalStack.Client.Models; +namespace LocalStack.Client; -using System; -using System.Collections.Generic; -using System.Linq; +public class Config : IConfig +{ + private readonly AwsServiceEndpointMetadata[] _serviceEndpointMetadata = AwsServiceEndpointMetadata.All; + private readonly IEnumerable _awsServiceEndpoints; -using LocalStack.Client.Enums; + private readonly IConfigOptions _configOptions; -namespace LocalStack.Client -{ - public class Config : IConfig + public Config() : this(new ConfigOptions()) { - private static readonly string EnvLocalStackHost = Environment.GetEnvironmentVariable("LOCALSTACK_HOST"); - private static readonly string EnvUseSsl = Environment.GetEnvironmentVariable("USE_SSL"); - private static readonly AwsServiceEndpointMetadata[] ServiceEndpointMetadata = AwsServiceEndpointMetadata.All; - - private readonly IEnumerable _awsServiceEndpoints; + } - public Config(string localStackHost = null) + public Config(IConfigOptions configOptions) + { + if (configOptions == null) { - localStackHost ??= (EnvLocalStackHost ?? "localhost"); - string protocol = EnvUseSsl != null && (EnvUseSsl == "1" || EnvUseSsl == "true") ? "https" : "http"; - - _awsServiceEndpoints = ServiceEndpointMetadata.Select(metadata => new AwsServiceEndpoint(metadata.ServiceId, metadata.CliName, metadata.Enum, metadata.Port, - localStackHost, metadata.ToString(protocol, localStackHost))); + throw new ArgumentNullException(nameof(configOptions)); } - public IEnumerable GetAwsServiceEndpoints() - { - return _awsServiceEndpoints; - } + string localStackHost = configOptions.LocalStackHost; + string protocol = configOptions.UseSsl ? "https" : "http"; + bool useLegacyPorts = configOptions.UseLegacyPorts; + int edgePort = configOptions.EdgePort; - public AwsServiceEndpoint GetAwsServiceEndpoint(AwsServiceEnum awsServiceEnum) + _awsServiceEndpoints = _serviceEndpointMetadata.Select(metadata => { - return _awsServiceEndpoints.SingleOrDefault(endpoint => endpoint.AwsServiceEnum == awsServiceEnum); - } + Uri serviceUrl = metadata.GetServiceUrl(protocol, localStackHost, GetServicePort(metadata.Port)); - public AwsServiceEndpoint GetAwsServiceEndpoint(string serviceId) - { - return _awsServiceEndpoints.SingleOrDefault(endpoint => endpoint.ServiceId == serviceId); - } + return new AwsServiceEndpoint(metadata.ServiceId, metadata.CliName, metadata.Enum, GetServicePort(metadata.Port), localStackHost, serviceUrl); + }); - public IDictionary GetAwsServicePorts() - { - return _awsServiceEndpoints.ToDictionary(endpoint => endpoint.AwsServiceEnum, endpoint => endpoint.Port); - } + _configOptions = configOptions; - public int GetAwsServicePorts(AwsServiceEnum awsServiceEnum) - { - return _awsServiceEndpoints - .First(endpoint => endpoint.AwsServiceEnum == awsServiceEnum) - .Port; - } + int GetServicePort(int metadataPort) => useLegacyPorts ? metadataPort : edgePort; } + + public IEnumerable GetAwsServiceEndpoints() + { + return _awsServiceEndpoints; + } + + public AwsServiceEndpoint? GetAwsServiceEndpoint(AwsService awsService) + { + return _awsServiceEndpoints.SingleOrDefault(endpoint => endpoint.AwsService == awsService); + } + + public AwsServiceEndpoint? GetAwsServiceEndpoint(string serviceId) + { + return _awsServiceEndpoints.SingleOrDefault(endpoint => endpoint.ServiceId == serviceId); + } + + public IDictionary GetAwsServicePorts() + { + return _awsServiceEndpoints.ToDictionary(endpoint => endpoint.AwsService, endpoint => endpoint.Port); + } + + public int GetAwsServicePort(AwsService awsService) + { + return _awsServiceEndpoints.First(endpoint => endpoint.AwsService == awsService).Port; + } + + public IConfigOptions GetConfigOptions() => _configOptions; } \ No newline at end of file diff --git a/src/LocalStack.Client/Contracts/IConfig.cs b/src/LocalStack.Client/Contracts/IConfig.cs index a8990c5..3fc6ced 100644 --- a/src/LocalStack.Client/Contracts/IConfig.cs +++ b/src/LocalStack.Client/Contracts/IConfig.cs @@ -1,21 +1,16 @@ -using LocalStack.Client.Models; +namespace LocalStack.Client.Contracts; -using System.Collections.Generic; - -using LocalStack.Client.Enums; - -namespace LocalStack.Client.Contracts +public interface IConfig { - public interface IConfig - { - IEnumerable GetAwsServiceEndpoints(); + IEnumerable GetAwsServiceEndpoints(); + + AwsServiceEndpoint? GetAwsServiceEndpoint(AwsService awsService); - AwsServiceEndpoint GetAwsServiceEndpoint(AwsServiceEnum awsServiceEnum); + AwsServiceEndpoint? GetAwsServiceEndpoint(string serviceId); - AwsServiceEndpoint GetAwsServiceEndpoint(string serviceId); + IDictionary GetAwsServicePorts(); - IDictionary GetAwsServicePorts(); + int GetAwsServicePort(AwsService awsService); - int GetAwsServicePorts(AwsServiceEnum awsServiceEnum); - } + IConfigOptions GetConfigOptions(); } \ No newline at end of file diff --git a/src/LocalStack.Client/Contracts/IConfigOptions.cs b/src/LocalStack.Client/Contracts/IConfigOptions.cs new file mode 100644 index 0000000..7e8d30b --- /dev/null +++ b/src/LocalStack.Client/Contracts/IConfigOptions.cs @@ -0,0 +1,12 @@ +namespace LocalStack.Client.Contracts; + +public interface IConfigOptions +{ + string LocalStackHost { get; } + + bool UseSsl { get; } + + bool UseLegacyPorts { get; } + + int EdgePort { get; } +} \ No newline at end of file diff --git a/src/LocalStack.Client/Contracts/ILocalStackOptions.cs b/src/LocalStack.Client/Contracts/ILocalStackOptions.cs new file mode 100644 index 0000000..796eb9b --- /dev/null +++ b/src/LocalStack.Client/Contracts/ILocalStackOptions.cs @@ -0,0 +1,10 @@ +namespace LocalStack.Client.Contracts; + +public interface ILocalStackOptions +{ + bool UseLocalStack { get; } + + SessionOptions Session { get; } + + ConfigOptions Config { get; } +} \ No newline at end of file diff --git a/src/LocalStack.Client/Contracts/ISession.cs b/src/LocalStack.Client/Contracts/ISession.cs index bd5f55c..543ddf4 100644 --- a/src/LocalStack.Client/Contracts/ISession.cs +++ b/src/LocalStack.Client/Contracts/ISession.cs @@ -1,9 +1,12 @@ -using Amazon.Runtime; +namespace LocalStack.Client.Contracts; -namespace LocalStack.Client.Contracts +public interface ISession { - public interface ISession - { - TClient CreateClient() where TClient : AmazonServiceClient; - } + TClient CreateClientByImplementation(bool useServiceUrl = false) where TClient : AmazonServiceClient; + + AmazonServiceClient CreateClientByImplementation(Type implType, bool useServiceUrl = false); + + AmazonServiceClient CreateClientByInterface(bool useServiceUrl = false) where TClient: IAmazonService; + + AmazonServiceClient CreateClientByInterface(Type serviceInterfaceType, bool useServiceUrl = false); } \ No newline at end of file diff --git a/src/LocalStack.Client/Contracts/ISessionOptions.cs b/src/LocalStack.Client/Contracts/ISessionOptions.cs index 89b975d..660d7d4 100644 --- a/src/LocalStack.Client/Contracts/ISessionOptions.cs +++ b/src/LocalStack.Client/Contracts/ISessionOptions.cs @@ -1,13 +1,12 @@ -namespace LocalStack.Client.Contracts +namespace LocalStack.Client.Contracts; + +public interface ISessionOptions { - public interface ISessionOptions - { - string AwsAccessKeyId { get; } + string AwsAccessKeyId { get; } - string AwsAccessKey { get; } + string AwsAccessKey { get; } - string AwsSessionToken { get; } + string AwsSessionToken { get; } - string RegionName { get; } - } + string RegionName { get; } } \ No newline at end of file diff --git a/src/LocalStack.Client/Contracts/ISessionReflection.cs b/src/LocalStack.Client/Contracts/ISessionReflection.cs index 94d6e16..64ab21b 100644 --- a/src/LocalStack.Client/Contracts/ISessionReflection.cs +++ b/src/LocalStack.Client/Contracts/ISessionReflection.cs @@ -1,14 +1,16 @@ -using Amazon.Runtime; -using Amazon.Runtime.Internal; +namespace LocalStack.Client.Contracts; -namespace LocalStack.Client.Contracts +public interface ISessionReflection { - public interface ISessionReflection - { - IServiceMetadata ExtractServiceMetadata() where TClient : AmazonServiceClient; + IServiceMetadata ExtractServiceMetadata() where TClient : AmazonServiceClient; - ClientConfig CreateClientConfig() where TClient : AmazonServiceClient; + IServiceMetadata ExtractServiceMetadata(Type clientType); - bool SetForcePathStyle(ClientConfig clientConfig, bool value = true); - } + ClientConfig CreateClientConfig() where TClient : AmazonServiceClient; + + ClientConfig CreateClientConfig(Type clientType); + + bool SetForcePathStyle(ClientConfig clientConfig, bool value = true); + + void SetClientRegion(AmazonServiceClient amazonServiceClient, string systemName); } \ No newline at end of file diff --git a/src/LocalStack.Client/Contracts/ISessionStandalone.cs b/src/LocalStack.Client/Contracts/ISessionStandalone.cs index 0f98eb7..065b2a5 100644 --- a/src/LocalStack.Client/Contracts/ISessionStandalone.cs +++ b/src/LocalStack.Client/Contracts/ISessionStandalone.cs @@ -1,11 +1,10 @@ -namespace LocalStack.Client.Contracts +namespace LocalStack.Client.Contracts; + +public interface ISessionStandalone { - public interface ISessionStandalone - { - ISessionStandalone WithSessionOptions(string awsAccessKeyId = null, string awsAccessKey = null, string awsSessionToken = null, string regionName = null); + ISessionStandalone WithSessionOptions(ISessionOptions sessionOptions); - ISessionStandalone WithConfig(string localStackHost = null); + ISessionStandalone WithConfigurationOptions(IConfigOptions configOptions); - ISession Create(); - } + ISession Create(); } \ No newline at end of file diff --git a/src/LocalStack.Client/Enums/AwsService.cs b/src/LocalStack.Client/Enums/AwsService.cs new file mode 100644 index 0000000..22dd038 --- /dev/null +++ b/src/LocalStack.Client/Enums/AwsService.cs @@ -0,0 +1,127 @@ +namespace LocalStack.Client.Enums; + +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, + 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 baeb34d..b5405fd 100644 --- a/src/LocalStack.Client/Enums/AwsServiceEndpointMetadata.cs +++ b/src/LocalStack.Client/Enums/AwsServiceEndpointMetadata.cs @@ -1,135 +1,207 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +namespace LocalStack.Client.Enums; -namespace LocalStack.Client.Enums +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class AwsServiceEndpointMetadata { - [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] - public class AwsServiceEndpointMetadata + private const string CommonEndpointPattern = "{0}://{1}:{2}"; + + public static readonly AwsServiceEndpointMetadata ApiGateway = new("API Gateway", "apigateway", CommonEndpointPattern, 4567, AwsService.ApiGateway); + public static readonly AwsServiceEndpointMetadata ApiGatewayV2 = new("ApiGatewayV2", "apigatewayv2", CommonEndpointPattern, 4567, AwsService.ApiGatewayV2); + public static readonly AwsServiceEndpointMetadata Kinesis = new("Kinesis", "kinesis", CommonEndpointPattern, 4568, AwsService.Kinesis); + public static readonly AwsServiceEndpointMetadata DynamoDb = new("DynamoDB", "dynamodb", CommonEndpointPattern, 4569, AwsService.DynamoDb); + public static readonly AwsServiceEndpointMetadata DynamoDbStreams = new("DynamoDB Streams", "dynamodbstreams", CommonEndpointPattern, 4570, AwsService.DynamoDbStreams); + public static readonly AwsServiceEndpointMetadata ElasticSearch = new("Elasticsearch Service", "elasticsearch", CommonEndpointPattern, 4571, AwsService.ElasticSearch); + public static readonly AwsServiceEndpointMetadata OpenSearch = new("OpenSearch", "opensearch", CommonEndpointPattern, 4571, AwsService.OpenSearch); + public static readonly AwsServiceEndpointMetadata S3 = new("S3", "s3", CommonEndpointPattern, 4572, AwsService.S3); + public static readonly AwsServiceEndpointMetadata Firehose = new("Firehose", "firehose", CommonEndpointPattern, 4573, AwsService.Firehose); + public static readonly AwsServiceEndpointMetadata Lambda = new("Lambda", "lambda", CommonEndpointPattern, 4574, AwsService.Lambda); + public static readonly AwsServiceEndpointMetadata Sns = new("SNS", "sns", CommonEndpointPattern, 4575, AwsService.Sns); + public static readonly AwsServiceEndpointMetadata Sqs = new("SQS", "sqs", CommonEndpointPattern, 4576, AwsService.Sqs); + public static readonly AwsServiceEndpointMetadata Redshift = new("Redshift", "redshift", CommonEndpointPattern, 4577, AwsService.Redshift); + public static readonly AwsServiceEndpointMetadata RedshiftData = new("Redshift Data", "redshift-data", CommonEndpointPattern, 4577, AwsService.RedshiftData); + public static readonly AwsServiceEndpointMetadata Es = new("ES", "es", CommonEndpointPattern, 4578, AwsService.Es); + public static readonly AwsServiceEndpointMetadata Ses = new("SES", "ses", CommonEndpointPattern, 4579, AwsService.Ses); + public static readonly AwsServiceEndpointMetadata Sesv2 = new("SESv2", "sesv2", CommonEndpointPattern, 4579, AwsService.Sesv2); + public static readonly AwsServiceEndpointMetadata Route53 = new("Route 53", "route53", CommonEndpointPattern, 4580, AwsService.Route53); + public static readonly AwsServiceEndpointMetadata Route53Resolver = new("Route53Resolver", "route53resolver", CommonEndpointPattern, 4580, AwsService.Route53Resolver); + public static readonly AwsServiceEndpointMetadata Route53Domains = new("Route 53 Domains", "route53domains", CommonEndpointPattern, 4566, AwsService.Route53Domains); + public static readonly AwsServiceEndpointMetadata CloudFormation = new("CloudFormation", "cloudformation", CommonEndpointPattern, 4581, AwsService.CloudFormation); + public static readonly AwsServiceEndpointMetadata CloudWatch = new("CloudWatch", "cloudwatch", CommonEndpointPattern, 4582, AwsService.CloudWatch); + public static readonly AwsServiceEndpointMetadata Ssm = new("SSM", "ssm", CommonEndpointPattern, 4583, AwsService.Ssm); + public static readonly AwsServiceEndpointMetadata SecretsManager = new("Secrets Manager", "secretsmanager", CommonEndpointPattern, 4584, AwsService.SecretsManager); + public static readonly AwsServiceEndpointMetadata StepFunctions = new("SFN", "stepfunctions", CommonEndpointPattern, 4585, AwsService.StepFunctions); + public static readonly AwsServiceEndpointMetadata Logs = new("CloudWatch Logs", "logs", CommonEndpointPattern, 4586, AwsService.Logs); + public static readonly AwsServiceEndpointMetadata Events = new("CloudWatch Events", "events", CommonEndpointPattern, 4587, AwsService.Events); + public static readonly AwsServiceEndpointMetadata Elb = new("Elastic Load Balancing", "elb", CommonEndpointPattern, 4588, AwsService.Elb); + public static readonly AwsServiceEndpointMetadata Iot = new("IoT", "iot", CommonEndpointPattern, 4589, AwsService.Iot); + public static readonly AwsServiceEndpointMetadata IoTAnalytics = new("IoTAnalytics", "iotanalytics", CommonEndpointPattern, 4589, AwsService.IoTAnalytics); + public static readonly AwsServiceEndpointMetadata IoTEvents = new("IoT Events", "iotevents", CommonEndpointPattern, 4589, AwsService.IoTEvents); + public static readonly AwsServiceEndpointMetadata IoTEventsData = new("IoT Events Data", "iotevents-data", CommonEndpointPattern, 4589, AwsService.IoTEventsData); + public static readonly AwsServiceEndpointMetadata IoTWireless = new("IoT Wireless", "iotwireless", CommonEndpointPattern, 4589, AwsService.IoTWireless); + public static readonly AwsServiceEndpointMetadata IoTDataPlane = new("IoT Data Plane", "iot-data", CommonEndpointPattern, 4589, AwsService.IoTDataPlane); + public static readonly AwsServiceEndpointMetadata IoTJobsDataPlane = new("IoT Jobs Data Plane", "iot-jobs-data", CommonEndpointPattern, 4589, AwsService.IoTJobsDataPlane); + public static readonly AwsServiceEndpointMetadata CognitoIdp = new("Cognito Identity Provider", "cognito-idp", CommonEndpointPattern, 4590, AwsService.CognitoIdp); + public static readonly AwsServiceEndpointMetadata CognitoIdentity = new("Cognito Identity", "cognito-identity", CommonEndpointPattern, 4591, AwsService.CognitoIdentity); + public static readonly AwsServiceEndpointMetadata Sts = new("STS", "sts", CommonEndpointPattern, 4592, AwsService.Sts); + public static readonly AwsServiceEndpointMetadata Iam = new("IAM", "iam", CommonEndpointPattern, 4593, AwsService.Iam); + public static readonly AwsServiceEndpointMetadata Rds = new("RDS", "rds", CommonEndpointPattern, 4594, AwsService.Rds); + public static readonly AwsServiceEndpointMetadata RdsData = new("RDS Data", "rds-data", CommonEndpointPattern, 4594, AwsService.RdsData); + public static readonly AwsServiceEndpointMetadata CloudSearch = new("CloudSearch", "cloudsearch", CommonEndpointPattern, 4595, AwsService.CloudSearch); + public static readonly AwsServiceEndpointMetadata Swf = new("SWF", "swf", CommonEndpointPattern, 4596, AwsService.Swf); + public static readonly AwsServiceEndpointMetadata Ec2 = new("EC2", "ec2", CommonEndpointPattern, 4597, AwsService.Ec2); + public static readonly AwsServiceEndpointMetadata ElastiCache = new("ElastiCache", "elasticache", CommonEndpointPattern, 4598, AwsService.ElastiCache); + public static readonly AwsServiceEndpointMetadata Kms = new("KMS", "kms", CommonEndpointPattern, 4599, AwsService.Kms); + public static readonly AwsServiceEndpointMetadata Emr = new("EMR", "emr", CommonEndpointPattern, 4600, AwsService.Emr); + public static readonly AwsServiceEndpointMetadata Ecs = new("ECS", "ecs", CommonEndpointPattern, 4601, AwsService.Ecs); + public static readonly AwsServiceEndpointMetadata Eks = new("EKS", "eks", CommonEndpointPattern, 4602, AwsService.Eks); + public static readonly AwsServiceEndpointMetadata XRay = new("XRay", "xray", CommonEndpointPattern, 4603, AwsService.XRay); + public static readonly AwsServiceEndpointMetadata ElasticBeanstalk = new("Elastic Beanstalk", "elasticbeanstalk", CommonEndpointPattern, 4604, AwsService.ElasticBeanstalk); + public static readonly AwsServiceEndpointMetadata AppSync = new("AppSync", "appsync", CommonEndpointPattern, 4605, AwsService.AppSync); + public static readonly AwsServiceEndpointMetadata CloudFront = new("CloudFront", "cloudfront", CommonEndpointPattern, 4606, AwsService.CloudFront); + public static readonly AwsServiceEndpointMetadata Athena = new("Athena", "athena", CommonEndpointPattern, 4607, AwsService.Athena); + public static readonly AwsServiceEndpointMetadata Glue = new("Glue", "glue", CommonEndpointPattern, 4608, AwsService.Glue); + public static readonly AwsServiceEndpointMetadata SageMaker = new("SageMaker", "sagemaker", CommonEndpointPattern, 4609, AwsService.SageMaker); + public static readonly AwsServiceEndpointMetadata SageMakerRuntime = new("SageMaker Runtime", "sagemaker-runtime", CommonEndpointPattern, 4609, AwsService.SageMakerRuntime); + public static readonly AwsServiceEndpointMetadata Ecr = new("ECR", "ecr", CommonEndpointPattern, 4610, AwsService.Ecr); + public static readonly AwsServiceEndpointMetadata Qldb = new("QLDB", "qldb", CommonEndpointPattern, 4611, AwsService.Qldb); + public static readonly AwsServiceEndpointMetadata QldbSession = new("QLDB Session", "qldb-session", CommonEndpointPattern, 4611, AwsService.QldbSession); + public static readonly AwsServiceEndpointMetadata CloudTrail = new("CloudTrail", "cloudtrail", CommonEndpointPattern, 4612, AwsService.CloudTrail); + public static readonly AwsServiceEndpointMetadata Glacier = new("Glacier", "glacier", CommonEndpointPattern, 4613, AwsService.Glacier); + public static readonly AwsServiceEndpointMetadata Batch = new("Batch", "batch", CommonEndpointPattern, 4614, AwsService.Batch); + public static readonly AwsServiceEndpointMetadata Organizations = new("Organizations", "organizations", CommonEndpointPattern, 4615, AwsService.Organizations); + public static readonly AwsServiceEndpointMetadata AutoScaling = new("Auto Scaling", "autoscaling", CommonEndpointPattern, 4616, AwsService.AutoScaling); + public static readonly AwsServiceEndpointMetadata MediaStore = new("MediaStore", "mediastore", CommonEndpointPattern, 4617, AwsService.MediaStore); + public static readonly AwsServiceEndpointMetadata MediaStoreData = new("MediaStore Data", "mediastore-data", CommonEndpointPattern, 4617, AwsService.MediaStoreData); + public static readonly AwsServiceEndpointMetadata Transfer = new("Transfer", "transfer", CommonEndpointPattern, 4618, AwsService.Transfer); + public static readonly AwsServiceEndpointMetadata Acm = new("ACM", "acm", CommonEndpointPattern, 4619, AwsService.Acm); + public static readonly AwsServiceEndpointMetadata CodeCommit = new("CodeCommit", "codecommit", CommonEndpointPattern, 4620, AwsService.CodeCommit); + public static readonly AwsServiceEndpointMetadata KinesisAnalytics = new("Kinesis Analytics", "kinesisanalytics", CommonEndpointPattern, 4621, AwsService.KinesisAnalytics); + public static readonly AwsServiceEndpointMetadata KinesisAnalyticsV2 = new("Kinesis Analytics V2", "kinesisanalyticsv2", CommonEndpointPattern, 4621, AwsService.KinesisAnalyticsV2); + public static readonly AwsServiceEndpointMetadata Amplify = new("Amplify", "amplify", CommonEndpointPattern, 4622, AwsService.Amplify); + public static readonly AwsServiceEndpointMetadata ApplicationAutoscaling = new("Application Auto Scaling", "application-autoscaling", CommonEndpointPattern, 4623, AwsService.ApplicationAutoscaling); + public static readonly AwsServiceEndpointMetadata Kafka = new("Kafka", "kafka", CommonEndpointPattern, 4624, AwsService.Kafka); + public static readonly AwsServiceEndpointMetadata ApiGatewayManagementApi = new("ApiGatewayManagementApi", "apigatewaymanagementapi", CommonEndpointPattern, 4625, AwsService.ApiGatewayManagementApi); + public static readonly AwsServiceEndpointMetadata TimeStreamQuery = new("Timestream Query", "timestream-query", CommonEndpointPattern, 4626, AwsService.TimeStreamQuery); + public static readonly AwsServiceEndpointMetadata TimeStreamWrite = new("Timestream Write", "timestream-write", CommonEndpointPattern, 4626, AwsService.TimeStreamWrite); + public static readonly AwsServiceEndpointMetadata S3Control = new("S3 Control", "s3control", CommonEndpointPattern, 4627, AwsService.S3Control); + public static readonly AwsServiceEndpointMetadata ElbV2 = new("Elastic Load Balancing v2", "elbv2", CommonEndpointPattern, 4628, AwsService.ElbV2); + public static readonly AwsServiceEndpointMetadata Support = new("Support", "support", CommonEndpointPattern, 4629, AwsService.Support); + public static readonly AwsServiceEndpointMetadata Neptune = new("Neptune", "neptune", CommonEndpointPattern, 4594, AwsService.Neptune); + public static readonly AwsServiceEndpointMetadata DocDb = new("DocDB", "docdb", CommonEndpointPattern, 4594, AwsService.DocDb); + public static readonly AwsServiceEndpointMetadata ServiceDiscovery = new("ServiceDiscovery", "servicediscovery", CommonEndpointPattern, 4630, AwsService.ServiceDiscovery); + public static readonly AwsServiceEndpointMetadata ServerlessApplicationRepository = new("ServerlessApplicationRepository", "serverlessrepo", CommonEndpointPattern, 4631, AwsService.ServerlessApplicationRepository); + public static readonly AwsServiceEndpointMetadata AppConfig = new("AppConfig", "appconfig", CommonEndpointPattern, 4632, AwsService.AppConfig); + public static readonly AwsServiceEndpointMetadata CostExplorer = new("Cost Explorer", "ce", CommonEndpointPattern, 4633, AwsService.CostExplorer); + public static readonly AwsServiceEndpointMetadata MediaConvert = new("MediaConvert", "mediaconvert", CommonEndpointPattern, 4634, AwsService.MediaConvert); + public static readonly AwsServiceEndpointMetadata ResourceGroupsTaggingApi = new("Resource Groups Tagging API", "resourcegroupstaggingapi", CommonEndpointPattern, 4635, AwsService.ResourceGroupsTaggingApi); + public static readonly AwsServiceEndpointMetadata ResourceGroups = new("Resource Groups", "resource-groups", CommonEndpointPattern, 4636, AwsService.ResourceGroups); + public static readonly AwsServiceEndpointMetadata Efs = new("EFS", "efs", CommonEndpointPattern, 4637, AwsService.Efs); + public static readonly AwsServiceEndpointMetadata Backup = new("Backup", "backup", CommonEndpointPattern, 4638, AwsService.Backup); + public static readonly AwsServiceEndpointMetadata LakeFormation = new("LakeFormation", "lakeformation", CommonEndpointPattern, 4639, AwsService.LakeFormation); + public static readonly AwsServiceEndpointMetadata Waf = new("WAF", "waf", CommonEndpointPattern, 4640, AwsService.Waf); + public static readonly AwsServiceEndpointMetadata WafV2 = new("WAFV2", "wafv2", CommonEndpointPattern, 4640, AwsService.WafV2); + public static readonly AwsServiceEndpointMetadata ConfigService = new("Config Service", "config", CommonEndpointPattern, 4641, AwsService.ConfigService); + public static readonly AwsServiceEndpointMetadata Mwaa = new("MWAA", "mwaa", CommonEndpointPattern, 4642, AwsService.Mwaa); + public static readonly AwsServiceEndpointMetadata EventBridge = new("EventBridge", "eventbridge", CommonEndpointPattern, 4587, AwsService.EventBridge); + public static readonly AwsServiceEndpointMetadata Fis = new("fis", "fis", CommonEndpointPattern, 4643, AwsService.Fis); + public static readonly AwsServiceEndpointMetadata MarketplaceMetering = new("Marketplace Metering", "meteringmarketplace", CommonEndpointPattern, 4644, AwsService.MarketplaceMetering); + public static readonly AwsServiceEndpointMetadata Transcribe = new("Transcribe", "transcribe", CommonEndpointPattern, 4566, AwsService.Transcribe); + public static readonly AwsServiceEndpointMetadata Mq = new("mq", "mq", CommonEndpointPattern, 4566, AwsService.Mq); + public static readonly AwsServiceEndpointMetadata EmrServerless = new("EMR Serverless", "emr-serverless", CommonEndpointPattern, 4566, AwsService.EmrServerless); + 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, + 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, 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) { - private const string CommonEndpointPattern = "{0}://{1}:{2}"; - - public static readonly AwsServiceEndpointMetadata ApiGateway = new AwsServiceEndpointMetadata("API Gateway", "apigateway", CommonEndpointPattern, 4567, AwsServiceEnum.ApiGateway); - public static readonly AwsServiceEndpointMetadata ApiGatewayV2 = new AwsServiceEndpointMetadata("ApiGatewayV2", "apigatewayv2", CommonEndpointPattern, 4567, AwsServiceEnum.ApiGatewayV2); - public static readonly AwsServiceEndpointMetadata Kinesis = new AwsServiceEndpointMetadata("Kinesis", "kinesis", CommonEndpointPattern, 4568, AwsServiceEnum.Kinesis); - public static readonly AwsServiceEndpointMetadata DynamoDb = new AwsServiceEndpointMetadata("DynamoDB", "dynamodb", CommonEndpointPattern, 4569, AwsServiceEnum.DynamoDb); - public static readonly AwsServiceEndpointMetadata DynamoDbStreams = new AwsServiceEndpointMetadata("DynamoDB Streams", "dynamodbstreams", CommonEndpointPattern, 4570, AwsServiceEnum.DynamoDbStreams); - public static readonly AwsServiceEndpointMetadata ElasticSearch = new AwsServiceEndpointMetadata("Elasticsearch Service", "elasticsearch", CommonEndpointPattern, 4571, AwsServiceEnum.ElasticSearch); - public static readonly AwsServiceEndpointMetadata S3 = new AwsServiceEndpointMetadata("S3", "s3", CommonEndpointPattern, 4572, AwsServiceEnum.S3); - public static readonly AwsServiceEndpointMetadata Firehose = new AwsServiceEndpointMetadata("Firehose", "firehose", CommonEndpointPattern, 4573, AwsServiceEnum.Firehose); - public static readonly AwsServiceEndpointMetadata Lambda = new AwsServiceEndpointMetadata("Lambda", "lambda", CommonEndpointPattern, 4574, AwsServiceEnum.Lambda); - public static readonly AwsServiceEndpointMetadata Sns = new AwsServiceEndpointMetadata("SNS", "sns", CommonEndpointPattern, 4575, AwsServiceEnum.Sns); - public static readonly AwsServiceEndpointMetadata Sqs = new AwsServiceEndpointMetadata("SQS", "sqs", CommonEndpointPattern, 4576, AwsServiceEnum.Sqs); - public static readonly AwsServiceEndpointMetadata Redshift = new AwsServiceEndpointMetadata("Redshift", "redshift", CommonEndpointPattern, 4577, AwsServiceEnum.Redshift); - public static readonly AwsServiceEndpointMetadata Es = new AwsServiceEndpointMetadata("ES", "es", CommonEndpointPattern, 4578, AwsServiceEnum.Es); - public static readonly AwsServiceEndpointMetadata Ses = new AwsServiceEndpointMetadata("SES", "ses", CommonEndpointPattern, 4579, AwsServiceEnum.Ses); - public static readonly AwsServiceEndpointMetadata Route53 = new AwsServiceEndpointMetadata("Route 53", "route53", CommonEndpointPattern, 4580, AwsServiceEnum.Route53); - public static readonly AwsServiceEndpointMetadata CloudFormation = new AwsServiceEndpointMetadata("CloudFormation", "cloudformation", CommonEndpointPattern, 4581, AwsServiceEnum.CloudFormation); - public static readonly AwsServiceEndpointMetadata CloudWatch = new AwsServiceEndpointMetadata("CloudWatch", "cloudwatch", CommonEndpointPattern, 4582, AwsServiceEnum.CloudWatch); - public static readonly AwsServiceEndpointMetadata Ssm = new AwsServiceEndpointMetadata("SSM", "ssm", CommonEndpointPattern, 4583, AwsServiceEnum.Ssm); - public static readonly AwsServiceEndpointMetadata SecretsManager = new AwsServiceEndpointMetadata("Secrets Manager", "secretsmanager", CommonEndpointPattern, 4584, AwsServiceEnum.SecretsManager); - public static readonly AwsServiceEndpointMetadata StepFunctions = new AwsServiceEndpointMetadata("SFN", "stepfunctions", CommonEndpointPattern, 4585, AwsServiceEnum.StepFunctions); - public static readonly AwsServiceEndpointMetadata Logs = new AwsServiceEndpointMetadata("CloudWatch Logs", "logs", CommonEndpointPattern, 4586, AwsServiceEnum.Logs); - public static readonly AwsServiceEndpointMetadata Events = new AwsServiceEndpointMetadata("CloudWatch Events", "events", CommonEndpointPattern, 4587, AwsServiceEnum.Events); - public static readonly AwsServiceEndpointMetadata Elb = new AwsServiceEndpointMetadata("Elastic Load Balancing", "elb", CommonEndpointPattern, 4588, AwsServiceEnum.Elb); - public static readonly AwsServiceEndpointMetadata Iot = new AwsServiceEndpointMetadata("IoT", "iot", CommonEndpointPattern, 4589, AwsServiceEnum.Iot); - public static readonly AwsServiceEndpointMetadata CognitoIdp = new AwsServiceEndpointMetadata("Cognito Identity Provider", "cognito-idp", CommonEndpointPattern, 4590, AwsServiceEnum.CognitoIdp); - public static readonly AwsServiceEndpointMetadata CognitoIdentity = new AwsServiceEndpointMetadata("Cognito Identity", "cognito-identity", CommonEndpointPattern, 4591, AwsServiceEnum.CognitoIdentity); - public static readonly AwsServiceEndpointMetadata Sts = new AwsServiceEndpointMetadata("STS", "sts", CommonEndpointPattern, 4592, AwsServiceEnum.Sts); - public static readonly AwsServiceEndpointMetadata Iam = new AwsServiceEndpointMetadata("IAM", "iam", CommonEndpointPattern, 4593, AwsServiceEnum.Iam); - public static readonly AwsServiceEndpointMetadata Rds = new AwsServiceEndpointMetadata("RDS", "rds", CommonEndpointPattern, 4594, AwsServiceEnum.Rds); - public static readonly AwsServiceEndpointMetadata RdsData = new AwsServiceEndpointMetadata("RDS Data", "rds-data", CommonEndpointPattern, 4594, AwsServiceEnum.RdsData); - public static readonly AwsServiceEndpointMetadata CloudSearch = new AwsServiceEndpointMetadata("CloudSearch", "cloudsearch", CommonEndpointPattern, 4595, AwsServiceEnum.CloudSearch); - public static readonly AwsServiceEndpointMetadata Swf = new AwsServiceEndpointMetadata("SWF", "swf", CommonEndpointPattern, 4596, AwsServiceEnum.Swf); - public static readonly AwsServiceEndpointMetadata Ec2 = new AwsServiceEndpointMetadata("EC2", "ec2", CommonEndpointPattern, 4597, AwsServiceEnum.Ec2); - public static readonly AwsServiceEndpointMetadata ElastiCache = new AwsServiceEndpointMetadata("ElastiCache", "elasticache", CommonEndpointPattern, 4598, AwsServiceEnum.ElastiCache); - public static readonly AwsServiceEndpointMetadata Kms = new AwsServiceEndpointMetadata("KMS", "kms", CommonEndpointPattern, 4599, AwsServiceEnum.Kms); - public static readonly AwsServiceEndpointMetadata Emr = new AwsServiceEndpointMetadata("EMR", "emr", CommonEndpointPattern, 4600, AwsServiceEnum.Emr); - public static readonly AwsServiceEndpointMetadata Ecs = new AwsServiceEndpointMetadata("ECS", "ecs", CommonEndpointPattern, 4601, AwsServiceEnum.Ecs); - public static readonly AwsServiceEndpointMetadata Eks = new AwsServiceEndpointMetadata("EKS", "eks", CommonEndpointPattern, 4602, AwsServiceEnum.Eks); - public static readonly AwsServiceEndpointMetadata XRay = new AwsServiceEndpointMetadata("XRay", "xray", CommonEndpointPattern, 4603, AwsServiceEnum.XRay); - public static readonly AwsServiceEndpointMetadata ElasticBeanstalk = new AwsServiceEndpointMetadata("Elastic Beanstalk", "elasticbeanstalk", CommonEndpointPattern, 4604, AwsServiceEnum.ElasticBeanstalk); - public static readonly AwsServiceEndpointMetadata AppSync = new AwsServiceEndpointMetadata("AppSync", "appsync", CommonEndpointPattern, 4605, AwsServiceEnum.AppSync); - public static readonly AwsServiceEndpointMetadata CloudFront = new AwsServiceEndpointMetadata("CloudFront", "cloudfront", CommonEndpointPattern, 4606, AwsServiceEnum.CloudFront); - public static readonly AwsServiceEndpointMetadata Athena = new AwsServiceEndpointMetadata("Athena", "athena", CommonEndpointPattern, 4607, AwsServiceEnum.Athena); - public static readonly AwsServiceEndpointMetadata Glue = new AwsServiceEndpointMetadata("Glue", "glue", CommonEndpointPattern, 4608, AwsServiceEnum.Glue); - public static readonly AwsServiceEndpointMetadata SageMaker = new AwsServiceEndpointMetadata("SageMaker", "sagemaker", CommonEndpointPattern, 4609, AwsServiceEnum.SageMaker); - public static readonly AwsServiceEndpointMetadata SageMakerRuntime = new AwsServiceEndpointMetadata("SageMaker Runtime", "sagemaker-runtime", CommonEndpointPattern, 4609, AwsServiceEnum.SageMakerRuntime); - public static readonly AwsServiceEndpointMetadata Ecr = new AwsServiceEndpointMetadata("ECR", "ecr", CommonEndpointPattern, 4610, AwsServiceEnum.Ecr); - public static readonly AwsServiceEndpointMetadata Qldb = new AwsServiceEndpointMetadata("QLDB", "qldb", CommonEndpointPattern, 4611, AwsServiceEnum.Qldb); - public static readonly AwsServiceEndpointMetadata CloudTrail = new AwsServiceEndpointMetadata("CloudTrail", "cloudtrail", CommonEndpointPattern, 4612, AwsServiceEnum.CloudTrail); - public static readonly AwsServiceEndpointMetadata Glacier = new AwsServiceEndpointMetadata("Glacier", "glacier", CommonEndpointPattern, 4613, AwsServiceEnum.Glacier); - public static readonly AwsServiceEndpointMetadata Batch = new AwsServiceEndpointMetadata("Batch", "batch", CommonEndpointPattern, 4614, AwsServiceEnum.Batch); - public static readonly AwsServiceEndpointMetadata Organizations = new AwsServiceEndpointMetadata("Organizations", "organizations", CommonEndpointPattern, 4615, AwsServiceEnum.Organizations); - public static readonly AwsServiceEndpointMetadata AutoScaling = new AwsServiceEndpointMetadata("Auto Scaling", "autoscaling", CommonEndpointPattern, 4616, AwsServiceEnum.AutoScaling); - public static readonly AwsServiceEndpointMetadata MediaStore = new AwsServiceEndpointMetadata("MediaStore", "mediastore", CommonEndpointPattern, 4617, AwsServiceEnum.MediaStore); - public static readonly AwsServiceEndpointMetadata MediaStoreData = new AwsServiceEndpointMetadata("MediaStore Data", "mediastore-data", CommonEndpointPattern, 4617, AwsServiceEnum.MediaStoreData); - - public static readonly AwsServiceEndpointMetadata[] All = - { - ApiGateway, ApiGatewayV2, Kinesis, DynamoDb, DynamoDbStreams, ElasticSearch, S3, Firehose, Lambda, Sns, Sqs, Redshift, Es, Ses, Route53, CloudFormation, CloudWatch, - Ssm, SecretsManager, StepFunctions, Logs, Events, Elb, Iot, CognitoIdp, CognitoIdentity, Sts, Iam, Rds, RdsData, CloudSearch, Swf, Ec2, ElastiCache, Kms, Emr, Ecs, - Eks, XRay, ElasticBeanstalk, AppSync, CloudFront, Athena, Glue, SageMaker, SageMakerRuntime, Ecr, Qldb, CloudTrail, Glacier, Batch, Organizations, AutoScaling, MediaStore, MediaStoreData - }; - - private AwsServiceEndpointMetadata() - { - } - - private AwsServiceEndpointMetadata(string serviceId, string cliName, string endPointPattern, int port, AwsServiceEnum @enum) - { - ServiceId = serviceId; - CliName = cliName; - EndPointPattern = endPointPattern; - Enum = @enum; - Port = port; - } + ServiceId = serviceId; + CliName = cliName; + EndPointPattern = endPointPattern; + Enum = @enum; + Port = port; + } - public string ServiceId { get; } + public string ServiceId { get; } - public string CliName { get; } + public string CliName { get; } - public string EndPointPattern { get; } + public string EndPointPattern { get; } - public int Port { get; } + public int Port { get; } - public AwsServiceEnum Enum { get; } + public AwsService Enum { get; } - public static AwsServiceEndpointMetadata ByName(string name) + public static AwsServiceEndpointMetadata? ByName(string name) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - return All.SingleOrDefault(service => service.ServiceId == name); + throw new ArgumentNullException(nameof(name)); } - public static AwsServiceEndpointMetadata ByEnum(AwsServiceEnum @enum) - { - return All.SingleOrDefault(service => service.Enum == @enum); - } + return All.SingleOrDefault(service => service.ServiceId == name); + } - public static AwsServiceEndpointMetadata ByPort(int port) - { - if (port <= 0) - { - throw new ArgumentException("Your port number must be greater than 0", nameof(port)); - } + public static AwsServiceEndpointMetadata? ByEnum(AwsService @enum) + { + return All.SingleOrDefault(service => service.Enum == @enum); + } - return All.SingleOrDefault(service => service.Port == port); + public static AwsServiceEndpointMetadata? ByPort(int port) + { + if (port <= 0) + { + throw new ArgumentException("Your port number must be greater than 0", nameof(port)); } - public string ToString(string proto, string host) + return All.SingleOrDefault(service => service.Port == port); + } + + public Uri GetServiceUrl(string proto, string host, int? port = null) + { + if (string.IsNullOrWhiteSpace(proto)) { - return proto == null || host == null - ? throw new ArgumentNullException(proto == null ? nameof(proto) : nameof(host)) - : string.Format(EndPointPattern, proto, host, Port); + throw new ArgumentNullException(nameof(proto)); } - public override string ToString() + if (string.IsNullOrWhiteSpace(host)) { - return $"{ServiceId} - {CliName} - {Port}"; + throw new ArgumentNullException(nameof(host)); } + + string uriString = string.Format(CultureInfo.CurrentCulture, EndPointPattern, proto, host, port ?? Port); + + return new Uri(uriString); + } + + public override string ToString() + { + return $"{ServiceId} - {CliName} - {Port}"; } } \ No newline at end of file diff --git a/src/LocalStack.Client/Enums/AwsServiceEnum.cs b/src/LocalStack.Client/Enums/AwsServiceEnum.cs deleted file mode 100644 index 1af3a7f..0000000 --- a/src/LocalStack.Client/Enums/AwsServiceEnum.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace LocalStack.Client.Enums -{ - public enum AwsServiceEnum - { - ApiGateway, - ApiGatewayV2, - Kinesis, - DynamoDb, - DynamoDbStreams, - ElasticSearch, - S3, - Firehose, - Lambda, - Sns, - Sqs, - Redshift, - Es, - Ses, - Route53, - CloudFormation, - CloudWatch, - Ssm, - SecretsManager, - StepFunctions, - Logs, - Events, - Elb, - Iot, - CognitoIdp, - CognitoIdentity, - Sts, - Iam, - Rds, - RdsData, - CloudSearch, - Swf, - Ec2, - ElastiCache, - Kms, - Emr, - Ecs, - Eks, - XRay, - ElasticBeanstalk, - AppSync, - CloudFront, - Athena, - Glue, - SageMaker, - SageMakerRuntime, - Ecr, - Qldb, - CloudTrail, - Glacier, - Batch, - Organizations, - AutoScaling, - MediaStore, - MediaStoreData - } -} \ No newline at end of file diff --git a/src/LocalStack.Client/Exceptions/LocalStackClientException.cs b/src/LocalStack.Client/Exceptions/LocalStackClientException.cs new file mode 100644 index 0000000..03aced5 --- /dev/null +++ b/src/LocalStack.Client/Exceptions/LocalStackClientException.cs @@ -0,0 +1,45 @@ +namespace LocalStack.Client.Exceptions; + +[Serializable] +#if NETFRAMEWORK +public class LocalStackClientException : Exception, System.Runtime.InteropServices._Exception +#else +public class LocalStackClientException : Exception +#endif +{ + /// + /// Construct instance of ConfigurationException + /// + /// The error message. + protected LocalStackClientException(string message) : base(message) + { + } + + /// + /// Construct instance of ConfigurationException + /// + /// The error message. + /// Original exception. + protected LocalStackClientException(string message, Exception exception) : base(message, exception) + { + } + + /// + protected LocalStackClientException() + { + } + + /// + /// Serialization constructor. + /// + /// 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 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 new file mode 100644 index 0000000..5ab7bb9 --- /dev/null +++ b/src/LocalStack.Client/Exceptions/MisconfiguredClientException.cs @@ -0,0 +1,30 @@ +namespace LocalStack.Client.Exceptions; + +[Serializable] +public class MisconfiguredClientException : LocalStackClientException +{ + /// + public MisconfiguredClientException(string message) : base(message) + { + } + + /// + public MisconfiguredClientException(string message, Exception exception) : base(message, exception) + { + } + + /// + 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) + { + } +} \ No newline at end of file diff --git a/src/LocalStack.Client/Exceptions/NotSupportedClientException.cs b/src/LocalStack.Client/Exceptions/NotSupportedClientException.cs new file mode 100644 index 0000000..e8bc125 --- /dev/null +++ b/src/LocalStack.Client/Exceptions/NotSupportedClientException.cs @@ -0,0 +1,28 @@ +namespace LocalStack.Client.Exceptions; + +[Serializable] +public class NotSupportedClientException : LocalStackClientException +{ + /// + public NotSupportedClientException(string message) : base(message) + { + } + + /// + public NotSupportedClientException(string message, Exception exception) : base(message, exception) + { + } + + 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) + { + } +} \ No newline at end of file diff --git a/src/LocalStack.Client/GlobalUsings.cs b/src/LocalStack.Client/GlobalUsings.cs new file mode 100644 index 0000000..df142da --- /dev/null +++ b/src/LocalStack.Client/GlobalUsings.cs @@ -0,0 +1,34 @@ +global using System; +global using System.Collections.Generic; +global using System.Diagnostics.CodeAnalysis; +global using System.Globalization; +global using System.Linq; +global using System.Reflection; +global using System.Runtime.Serialization; + +global using Amazon; +global using Amazon.Runtime; +global using Amazon.Runtime.Internal; + +global using LocalStack.Client.Contracts; +global using LocalStack.Client.Enums; +global using LocalStack.Client.Exceptions; +global using LocalStack.Client.Models; +global using LocalStack.Client.Options; +global using LocalStack.Client.Utils; + +#pragma warning disable MA0048 // File name must match type name +#if NETSTANDARD || NET472 +namespace System.Runtime.CompilerServices +{ + using System.ComponentModel; + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} +#endif \ No newline at end of file diff --git a/src/LocalStack.Client/LICENSE.txt b/src/LocalStack.Client/LICENSE.txt index f83905e..f9478d1 100644 --- a/src/LocalStack.Client/LICENSE.txt +++ b/src/LocalStack.Client/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 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 63e2073..3a7e54c 100644 --- a/src/LocalStack.Client/LocalStack.Client.csproj +++ b/src/LocalStack.Client/LocalStack.Client.csproj @@ -1,60 +1,58 @@  - - netstandard2.0;net461 - true - LocalStack.Client - LocalStack.Client - latest - - LocalStack.NET Client - - This is an easy-to-use .NET client for LocalStack. The client library provides a thin wrapper around aws-sdk-net which automatically configures the target endpoints to use LocalStack for your local cloud application development. - - aws-sdk, localstack, client-library, dotnet;dotnet-core - LICENSE.txt - - - - true - - - /Library/Frameworks/Mono.framework/Versions/Current/lib/mono - /usr/lib/mono - /usr/local/lib/mono - - - $(BaseFrameworkPathOverrideForMono)/4.6.1-api - - true - - - $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths) - - - - - - - - - - - - Always - - - - - - - - - - - - - - + + netstandard2.0;net472;net8.0;net9.0 + LocalStack.Client + LocalStack.Client + + LocalStack.NET Client + + This is an easy-to-use .NET client for LocalStack. The client library provides a thin wrapper around aws-sdk-net which automatically configures the target endpoints to use LocalStack for your local cloud application development. + + $(PackageMainVersion) + aws-sdk, localstack, client-library, dotnet, dotnet-core + LICENSE.txt + README.md + true + 1.4.1 + + true + true + $(NoWarn);MA0006;CA1510 + + + + true + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + + + NET472 + \ No newline at end of file diff --git a/src/LocalStack.Client/Models/AwsServiceEndpoint.cs b/src/LocalStack.Client/Models/AwsServiceEndpoint.cs index e69b14c..f174443 100644 --- a/src/LocalStack.Client/Models/AwsServiceEndpoint.cs +++ b/src/LocalStack.Client/Models/AwsServiceEndpoint.cs @@ -1,34 +1,9 @@ -using LocalStack.Client.Enums; +namespace LocalStack.Client.Models; -namespace LocalStack.Client.Models +public record AwsServiceEndpoint(string ServiceId, string CliName, AwsService AwsService, int Port, string Host, Uri ServiceUrl) { - public class AwsServiceEndpoint + public override string ToString() { - public AwsServiceEndpoint(string serviceId, string cliName, AwsServiceEnum @enum, int port, string host, string serviceUrl) - { - ServiceId = serviceId; - CliName = cliName; - AwsServiceEnum = @enum; - Port = port; - Host = host; - ServiceUrl = serviceUrl; - } - - public string ServiceId { get; } - - public string CliName { get; } - - public AwsServiceEnum AwsServiceEnum { get; } - - public int Port { get; } - - public string Host { get; } - - public string ServiceUrl { get; } - - public override string ToString() - { - return $"{ServiceId} - {ServiceUrl}"; - } + return $"{ServiceId} - {ServiceUrl}"; } } \ No newline at end of file diff --git a/src/LocalStack.Client/Models/Constants.cs b/src/LocalStack.Client/Models/Constants.cs new file mode 100644 index 0000000..da82654 --- /dev/null +++ b/src/LocalStack.Client/Models/Constants.cs @@ -0,0 +1,20 @@ +namespace LocalStack.Client.Models; + +public static class Constants +{ + public const string LocalStackHost = "localhost.localstack.cloud"; + + public const bool UseSsl = false; + + public const bool UseLegacyPorts = false; + + public const int EdgePort = 4566; + + public const string AwsAccessKeyId = "accessKey"; + + public const string AwsAccessKey = "secretKey"; + + public const string AwsSessionToken = "token"; + + public const string RegionName = "us-east-1"; +} \ No newline at end of file diff --git a/src/LocalStack.Client/Models/SessionOptions.cs b/src/LocalStack.Client/Models/SessionOptions.cs deleted file mode 100644 index d17e91b..0000000 --- a/src/LocalStack.Client/Models/SessionOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LocalStack.Client.Contracts; - -namespace LocalStack.Client.Models -{ - public class SessionOptions : ISessionOptions - { - private const string AwsAccessKeyIdConst = "accessKey"; - private const string AwsAccessKeyConst = "secretKey"; - private const string AwsSessionTokenConst = "token"; - private const string RegionNameConst = "us-east-1"; - - public SessionOptions(string awsAccessKeyId = AwsAccessKeyIdConst, string awsAccessKey = AwsAccessKeyConst, string awsSessionToken = AwsSessionTokenConst, - string regionName = RegionNameConst) - { - AwsAccessKeyId = awsAccessKeyId ?? AwsAccessKeyIdConst; - AwsAccessKey = awsAccessKey ?? AwsAccessKeyConst; - AwsSessionToken = awsSessionToken ?? AwsSessionTokenConst; - RegionName = regionName ?? RegionNameConst; - } - - public string AwsAccessKeyId { get; } - - public string AwsAccessKey { get; } - - public string AwsSessionToken { get; } - - public string RegionName { get; } - } -} \ No newline at end of file diff --git a/src/LocalStack.Client/Options/ConfigOptions.cs b/src/LocalStack.Client/Options/ConfigOptions.cs new file mode 100644 index 0000000..f4c5957 --- /dev/null +++ b/src/LocalStack.Client/Options/ConfigOptions.cs @@ -0,0 +1,30 @@ +namespace LocalStack.Client.Options; + +#if NET8_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif +public class ConfigOptions : IConfigOptions +{ + public ConfigOptions() + { + } + + [SuppressMessage("Usage", "S3427: Method overloads with default parameter values should not overlap", + Justification = "The default constructor need for ConfigureOptions")] + public ConfigOptions(string localStackHost = Constants.LocalStackHost, bool useSsl = Constants.UseSsl, bool useLegacyPorts = Constants.UseLegacyPorts, + int edgePort = Constants.EdgePort) + { + LocalStackHost = localStackHost; + UseSsl = useSsl; + UseLegacyPorts = useLegacyPorts; + EdgePort = edgePort; + } + + public string LocalStackHost { get; private set; } = Constants.LocalStackHost; + + public bool UseSsl { get; private set;} = Constants.UseSsl; + + public bool UseLegacyPorts { get; private set;} = Constants.UseLegacyPorts; + + public int EdgePort { get; private set;} = Constants.EdgePort; +} \ No newline at end of file diff --git a/src/LocalStack.Client/Options/LocalStackOptions.cs b/src/LocalStack.Client/Options/LocalStackOptions.cs new file mode 100644 index 0000000..0e96aba --- /dev/null +++ b/src/LocalStack.Client/Options/LocalStackOptions.cs @@ -0,0 +1,27 @@ +namespace LocalStack.Client.Options; + +#if NET8_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif +public class LocalStackOptions : ILocalStackOptions +{ + public LocalStackOptions() + { + UseLocalStack = false; + Session = new SessionOptions(); + Config = new ConfigOptions(); + } + + public LocalStackOptions(bool useLocalStack, SessionOptions sessionOptions, ConfigOptions configOptions) + { + UseLocalStack = useLocalStack; + Session = sessionOptions; + Config = configOptions; + } + + public bool UseLocalStack { get; private set; } + + public SessionOptions Session { get; private set; } + + public ConfigOptions Config { get; private set; } +} \ No newline at end of file diff --git a/src/LocalStack.Client/Options/SessionOptions.cs b/src/LocalStack.Client/Options/SessionOptions.cs new file mode 100644 index 0000000..8cce43f --- /dev/null +++ b/src/LocalStack.Client/Options/SessionOptions.cs @@ -0,0 +1,30 @@ +namespace LocalStack.Client.Options; + +#if NET8_0_OR_GREATER +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] +#endif +public class SessionOptions : ISessionOptions +{ + public SessionOptions() + { + } + + [SuppressMessage("Usage", "S3427: Method overloads with default parameter values should not overlap", + Justification = "The default constructor need for ConfigureOptions")] + public SessionOptions(string awsAccessKeyId = Constants.AwsAccessKeyId, string awsAccessKey = Constants.AwsAccessKey, + string awsSessionToken = Constants.AwsSessionToken, string regionName = Constants.RegionName) + { + AwsAccessKeyId = awsAccessKeyId; + AwsAccessKey = awsAccessKey; + AwsSessionToken = awsSessionToken; + RegionName = regionName; + } + + public string AwsAccessKeyId { get; private set; } = Constants.AwsAccessKeyId; + + public string AwsAccessKey { get; private set; } = Constants.AwsAccessKey; + + public string AwsSessionToken { get; private set; } = Constants.AwsSessionToken; + + public string RegionName { get; private set; } = Constants.RegionName; +} \ No newline at end of file diff --git a/src/LocalStack.Client/README.md b/src/LocalStack.Client/README.md new file mode 100644 index 0000000..88d6fb0 --- /dev/null +++ b/src/LocalStack.Client/README.md @@ -0,0 +1,294 @@ +# 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. + +## 🚀 Platform Compatibility & Quality Status + +### Supported Platforms + +- [.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 + +1. [Supported Platforms](#supported-platforms) +2. [Why LocalStack.NET Client?](#why-localstacknet-client) +3. [Prerequisites](#prerequisites) +4. [Getting Started](#getting-started) + - [Setup](#setup) + - [Configuration](#configuration) +5. [Known Issues](#known-issues) +6. [Developing](#developing) + - [Building the Project](#building-the-project) + - [Sandbox Applications](#sandbox-applications) + - [Running Tests](#running-tests) +7. [Changelog](#changelog) +8. [License](#license) + +## 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 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. + +- **Enhanced Development Speed:** Reduces boilerplate and manual configurations, speeding up the development process. + +## Prerequisites + +To utilize this library, you need to have LocalStack running. While LocalStack can be installed directly on your machine and accessed via the localstack cli, the recommended approach is to run LocalStack using [Docker](https://docs.docker.com/get-docker/) or [docker-compose](https://docs.docker.com/compose/install/). + +For detailed installation and setup instructions, please refer to the [official LocalStack installation guide](https://docs.localstack.cloud/getting-started/installation/). + +## Getting Started + +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 +# 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). + +### Setup + +Here's a basic example of how to setup `LocalStack.NET`: + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + // Add framework services. + services.AddMvc(); + + services.AddLocalStack(Configuration) + services.AddDefaultAWSOptions(Configuration.GetAWSOptions()); + services.AddAwsService(); + services.AddAwsService(); +} +``` + +The `AddLocalStack` method integrates LocalStack.NET into your application, and the `AddAwsService` method allows you to specify which AWS services you want to use with LocalStack. + +(Alternatively, `AddAWSServiceLocalStack` method can be used to prevent mix-up with `AddAWSService`.) + +`AddLocalStack` extension method is responsible for both configurations and adding of `LocalStack.Client` dependencies to service collection. + +### Configuration + +To configure LocalStack.NET, you can use entries in the appsettings.json files. Here's a basic example for different environments: + +`appsettings.Development.json` + +```json +"LocalStack": { + "UseLocalStack": true, + "Session": { + "RegionName": "eu-central-1" + }, + "Config": { + "LocalStackHost": "localhost.localstack.cloud", // or "localhost", + "EdgePort": 4566 + } +} +``` + +`appsettings.Production.json` + +```json +"LocalStack": { + "UseLocalStack": false +}, +"AWS": { + "Profile": "", + "Region": "eu-central-1" +} +``` + +The `RegionName` is important as LocalStack creates resources based on the specified region. For more advanced configurations and understanding how LocalStack.NET operates with LocalStack, refer to [documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Setup#configuration). + +## Known Issues + +- **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. + +```csharp +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. + +### Building the Project + +To build the project, use the following commands based on your operating system: + +Windows + +```powershell +build.ps1 +``` + +Linux + +```bash +./build.sh +``` + +### Sandbox Applications + +The LocalStack .NET repository includes several sandbox console applications located in [tests/sandboxes](https://github.com/localstack-dotnet/localstack-dotnet-client/tree/master/tests/sandboxes). These applications serve both as testing tools and as examples. Refer to [the documentation](https://github.com/localstack-dotnet/localstack-dotnet-client/wiki/Developing#sandbox-applications) for more information + +### Running Tests + +To execute the tests, use the commands below: + +Windows + +```powershell +build.ps1 --target=tests +``` + +Linux + +```bash +./build.sh --target=tests +``` + +## Changelog + +Please refer to [`CHANGELOG.md`](CHANGELOG.md) to see the complete list of changes for each release. + +## License + +Licensed under MIT, see [LICENSE](LICENSE) for the full text. diff --git a/src/LocalStack.Client/Session.cs b/src/LocalStack.Client/Session.cs index db83726..72a5c5a 100644 --- a/src/LocalStack.Client/Session.cs +++ b/src/LocalStack.Client/Session.cs @@ -1,44 +1,143 @@ -using Amazon.Runtime; -using Amazon.Runtime.Internal; +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - disabled because of it's not possible for this case +#pragma warning disable CS8603 // Possible null reference return. - disabled because of it's not possible for this case +namespace LocalStack.Client; -using LocalStack.Client.Contracts; -using LocalStack.Client.Models; +public class Session : ISession +{ + private readonly IConfig _config; + private readonly ISessionOptions _sessionOptions; + private readonly ISessionReflection _sessionReflection; -using System; + public Session(ISessionOptions sessionOptions, IConfig config, ISessionReflection sessionReflection) + { + _sessionOptions = sessionOptions; + _config = config; + _sessionReflection = sessionReflection; + } -namespace LocalStack.Client -{ - public class Session : ISession +#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 { - private readonly IConfig _config; - private readonly ISessionOptions _sessionOptions; - private readonly ISessionReflection _sessionReflection; + Type clientType = typeof(TClient); + + return (TClient)CreateClientByImplementation(clientType, useServiceUrl); + } - public Session(ISessionOptions sessionOptions, IConfig config, ISessionReflection 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 AmazonServiceClient CreateClientByImplementation( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicFields)] +#endif + Type implType, + bool useServiceUrl = false) + { + if (!useServiceUrl && string.IsNullOrWhiteSpace(_sessionOptions.RegionName)) { - _sessionOptions = sessionOptions; - _config = config; - _sessionReflection = sessionReflection; + throw new MisconfiguredClientException($"{nameof(_sessionOptions.RegionName)} must be set if {nameof(useServiceUrl)} is false."); } - public TClient CreateClient() where TClient : AmazonServiceClient + IServiceMetadata serviceMetadata = _sessionReflection.ExtractServiceMetadata(implType); + AwsServiceEndpoint awsServiceEndpoint = _config.GetAwsServiceEndpoint(serviceMetadata.ServiceId) ?? + throw new NotSupportedClientException($"{serviceMetadata.ServiceId} is not supported by this mock session."); + + AWSCredentials awsCredentials = new SessionAWSCredentials(_sessionOptions.AwsAccessKeyId, _sessionOptions.AwsAccessKey, _sessionOptions.AwsSessionToken); + ClientConfig clientConfig = _sessionReflection.CreateClientConfig(implType); + + clientConfig.UseHttp = !_config.GetConfigOptions().UseSsl; + _sessionReflection.SetForcePathStyle(clientConfig); + clientConfig.ProxyHost = awsServiceEndpoint.Host; + clientConfig.ProxyPort = awsServiceEndpoint.Port; + + if (useServiceUrl) + { + clientConfig.ServiceURL = awsServiceEndpoint.ServiceUrl.AbsoluteUri; + } + else if (!string.IsNullOrWhiteSpace(_sessionOptions.RegionName)) { - IServiceMetadata serviceMetadata = _sessionReflection.ExtractServiceMetadata(); - AwsServiceEndpoint awsServiceEndpoint = _config.GetAwsServiceEndpoint(serviceMetadata.ServiceId) ?? - throw new InvalidOperationException($"{serviceMetadata.ServiceId} is not supported by this mock session."); + clientConfig.RegionEndpoint = RegionEndpoint.GetBySystemName(_sessionOptions.RegionName); + } + + var clientInstance = (AmazonServiceClient)Activator.CreateInstance(implType, awsCredentials, clientConfig); - AWSCredentials awsCredentials = new SessionAWSCredentials(_sessionOptions.AwsAccessKeyId, _sessionOptions.AwsAccessKey, _sessionOptions.AwsSessionToken); - ClientConfig clientConfig = _sessionReflection.CreateClientConfig(); + return clientInstance; + } - clientConfig.ServiceURL = awsServiceEndpoint.ServiceUrl; - clientConfig.UseHttp = true; - _sessionReflection.SetForcePathStyle(clientConfig); - clientConfig.ProxyHost = awsServiceEndpoint.Host; - clientConfig.ProxyPort = awsServiceEndpoint.Port; +#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); - var clientInstance = (TClient) Activator.CreateInstance(typeof(TClient), awsCredentials, clientConfig); + return CreateClientByInterface(serviceInterfaceType, useServiceUrl); + } - return clientInstance; + public AmazonServiceClient CreateClientByInterface( +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicFields)] +#endif + Type serviceInterfaceType, + bool useServiceUrl = false) + { + if (serviceInterfaceType == null) + { + throw new ArgumentNullException(nameof(serviceInterfaceType)); } + + var clientTypeName = $"{serviceInterfaceType.Namespace}.{serviceInterfaceType.Name.Substring(1)}Client"; + Type clientType = serviceInterfaceType.GetTypeInfo().Assembly.GetType(clientTypeName); + + if (clientType == null) + { + throw new AmazonClientException($"Failed to find service client {clientTypeName} which implements {serviceInterfaceType.FullName}."); + } + + if (!useServiceUrl && string.IsNullOrWhiteSpace(_sessionOptions.RegionName)) + { + throw new MisconfiguredClientException($"{nameof(_sessionOptions.RegionName)} must be set if {nameof(useServiceUrl)} is false."); + } + + IServiceMetadata serviceMetadata = _sessionReflection.ExtractServiceMetadata(clientType); + + AwsServiceEndpoint awsServiceEndpoint = _config.GetAwsServiceEndpoint(serviceMetadata.ServiceId) ?? + throw new NotSupportedClientException($"{serviceMetadata.ServiceId} is not supported by this mock session."); + + AWSCredentials awsCredentials = new SessionAWSCredentials(_sessionOptions.AwsAccessKeyId, _sessionOptions.AwsAccessKey, _sessionOptions.AwsSessionToken); + + ClientConfig clientConfig = _sessionReflection.CreateClientConfig(clientType); + + clientConfig.UseHttp = !_config.GetConfigOptions().UseSsl; + _sessionReflection.SetForcePathStyle(clientConfig); + clientConfig.ProxyHost = awsServiceEndpoint.Host; + clientConfig.ProxyPort = awsServiceEndpoint.Port; + + if (useServiceUrl) + { + clientConfig.ServiceURL = awsServiceEndpoint.ServiceUrl.AbsoluteUri; + } + else if (!string.IsNullOrWhiteSpace(_sessionOptions.RegionName)) + { + clientConfig.RegionEndpoint = RegionEndpoint.GetBySystemName(_sessionOptions.RegionName); + } + + ConstructorInfo? constructor = clientType.GetConstructor(new[] { typeof(AWSCredentials), clientConfig.GetType() }); + + if (constructor == null) + { + var message = $"Service client {clientTypeName} missing a constructor with parameters AWSCredentials and {clientConfig.GetType().FullName}."; + + throw new AmazonClientException(message); + } + + var client = (AmazonServiceClient)constructor.Invoke(new object[] { awsCredentials, clientConfig }); + + return client; } } \ No newline at end of file diff --git a/src/LocalStack.Client/SessionStandalone.cs b/src/LocalStack.Client/SessionStandalone.cs index 46b447d..cbdfd17 100644 --- a/src/LocalStack.Client/SessionStandalone.cs +++ b/src/LocalStack.Client/SessionStandalone.cs @@ -1,50 +1,39 @@ -using LocalStack.Client.Contracts; -using LocalStack.Client.Models; -using LocalStack.Client.Utils; +namespace LocalStack.Client; -namespace LocalStack.Client +public class SessionStandalone : ISessionStandalone { - public class SessionStandalone : ISessionStandalone + private ISessionOptions? _sessionOptions; + private IConfigOptions? _configOptions; + + private SessionStandalone() + { + } + + public ISessionStandalone WithSessionOptions(ISessionOptions sessionOptions) + { + _sessionOptions = sessionOptions; + + return this; + } + + public ISessionStandalone WithConfigurationOptions(IConfigOptions configOptions) + { + _configOptions = configOptions; + + return this; + } + + public ISession Create() + { + ISessionOptions sessionOptions = _sessionOptions ?? new SessionOptions(); + IConfig config = new Config(_configOptions ?? new ConfigOptions()); + ISessionReflection sessionReflection = new SessionReflection(); + + return new Session(sessionOptions, config, sessionReflection); + } + + public static ISessionStandalone Init() { - private string _awsAccessKey; - private string _awsAccessKeyId; - private string _awsSessionToken; - private string _localStackHost; - private string _regionName; - - private SessionStandalone() - { - } - - public ISessionStandalone WithSessionOptions(string awsAccessKeyId = null, string awsAccessKey = null, string awsSessionToken = null, string regionName = null) - { - _awsAccessKeyId = awsAccessKeyId; - _awsAccessKey = awsAccessKey; - _awsSessionToken = awsSessionToken; - _regionName = regionName; - - return this; - } - - public ISessionStandalone WithConfig(string localStackHost = null) - { - _localStackHost = localStackHost; - - return this; - } - - public ISession Create() - { - var sessionOptions = new SessionOptions(_awsAccessKeyId, _awsAccessKey, _awsSessionToken, _regionName); - var config = new Config(_localStackHost); - var sessionReflection = new SessionReflection(); - - return new Session(sessionOptions, config, sessionReflection); - } - - public static ISessionStandalone Init() - { - return new SessionStandalone(); - } + return new SessionStandalone(); } } \ No newline at end of file diff --git a/src/LocalStack.Client/Utils/SessionReflection.cs b/src/LocalStack.Client/Utils/SessionReflection.cs index cf4b0cb..7f5b748 100644 --- a/src/LocalStack.Client/Utils/SessionReflection.cs +++ b/src/LocalStack.Client/Utils/SessionReflection.cs @@ -1,71 +1,139 @@ -using Amazon.Runtime; -using Amazon.Runtime.Internal; +#pragma warning disable S3011 // We need to use reflection to access private fields for service metadata +namespace LocalStack.Client.Utils; -using LocalStack.Client.Contracts; +public class SessionReflection : ISessionReflection +{ + public IServiceMetadata ExtractServiceMetadata() where TClient : AmazonServiceClient + { + Type clientType = typeof(TClient); -using System; -using System.Linq; -using System.Reflection; + return ExtractServiceMetadata(clientType); + } -namespace LocalStack.Client.Utils -{ - public class SessionReflection : ISessionReflection +#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 + ) { - public IServiceMetadata ExtractServiceMetadata() where TClient : AmazonServiceClient + if (clientType == null) { - Type clientType = typeof(TClient); + throw new ArgumentNullException(nameof(clientType)); + } + + FieldInfo serviceMetadataField = clientType.GetField("serviceMetadata", BindingFlags.Static | BindingFlags.NonPublic) ?? + throw new InvalidOperationException($"Invalid service type {clientType}"); + + #pragma warning disable CS8600,CS8603 // Not possible to get null value from this private field + var serviceMetadata = (IServiceMetadata)serviceMetadataField.GetValue(null); + + return serviceMetadata; + } - FieldInfo serviceMetadataField = clientType.GetField("serviceMetadata", BindingFlags.Static | BindingFlags.NonPublic) ?? - throw new InvalidOperationException($"Invalid service type {typeof(TClient)}"); + public ClientConfig CreateClientConfig() where TClient : AmazonServiceClient + { + Type clientType = typeof(TClient); - var serviceMetadata = (IServiceMetadata) serviceMetadataField.GetValue(null); + return CreateClientConfig(clientType); + } - return serviceMetadata; +#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) + { + throw new ArgumentNullException(nameof(clientType)); } - public ClientConfig CreateClientConfig() where TClient : AmazonServiceClient + ConstructorInfo clientConstructorInfo = FindConstructorWithCredentialsAndClientConfig(clientType); + ParameterInfo clientConfigParam = clientConstructorInfo.GetParameters()[1]; + + return (ClientConfig)Activator.CreateInstance(clientConfigParam.ParameterType); + } + +#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) { - ConstructorInfo clientConstructorInfo = FindConstructorWithCredentialsAndClientConfig(typeof(TClient)); - ParameterInfo clientConfigParam = clientConstructorInfo.GetParameters()[1]; + throw new ArgumentNullException(nameof(amazonServiceClient)); + } + + PropertyInfo? regionEndpointProperty = amazonServiceClient.Config.GetType() + .GetProperty(nameof(amazonServiceClient.Config.RegionEndpoint), + BindingFlags.Public | BindingFlags.Instance); + regionEndpointProperty?.SetValue(amazonServiceClient.Config, RegionEndpoint.GetBySystemName(systemName)); + } - return (ClientConfig) Activator.CreateInstance(clientConfigParam.ParameterType); +#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) + { + throw new ArgumentNullException(nameof(clientConfig)); } - public bool SetForcePathStyle(ClientConfig clientConfig, bool value = true) + PropertyInfo? forcePathStyleProperty = clientConfig.GetType().GetProperty("ForcePathStyle", BindingFlags.Public | BindingFlags.Instance); + + if (forcePathStyleProperty == null) { - PropertyInfo forcePathStyleProperty = clientConfig.GetType().GetProperty("ForcePathStyle", BindingFlags.Public | BindingFlags.Instance); + return false; + } - if (forcePathStyleProperty == null) - { - return false; - } + forcePathStyleProperty.SetValue(clientConfig, value); - forcePathStyleProperty.SetValue(clientConfig, value); + return true; + } - return true; - } + private static ConstructorInfo FindConstructorWithCredentialsAndClientConfig(Type clientType) + { + return clientType.GetConstructors(BindingFlags.Instance | BindingFlags.Public) + .Single(info => + { + ParameterInfo[] parameterInfos = info.GetParameters(); - private static ConstructorInfo FindConstructorWithCredentialsAndClientConfig(Type clientType) - { - return clientType.GetConstructors(BindingFlags.Instance | BindingFlags.Public) - .Where(info => + if (parameterInfos.Length != 2) { - ParameterInfo[] parameterInfos = info.GetParameters(); - - if (parameterInfos.Length != 2) - { - return false; - } - - ParameterInfo credentialsParameter = parameterInfos[0]; - ParameterInfo clientConfigParameter = parameterInfos[1]; - - return credentialsParameter.Name == "credentials" && - credentialsParameter.ParameterType == typeof(AWSCredentials) && - clientConfigParameter.Name == "clientConfig" && - clientConfigParameter.ParameterType.IsSubclassOf(typeof(ClientConfig)); - }) - .Single(); - } + return false; + } + + ParameterInfo credentialsParameter = parameterInfos[0]; + ParameterInfo clientConfigParameter = parameterInfos[1]; + + return credentialsParameter.Name == "credentials" && + credentialsParameter.ParameterType == typeof(AWSCredentials) && + clientConfigParameter.Name == "clientConfig" && + clientConfigParameter.ParameterType.IsSubclassOf(typeof(ClientConfig)); + }); } } \ No newline at end of file diff --git a/src/LocalStack.sln b/src/LocalStack.sln deleted file mode 100644 index ac02a9c..0000000 --- a/src/LocalStack.sln +++ /dev/null @@ -1,65 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.452 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client", "LocalStack.Client\LocalStack.Client.csproj", "{22C080D7-929C-44F1-909C-831EF9D2810F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sandboxes", "Sandboxes", "{962A750E-8FE2-461F-B3FC-2B401309B5FD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3F0F4BAA-02EF-4008-9CF8-E73AA92D4664}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Sandbox", "..\tests\sandboxes\LocalStack.Client.Sandbox\LocalStack.Client.Sandbox.csproj", "{E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Tests", "..\tests\LocalStack.Client.Tests\LocalStack.Client.Tests.csproj", "{9FC6CABE-ED38-4048-B511-69D76870ABF8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Integration.Tests", "..\tests\LocalStack.Client.Integration.Tests\LocalStack.Client.Integration.Tests.csproj", "{691A4094-2074-474A-81A3-E33B728AE54E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalStack.Client.Sandbox.DependencyInjection", "..\tests\sandboxes\LocalStack.Client.Sandbox.DependencyInjection\LocalStack.Client.Sandbox.DependencyInjection.csproj", "{4E90D3D1-D570-4205-9C6E-B917B5508912}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{06034ACF-97AD-4266-8E46-42B1804C89B6}" - ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {22C080D7-929C-44F1-909C-831EF9D2810F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22C080D7-929C-44F1-909C-831EF9D2810F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22C080D7-929C-44F1-909C-831EF9D2810F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22C080D7-929C-44F1-909C-831EF9D2810F}.Release|Any CPU.Build.0 = Release|Any CPU - {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D}.Release|Any CPU.Build.0 = Release|Any CPU - {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9FC6CABE-ED38-4048-B511-69D76870ABF8}.Release|Any CPU.Build.0 = Release|Any CPU - {691A4094-2074-474A-81A3-E33B728AE54E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {691A4094-2074-474A-81A3-E33B728AE54E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {691A4094-2074-474A-81A3-E33B728AE54E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {691A4094-2074-474A-81A3-E33B728AE54E}.Release|Any CPU.Build.0 = Release|Any CPU - {4E90D3D1-D570-4205-9C6E-B917B5508912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E90D3D1-D570-4205-9C6E-B917B5508912}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E90D3D1-D570-4205-9C6E-B917B5508912}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E90D3D1-D570-4205-9C6E-B917B5508912}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {962A750E-8FE2-461F-B3FC-2B401309B5FD} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} - {E7E16B66-EE23-4B49-89C5-4FF64F2ED95D} = {962A750E-8FE2-461F-B3FC-2B401309B5FD} - {9FC6CABE-ED38-4048-B511-69D76870ABF8} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} - {691A4094-2074-474A-81A3-E33B728AE54E} = {3F0F4BAA-02EF-4008-9CF8-E73AA92D4664} - {4E90D3D1-D570-4205-9C6E-B917B5508912} = {962A750E-8FE2-461F-B3FC-2B401309B5FD} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E4925255-67AA-4095-816B-CC10A5490E71} - EndGlobalSection -EndGlobal diff --git a/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs b/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs new file mode 100644 index 0000000..936e416 --- /dev/null +++ b/tests/LocalStack.Client.Extensions.Tests/AwsClientFactoryWrapperTests.cs @@ -0,0 +1,93 @@ +namespace LocalStack.Client.Extensions.Tests; + +public class AwsClientFactoryWrapperTests +{ + private readonly AwsClientFactoryWrapper _awsClientFactoryWrapper; + private readonly Mock _mockServiceProvider; + private readonly AWSOptions _awsOptions; + + public AwsClientFactoryWrapperTests() + { + _awsClientFactoryWrapper = new AwsClientFactoryWrapper(); + _mockServiceProvider = new Mock(); + _awsOptions = new AWSOptions(); + } + + [Fact] + public void CreateServiceClient_Should_Throw_LocalStackClientConfigurationException_When_ClientFactoryType_Is_Null() + { + 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"); + + 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() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new KeyValuePair[] { new("LocalStack:UseLocalStack", "false") }); + IConfigurationRoot configurationRoot = configurationBuilder.Build(); + + // Explicit AWS SDK configuration + var awsOptions = new AWSOptions + { + Credentials = new BasicAWSCredentials("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"), Region = RegionEndpoint.USWest2 + }; + + ServiceCollection serviceCollection = new(); + serviceCollection.AddLocalStack(configurationRoot); + serviceCollection.AddDefaultAWSOptions(awsOptions); + serviceCollection.AddAWSServiceLocalStack(); + ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + + var requiredService = serviceProvider.GetRequiredService(); + + Assert.NotNull(requiredService); + } + + private static void SetPrivateReadonlyField(FieldInfo field, string value) + { + var method = new DynamicMethod("SetReadOnlyField", null, new[] { typeof(object), typeof(object) }, typeof(AwsClientFactoryWrapper), true); + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, field.DeclaringType!); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, field); + il.Emit(OpCodes.Ret); + + method.Invoke(null, new object[] { null!, value }); + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Extensions.Tests/Extensions/ObjectExtensions.cs b/tests/LocalStack.Client.Extensions.Tests/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..edb9db1 --- /dev/null +++ b/tests/LocalStack.Client.Extensions.Tests/Extensions/ObjectExtensions.cs @@ -0,0 +1,14 @@ +#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 +{ + public static bool DeepEquals(this object obj1, object obj2) + { + string obj1Ser = JsonSerializer.Serialize(obj1); + string obj2Ser = JsonSerializer.Serialize(obj2); + + return string.Equals(obj1Ser, obj2Ser, StringComparison.Ordinal); + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Extensions.Tests/GlobalUsings.cs b/tests/LocalStack.Client.Extensions.Tests/GlobalUsings.cs new file mode 100644 index 0000000..43a77c9 --- /dev/null +++ b/tests/LocalStack.Client.Extensions.Tests/GlobalUsings.cs @@ -0,0 +1,28 @@ +global using System; +global using System.Collections.Generic; +global using System.Globalization; +global using System.Reflection; +global using System.Reflection.Emit; +global using System.Text.Json; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Options; + +global using Amazon; +global using Amazon.Extensions.NETCore.Setup; +global using Amazon.Runtime; +global using Amazon.S3; + +global using LocalStack.Client.Contracts; +global using LocalStack.Client.Extensions.Contracts; +global using LocalStack.Client.Extensions.Exceptions; +global using LocalStack.Client.Extensions.Tests.Extensions; +global using LocalStack.Client.Options; +global using LocalStack.Client.Utils; +global using LocalStack.Client.Models; +global using LocalStack.Tests.Common.Mocks.MockServiceClients; + +global using Moq; +global using Xunit; diff --git a/tests/LocalStack.Client.Extensions.Tests/LocalStack.Client.Extensions.Tests.csproj b/tests/LocalStack.Client.Extensions.Tests/LocalStack.Client.Extensions.Tests.csproj new file mode 100644 index 0000000..c6a3b13 --- /dev/null +++ b/tests/LocalStack.Client.Extensions.Tests/LocalStack.Client.Extensions.Tests.csproj @@ -0,0 +1,35 @@ + + + + net8.0;net9.0 + $(NoWarn);CA1707;MA0006 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/LocalStack.Client.Extensions.Tests/ServiceCollectionExtensionsTests.cs b/tests/LocalStack.Client.Extensions.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..23a9934 --- /dev/null +++ b/tests/LocalStack.Client.Extensions.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,339 @@ +namespace LocalStack.Client.Extensions.Tests; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddLocalStack_Should_Configure_LocalStackOptions_If_There_Is_Not_LocalStack_Section() + { + IConfiguration configuration = new ConfigurationBuilder().Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + LocalStackOptions? localStackOptions = provider.GetRequiredService>()?.Value; + + Assert.NotNull(localStackOptions); + Assert.False(localStackOptions.UseLocalStack); + Assert.True(new SessionOptions().DeepEquals(localStackOptions.Session)); + Assert.True(new ConfigOptions().DeepEquals(localStackOptions.Config)); + } + + [Fact] + public void AddLocalStack_Should_Configure_LocalStackOptions_By_LocalStack_Section() + { + var configurationValue = new Dictionary(StringComparer.Ordinal) { { "LocalStack:UseLocalStack", "true" } }; + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValue!).Build(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + LocalStackOptions? localStackOptions = provider.GetRequiredService>()?.Value; + + Assert.NotNull(localStackOptions); + Assert.True(localStackOptions.UseLocalStack); + Assert.True(new SessionOptions().DeepEquals(localStackOptions.Session)); + Assert.True(new ConfigOptions().DeepEquals(localStackOptions.Config)); + } + + [Fact] + public void AddLocalStack_Should_Configure_SessionOptions_If_There_Is_Not_Session_Section() + { + IConfiguration configuration = new ConfigurationBuilder().Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + SessionOptions? sessionOptions = provider.GetRequiredService>()?.Value; + + Assert.NotNull(sessionOptions); + Assert.True(new SessionOptions().DeepEquals(sessionOptions)); + } + + [Fact] + public void AddLocalStack_Should_Configure_SessionOptions_By_Session_Section() + { + const string awsAccessKeyId = "myawsakid"; + const string awsAccessKey = "myawsak"; + const string awsSessionToken = "myawsst"; + const string regionName = "mars-central-1"; + + var configurationValue = new Dictionary(StringComparer.Ordinal) + { + { "LocalStack:Session:AwsAccessKeyId", awsAccessKeyId }, + { "LocalStack:Session:AwsAccessKey", awsAccessKey }, + { "LocalStack:Session:AwsSessionToken", awsSessionToken }, + { "LocalStack:Session:RegionName", regionName }, + }; + + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValue!).Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + SessionOptions? sessionOptions = provider.GetRequiredService>()?.Value; + + Assert.NotNull(sessionOptions); + Assert.Equal(awsAccessKeyId, sessionOptions.AwsAccessKeyId); + Assert.Equal(awsAccessKey, sessionOptions.AwsAccessKey); + Assert.Equal(awsSessionToken, sessionOptions.AwsSessionToken); + Assert.Equal(regionName, sessionOptions.RegionName); + } + + [Fact] + public void AddLocalStack_Should_Configure_ConfigOptions_If_There_Is_Not_Session_Section() + { + IConfiguration configuration = new ConfigurationBuilder().Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + ConfigOptions? configOptions = provider.GetRequiredService>()?.Value; + + Assert.NotNull(configOptions); + Assert.True(new ConfigOptions().DeepEquals(configOptions)); + } + + [Fact] + public void AddLocalStack_Should_Configure_ConfigOptions_By_Session_Section() + { + const string localStackHost = "myhost"; + const bool useSsl = true; + const bool useLegacyPorts = true; + const int edgePort = 1245; + + var configurationValue = new Dictionary(StringComparer.Ordinal) + { + { "LocalStack:Config:LocalStackHost", localStackHost }, + { "LocalStack:Config:UseSsl", useSsl.ToString() }, + { "LocalStack:Config:UseLegacyPorts", useLegacyPorts.ToString() }, + { "LocalStack:Config:EdgePort", edgePort.ToString(CultureInfo.InvariantCulture) }, + }; + + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValue!).Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + ConfigOptions? configOptions = provider.GetRequiredService>()?.Value; + + Assert.NotNull(configOptions); + Assert.Equal(localStackHost, configOptions.LocalStackHost); + Assert.Equal(useSsl, configOptions.UseSsl); + Assert.Equal(useLegacyPorts, configOptions.UseLegacyPorts); + Assert.Equal(edgePort, configOptions.EdgePort); + } + + [Fact] + public void AddLocalStackServices_Should_Add_IConfig_To_Container_As_Config() + { + IConfiguration configuration = new ConfigurationBuilder().Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + var config = provider.GetRequiredService(); + + Assert.NotNull(config); + Assert.IsType(config); + } + + [Fact] + public void AddLocalStackServices_Should_Add_ISession_To_Container_As_Session() + { + IConfiguration configuration = new ConfigurationBuilder().Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + var session = provider.GetRequiredService(); + + Assert.NotNull(session); + Assert.IsType(session); + } + + [Fact] + public void AddLocalStackServices_Should_Add_IAwsClientFactoryWrapper_To_Container_As_AwsClientFactoryWrapper() + { + IConfiguration configuration = new ConfigurationBuilder().Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + var awsClientFactoryWrapper = provider.GetRequiredService(); + Type? factoryType = typeof(IAwsClientFactoryWrapper).Assembly.GetType("LocalStack.Client.Extensions.AwsClientFactoryWrapper"); + + Assert.NotNull(factoryType); + Assert.NotNull(awsClientFactoryWrapper); + Assert.IsType(factoryType, awsClientFactoryWrapper); + } + + [Fact] + public void AddLocalStackServices_Should_Add_ISessionReflection_To_Container_As_SessionReflection() + { + IConfiguration configuration = new ConfigurationBuilder().Build(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration); + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + var sessionReflection = provider.GetRequiredService(); + + Assert.NotNull(sessionReflection); + Assert.IsType(sessionReflection); + } + + [Theory, InlineData(true, "eu-central-1"), InlineData(true, "us-west-1"), InlineData(true, "af-south-1"), InlineData(true, "ap-southeast-1"), + InlineData(true, "ca-central-1"), InlineData(true, "eu-west-2"), InlineData(true, "sa-east-1"), InlineData(false, "eu-central-1"), InlineData(false, "us-west-1"), + InlineData(false, "af-south-1"), InlineData(false, "ap-southeast-1"), InlineData(false, "ca-central-1"), InlineData(false, "eu-west-2"), + InlineData(false, "sa-east-1")] + public void GetRequiredService_Should_Return_AmazonService_That_Configured_For_LocalStack_If_UseLocalStack_Is_True( + bool useAlternateNameAddServiceMethod, string systemName) + { + var configurationValue = new Dictionary(StringComparer.Ordinal) { { "LocalStack:UseLocalStack", "true" }, { "LocalStack:Session:RegionName", systemName } }; + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValue!).Build(); + + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var configOptions = new ConfigOptions(); + + var mockConfig = new Mock(MockBehavior.Strict); + IConfig mockConfigObject = mockConfig.Object; + + mockConfig.Setup(config => config.GetAwsServiceEndpoint(It.Is(s => s == mockServiceMetadata.ServiceId))).Returns(() => mockAwsServiceEndpoint); + mockConfig.Setup(config => config.GetConfigOptions()).Returns(() => configOptions); + + IServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection = serviceCollection.AddLocalStack(configuration).Replace(ServiceDescriptor.Singleton(_ => mockConfigObject)); + + if (!useAlternateNameAddServiceMethod) + { + serviceCollection.AddAwsService(); + } + else + { + serviceCollection.AddAWSServiceLocalStack(); + } + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + var mockAmazonService = provider.GetRequiredService(); + + IClientConfig clientConfig = mockAmazonService.Config; + + Assert.Equal(configOptions.UseSsl, !clientConfig.UseHttp); + Assert.Equal(mockAwsServiceEndpoint.Host, clientConfig.ProxyHost); + Assert.Equal(mockAwsServiceEndpoint.Port, clientConfig.ProxyPort); + Assert.Equal(RegionEndpoint.GetBySystemName(systemName), clientConfig.RegionEndpoint); + } + + [Theory, InlineData(false, false), InlineData(false, true), InlineData(true, true), InlineData(true, false)] + public void + GetRequiredService_Should_Return_AmazonService_That_Configured_For_LocalStack_If_UseLocalStack_Is_True_And_Should_Configure_ServiceUrl_Or_RegionEndpoint_By_Given_UseServiceUrl_Parameter( + bool useAlternateNameAddServiceMethod, bool useServiceUrl) + { + var configurationValue = new Dictionary(StringComparer.Ordinal) { { "LocalStack:UseLocalStack", "true" } }; + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValue!).Build(); + + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var configOptions = new ConfigOptions(); + + var mockConfig = new Mock(MockBehavior.Strict); + IConfig mockConfigObject = mockConfig.Object; + + mockConfig.Setup(config => config.GetAwsServiceEndpoint(It.Is(s => s == mockServiceMetadata.ServiceId))).Returns(() => mockAwsServiceEndpoint); + mockConfig.Setup(config => config.GetConfigOptions()).Returns(() => configOptions); + + IServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection = serviceCollection.AddLocalStack(configuration).Replace(ServiceDescriptor.Singleton(_ => mockConfigObject)); + + if (!useAlternateNameAddServiceMethod) + { + serviceCollection.AddAwsService(useServiceUrl: useServiceUrl); + } + else + { + serviceCollection.AddAWSServiceLocalStack(useServiceUrl: useServiceUrl); + } + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + var mockAmazonService = provider.GetRequiredService(); + + IClientConfig clientConfig = mockAmazonService.Config; + + Assert.Equal(configOptions.UseSsl, !clientConfig.UseHttp); + Assert.Equal(mockAwsServiceEndpoint.Host, clientConfig.ProxyHost); + Assert.Equal(mockAwsServiceEndpoint.Port, clientConfig.ProxyPort); + + if (useServiceUrl) + { + Assert.Null(clientConfig.RegionEndpoint); + Assert.NotNull(clientConfig.ServiceURL); + Assert.Equal(mockAwsServiceEndpoint.ServiceUrl.AbsoluteUri, clientConfig.ServiceURL); + } + else + { + Assert.Null(clientConfig.ServiceURL); + Assert.NotNull(clientConfig.RegionEndpoint); + Assert.Equal(RegionEndpoint.GetBySystemName(Constants.RegionName), clientConfig.RegionEndpoint); + } + } + + [Theory, InlineData(false, false, false), InlineData(false, false, true), InlineData(true, false, false), InlineData(true, false, true), + InlineData(false, true, false), InlineData(false, true, true), InlineData(true, true, false), InlineData(true, true, true)] + public void GetRequiredService_Should_Use_Suitable_ClientFactory_To_Create_AwsService_By_UseLocalStack_Value( + bool useLocalStack, bool useAlternateNameAddServiceMethod, bool useServiceUrl) + { + int sessionInvolved = useLocalStack ? 1 : 0; + int awsClientFactoryInvolved = useLocalStack ? 0 : 1; + + var configurationValue = new Dictionary(StringComparer.Ordinal) { { "LocalStack:UseLocalStack", useLocalStack.ToString() } }; + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValue!).Build(); + + var mockSession = new Mock(MockBehavior.Strict); + var mockClientFactory = new Mock(MockBehavior.Strict); + + ISession mockSessionObject = mockSession.Object; + IAwsClientFactoryWrapper mockAwsClientFactoryWrapper = mockClientFactory.Object; + + IServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection.AddLocalStack(configuration) + .Replace(ServiceDescriptor.Singleton(_ => mockSessionObject)) + .Replace(ServiceDescriptor.Singleton(_ => mockAwsClientFactoryWrapper)); + + if (!useAlternateNameAddServiceMethod) + { + serviceCollection.AddAwsService(useServiceUrl: useServiceUrl); + } + else + { + serviceCollection.AddAWSServiceLocalStack(useServiceUrl: useServiceUrl); + } + + ServiceProvider provider = serviceCollection.BuildServiceProvider(); + + mockSession.Setup(session => session.CreateClientByInterface(It.IsAny())) + .Returns(() => new MockAmazonServiceClient("tsada", "sadasdas", "sadasda", MockClientConfig.CreateDefaultMockClientConfig())); + mockClientFactory.Setup(wrapper => wrapper.CreateServiceClient(It.IsAny(), It.IsAny())) + .Returns(() => new MockAmazonServiceClient("tsada", "sadasdas", "sadasda", MockClientConfig.CreateDefaultMockClientConfig())); + + var mockAmazonService = provider.GetRequiredService(); + + Assert.NotNull(mockAmazonService); + + mockClientFactory.Verify(wrapper => wrapper.CreateServiceClient(It.IsAny(), It.IsAny()), + Times.Exactly(awsClientFactoryInvolved)); + mockSession.Verify(session => session.CreateClientByInterface(It.Is(b => b == useServiceUrl)), Times.Exactly(sessionInvolved)); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..94888ef --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackCollections.cs @@ -0,0 +1,9 @@ +#pragma warning disable MA0048 // File name must match type name - disabled because of readability + +namespace LocalStack.Client.Functional.Tests.Fixtures; + +[CollectionDefinition(nameof(LocalStackCollectionV37))] +public class LocalStackCollectionV37 : ICollectionFixture, ICollectionFixture; + +[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 new file mode 100644 index 0000000..56959a4 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Fixtures/LocalStackFixtures.cs @@ -0,0 +1,44 @@ +#pragma warning disable MA0048 // File name must match type name - disabled because of readability + +namespace LocalStack.Client.Functional.Tests.Fixtures; + +public abstract class LocalStackFixtureBase : IAsyncLifetime, ILocalStackFixture +{ + protected LocalStackFixtureBase(LocalStackBuilder localStackBuilder) + { + ArgumentNullException.ThrowIfNull(localStackBuilder); + + LocalStackContainer = localStackBuilder.Build(); + } + + public LocalStackContainer LocalStackContainer { get; } + + public async Task InitializeAsync() + { + await LocalStackContainer.StartAsync(); + } + + public async Task DisposeAsync() + { + await LocalStackContainer.StopAsync(); + } +} + +public sealed class LocalStackFixtureV37 : LocalStackFixtureBase +{ + public LocalStackFixtureV37() : base(TestContainers.LocalStackBuilder(TestConstants.LocalStackV37)) + { + } +} + +public sealed class LocalStackFixtureV46 : LocalStackFixtureBase +{ + public LocalStackFixtureV46() : base(TestContainers.LocalStackBuilder(TestConstants.LocalStackV46)) + { + } +} + +public interface ILocalStackFixture +{ + LocalStackContainer LocalStackContainer { get; } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Fixtures/TestFixture.cs b/tests/LocalStack.Client.Functional.Tests/Fixtures/TestFixture.cs new file mode 100644 index 0000000..88e646d --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Fixtures/TestFixture.cs @@ -0,0 +1,29 @@ +#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 +{ + public ConfigurationBuilder CreateConfigureAppConfiguration(string configFile, ushort hostPort = 4566) + { + var builder = new ConfigurationBuilder(); + + builder.SetBasePath(Directory.GetCurrentDirectory()); + builder.AddJsonFile("appsettings.json", optional: true); + builder.AddJsonFile(configFile, optional: true); + var keyValuePairs = new Dictionary(StringComparer.Ordinal) { { "LocalStack:Config:EdgePort", hostPort.ToString(CultureInfo.InvariantCulture) }, }; + builder.AddInMemoryCollection(keyValuePairs!); + builder.AddEnvironmentVariables(); + + return builder; + } + + public IServiceCollection CreateServiceCollection(IConfiguration configuration) + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddLocalStack(configuration); + + return serviceCollection; + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/GlobalUsings.cs b/tests/LocalStack.Client.Functional.Tests/GlobalUsings.cs new file mode 100644 index 0000000..337ce58 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/GlobalUsings.cs @@ -0,0 +1,69 @@ +global using System; +global using System.Collections.Generic; +global using System.Diagnostics.CodeAnalysis; +global using System.Dynamic; +global using System.Globalization; +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; +global using Amazon.DynamoDBv2.Model; +global using Amazon.S3; +global using Amazon.S3.Model; +global using Amazon.S3.Transfer; +global using Amazon.SQS; +global using Amazon.SQS.Model; +global using Amazon.SimpleNotificationService; +global using Amazon.SimpleNotificationService.Model; + +global using AutoFixture; + +global using DotNet.Testcontainers.Builders; + +global using Newtonsoft.Json; +global using Newtonsoft.Json.Converters; + +global using LocalStack.Client.Extensions; +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; + +#pragma warning disable MA0048 // File name must match type name +#if NETCOREAPP +namespace System.Runtime.CompilerServices +{ + using System.ComponentModel; + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} +#endif \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/LocalStack.Client.Functional.Tests.csproj b/tests/LocalStack.Client.Functional.Tests/LocalStack.Client.Functional.Tests.csproj new file mode 100644 index 0000000..171edcb --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/LocalStack.Client.Functional.Tests.csproj @@ -0,0 +1,78 @@ + + + + net8.0;net9.0 + latest + $(NoWarn);CA1707;MA0006;MA0004;CA1711;CA2007;MA0132;CA1848;CA2254;S4144;CA1515 + + + + + + + + + + PreserveNewest + + + PreserveNewest + appsettings.json + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + Always + + + + + NETCOREAPP + + + \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/SampleData.txt b/tests/LocalStack.Client.Functional.Tests/SampleData.txt new file mode 100644 index 0000000..b3cdee7 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/SampleData.txt @@ -0,0 +1,14 @@ +The standard Lorem Ipsum passage, used since the 1500s +"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + +Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC +"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?" + +1914 translation by H. Rackham +"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?" + +Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC +"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat." + +1914 translation by H. Rackham +"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains." \ 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 new file mode 100644 index 0000000..0f45c02 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/BaseScenario.cs @@ -0,0 +1,33 @@ +namespace LocalStack.Client.Functional.Tests.Scenarios; + +public abstract class BaseScenario +{ + protected BaseScenario(TestFixture testFixture, ILocalStackFixture localStackFixture, string configFile = TestConstants.LocalStackConfig, bool useServiceUrl = false) + { + ArgumentNullException.ThrowIfNull(testFixture); + ArgumentNullException.ThrowIfNull(localStackFixture); + + ushort mappedPublicPort = localStackFixture.LocalStackContainer.GetMappedPublicPort(4566); + ConfigurationBuilder configurationBuilder = testFixture.CreateConfigureAppConfiguration(configFile, mappedPublicPort); + Configuration = configurationBuilder.Build(); + + IServiceCollection serviceCollection = testFixture.CreateServiceCollection(Configuration); + + serviceCollection.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; } +} \ No newline at end of file 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 new file mode 100644 index 0000000..79261b5 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/BaseDynamoDbScenario.cs @@ -0,0 +1,157 @@ +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace LocalStack.Client.Functional.Tests.Scenarios.DynamoDb; + +public abstract class BaseDynamoDbScenario : BaseScenario +{ + protected const string TestTableName = "Movies"; + + protected BaseDynamoDbScenario(TestFixture testFixture, ILocalStackFixture localStackFixture, string configFile = TestConstants.LocalStackConfig, + bool useServiceUrl = false) : base(testFixture, localStackFixture, configFile, useServiceUrl) + { + DynamoDb = ServiceProvider.GetRequiredService(); + DynamoDbContext = new DynamoDBContextBuilder().WithDynamoDBClient(() => DynamoDb).Build(); + } + + protected IAmazonDynamoDB DynamoDb { get; private set; } + + protected IDynamoDBContext DynamoDbContext { get; private set; } + + [Fact] + public virtual async Task DynamoDbService_Should_Create_A_DynamoDb_Table_Async() + { + var tableName = Guid.NewGuid().ToString(); + CreateTableResponse createTableResponse = await CreateTestTableAsync(tableName); + Assert.Equal(HttpStatusCode.OK, createTableResponse.HttpStatusCode); + } + + [Fact] + public virtual async Task DynamoDbService_Should_Delete_A_DynamoDb_Table_Async() + { + var tableName = Guid.NewGuid().ToString(); + await CreateTestTableAsync(tableName); + + DeleteTableResponse deleteTableResponse = await DeleteTestTableAsync(tableName); + Assert.Equal(HttpStatusCode.OK, deleteTableResponse.HttpStatusCode); + } + + [Fact] + public virtual async Task DynamoDbService_Should_Add_A_Record_To_A_DynamoDb_Table_Async() + { + var tableName = Guid.NewGuid().ToString(); + + // 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); + + // 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])); + } + + [Fact] + public virtual async Task DynamoDbService_Should_List_Records_In_A_DynamoDb_Table_Async() + { + var tableName = Guid.NewGuid().ToString(); + const int recordCount = 5; + + // Fix: Use GetTargetTableConfig instead of DynamoDBOperationConfig + var getTargetTableConfig = new GetTargetTableConfig() { OverrideTableName = tableName }; + await CreateTestTableAsync(tableName); + + // 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; + }),]; + + foreach (Document document in documents) + { + await targetTable.PutItemAsync(document); + } + + // 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); + Assert.Equal(recordCount, movieEntities.Count); + Assert.All(returnedMovieEntities, movieEntity => + { + MovieEntity entity = movieEntities.First(e => e.MovieId == movieEntity.MovieId); + + Assert.True(movieEntity.DeepEquals(entity)); + }); + } + + protected Task CreateTestTableAsync(string? tableName = null) + { + var postTableCreateRequest = new CreateTableRequest + { + AttributeDefinitions = + [ + 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 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 KeySchemaElement { AttributeName = nameof(MovieEntity.MovieId), KeyType = KeyType.HASH }], + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 5, WriteCapacityUnits = 5 }, + }, + + ], + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 5, WriteCapacityUnits = 6 }, + }; + + return DynamoDb.CreateTableAsync(postTableCreateRequest); + } + + protected Task DeleteTestTableAsync(string? tableName = null) + { + var deleteTableRequest = new DeleteTableRequest(tableName ?? TestTableName); + + return DynamoDb.DeleteTableAsync(deleteTableRequest); + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/DynamoDbScenario.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/DynamoDbScenario.cs new file mode 100644 index 0000000..15a3e8c --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/DynamoDbScenario.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.DynamoDb; + +[Collection(nameof(LocalStackCollectionV37))] +public sealed class DynamoDbScenarioV37 : BaseDynamoDbScenario +{ + public DynamoDbScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) + { + } +} + +[Collection(nameof(LocalStackCollectionV46))] +public sealed class DynamoDbScenarioV46 : BaseDynamoDbScenario +{ + 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 new file mode 100644 index 0000000..1e5d4be --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/DynamoDb/Entities/MovieEntity.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + +namespace LocalStack.Client.Functional.Tests.Scenarios.DynamoDb.Entities; + +public class MovieEntity +{ + [DynamoDBHashKey] + 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 new file mode 100644 index 0000000..11af890 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/BaseRealLife.cs @@ -0,0 +1,96 @@ +using MessageAttributeValue = Amazon.SimpleNotificationService.Model.MessageAttributeValue; + +namespace LocalStack.Client.Functional.Tests.Scenarios.RealLife; + +public abstract class BaseRealLife : BaseScenario +{ + protected BaseRealLife(TestFixture testFixture, ILocalStackFixture localStackFixture, string configFile = TestConstants.LocalStackConfig, bool useServiceUrl = false) + : base(testFixture, localStackFixture, configFile, useServiceUrl) + { + AmazonSimpleNotificationService = ServiceProvider.GetRequiredService(); + AmazonSqs = ServiceProvider.GetRequiredService(); + } + + protected IAmazonSimpleNotificationService AmazonSimpleNotificationService { get; set; } + + protected IAmazonSQS AmazonSqs { get; set; } + + [Fact, SuppressMessage("Test", "MA0051:Method is too long", Justification = "Test method")] + public virtual async 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() + { + var topicName = Guid.NewGuid().ToString(); + var queueName = Guid.NewGuid().ToString(); + var jobCreatedEvent = new JobCreatedEvent(423565221, 191, 125522, "Painting Service"); + + var createTopicRequest = new CreateTopicRequest(topicName); + CreateTopicResponse createTopicResponse = await AmazonSimpleNotificationService.CreateTopicAsync(createTopicRequest); + + Assert.Equal(HttpStatusCode.OK, createTopicResponse.HttpStatusCode); + + var createQueueRequest = new CreateQueueRequest(queueName); + 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); + + 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); + + Assert.Equal(HttpStatusCode.OK, subscribeResponse.HttpStatusCode); + + string serializedObject = JsonConvert.SerializeObject(jobCreatedEvent); + var messageAttributes = new Dictionary(StringComparer.Ordinal) + { + { nameof(jobCreatedEvent.EventName), new MessageAttributeValue { DataType = "String", StringValue = jobCreatedEvent.EventName } }, + }; + + var publishRequest = new PublishRequest + { + Message = serializedObject, TopicArn = createTopicResponse.TopicArn, Subject = jobCreatedEvent.EventName, MessageAttributes = messageAttributes + }; + + PublishResponse publishResponse = await AmazonSimpleNotificationService.PublishAsync(publishRequest); + + Assert.Equal(HttpStatusCode.OK, publishResponse.HttpStatusCode); + + var receiveMessageRequest = new ReceiveMessageRequest(createQueueResponse.QueueUrl); + ReceiveMessageResponse receiveMessageResponse = await AmazonSqs.ReceiveMessageAsync(receiveMessageRequest); + + Assert.Equal(HttpStatusCode.OK, receiveMessageResponse.HttpStatusCode); + + if ((receiveMessageResponse.Messages?.Count ?? 0) == 0) + { + await Task.Delay(2000); + receiveMessageResponse = await AmazonSqs.ReceiveMessageAsync(receiveMessageRequest); + + Assert.Equal(HttpStatusCode.OK, receiveMessageResponse.HttpStatusCode); + } + + Assert.NotNull(receiveMessageResponse.Messages); + Assert.NotEmpty(receiveMessageResponse.Messages!); + Assert.Single(receiveMessageResponse.Messages); + + dynamic? deserializedMessage = JsonConvert.DeserializeObject(receiveMessageResponse.Messages[0].Body, new ExpandoObjectConverter()); + + Assert.NotNull(deserializedMessage); + Assert.NotNull(deserializedMessage!.MessageId); + Assert.Equal(publishResponse.MessageId, (string)deserializedMessage.MessageId); + + JobCreatedEvent sqsJobCreatedEvent = JsonConvert.DeserializeObject(deserializedMessage.Message); + + Assert.NotNull(sqsJobCreatedEvent); + Assert.Equal(jobCreatedEvent.EventName, sqsJobCreatedEvent.EventName); + Assert.Equal(jobCreatedEvent.Description, sqsJobCreatedEvent.Description); + Assert.Equal(jobCreatedEvent.JobId, sqsJobCreatedEvent.JobId); + Assert.Equal(jobCreatedEvent.ServiceId, sqsJobCreatedEvent.ServiceId); + Assert.Equal(jobCreatedEvent.UserId, sqsJobCreatedEvent.UserId); + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/SnsToSqsScenarios.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/SnsToSqsScenarios.cs new file mode 100644 index 0000000..78ea378 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/RealLife/SnsToSqsScenarios.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.RealLife; + +[Collection(nameof(LocalStackCollectionV37))] +public sealed class SnsToSqsScenarioV37 : BaseRealLife +{ + public SnsToSqsScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) + { + } +} + +[Collection(nameof(LocalStackCollectionV46))] +public sealed class SnsToSqsScenarioV46 : BaseRealLife +{ + public SnsToSqsScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) + { + } +} \ 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 new file mode 100644 index 0000000..bc60c53 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/BaseS3Scenario.cs @@ -0,0 +1,106 @@ +namespace LocalStack.Client.Functional.Tests.Scenarios.S3; + +public abstract class BaseS3Scenario : BaseScenario +{ + protected const string BucketName = "test-bucket-3"; + private const string FilePath = "SampleData.txt"; + protected const string Key = "SampleData.txt"; + + protected BaseS3Scenario(TestFixture testFixture, ILocalStackFixture localStackFixture, string configFile = TestConstants.LocalStackConfig, + bool useServiceUrl = false) : base(testFixture, localStackFixture, configFile, useServiceUrl) + { + AmazonS3 = ServiceProvider.GetRequiredService(); + } + + protected IAmazonS3 AmazonS3 { get; private set; } + + [Fact] + public async Task S3Service_Should_Create_A_Bucket_Async() + { + var bucketName = Guid.NewGuid().ToString(); + PutBucketResponse putBucketResponse = await CreateTestBucketAsync(bucketName); + + Assert.Equal(HttpStatusCode.OK, putBucketResponse.HttpStatusCode); + } + + [Fact] + public async Task S3Service_Should_Delete_A_Bucket_Async() + { + var bucketName = Guid.NewGuid().ToString(); + PutBucketResponse putBucketResponse = await CreateTestBucketAsync(bucketName); + + Assert.Equal(HttpStatusCode.OK, putBucketResponse.HttpStatusCode); + + DeleteBucketResponse deleteBucketResponse = await DeleteTestBucketAsync(bucketName); + Assert.Equal(HttpStatusCode.NoContent, deleteBucketResponse.HttpStatusCode); + } + + [Fact] + public async Task S3Service_Should_Upload_A_File_To_A_Bucket_Async() + { + var bucketName = Guid.NewGuid().ToString(); + await CreateTestBucketAsync(bucketName); + await UploadTestFileAsync(key: Key, bucketName: bucketName); + + GetObjectResponse getObjectResponse = await AmazonS3.GetObjectAsync(bucketName, Key); + + Assert.Equal(HttpStatusCode.OK, getObjectResponse.HttpStatusCode); + } + + [Fact] + public async Task S3Service_Should_Delete_A_File_To_A_Bucket_Async() + { + var bucketName = Guid.NewGuid().ToString(); + await CreateTestBucketAsync(bucketName); + await UploadTestFileAsync(key: Key, bucketName: bucketName); + + DeleteObjectResponse deleteObjectResponse = await AmazonS3.DeleteObjectAsync(bucketName, Key); + + Assert.Equal(HttpStatusCode.NoContent, deleteObjectResponse.HttpStatusCode); + } + + [Fact] + public async Task S3Service_Should_List_Files_In_A_Bucket_Async() + { + var bucketName = Guid.NewGuid().ToString(); + await CreateTestBucketAsync(bucketName); + + const int uploadCount = 4; + var fileNames = new string[uploadCount]; + + for (var i = 0; i < uploadCount; i++) + { + var fileName = $"SampleData{i}.txt"; + + await UploadTestFileAsync(fileName, bucketName); + fileNames[i] = fileName; + } + + ListObjectsResponse listObjectsResponse = await AmazonS3.ListObjectsAsync(bucketName); + List s3Objects = listObjectsResponse.S3Objects; + + Assert.Equal(uploadCount, s3Objects.Count); + Assert.All(fileNames, s => Assert.NotNull(s3Objects.Find(o => o.Key == s))); + } + + protected Task CreateTestBucketAsync(string? bucketName = null) + { + var putBucketRequest = new PutBucketRequest { BucketName = bucketName ?? BucketName, UseClientRegion = true }; + + return AmazonS3.PutBucketAsync(putBucketRequest); + } + + protected Task DeleteTestBucketAsync(string? bucketName = null) + { + var deleteBucketRequest = new DeleteBucketRequest { BucketName = bucketName ?? BucketName, UseClientRegion = true }; + + return AmazonS3.DeleteBucketAsync(deleteBucketRequest); + } + + protected async Task UploadTestFileAsync(string? key = null, string? bucketName = null) + { + using var fileTransferUtility = new TransferUtility(AmazonS3); + + 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 new file mode 100644 index 0000000..dd1ce9c --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/S3/S3Scenarios.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.S3; + +[Collection(nameof(LocalStackCollectionV37))] +public sealed class S3ScenarioV37 : BaseS3Scenario +{ + public S3ScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) + { + } +} + +[Collection(nameof(LocalStackCollectionV46))] +public sealed class S3ScenarioV46 : BaseS3Scenario +{ + 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 new file mode 100644 index 0000000..97f2912 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/BaseSnsScenario.cs @@ -0,0 +1,120 @@ +using JsonSerializer = System.Text.Json.JsonSerializer; +using MessageAttributeValue = Amazon.SimpleNotificationService.Model.MessageAttributeValue; + +namespace LocalStack.Client.Functional.Tests.Scenarios.SNS; + +public abstract class BaseSnsScenario : BaseScenario +{ + protected BaseSnsScenario(TestFixture testFixture, ILocalStackFixture localStackFixture, string configFile = TestConstants.LocalStackConfig, + bool useServiceUrl = false) : base(testFixture, localStackFixture, configFile, useServiceUrl) + { + AmazonSimpleNotificationService = ServiceProvider.GetRequiredService(); + } + + protected IAmazonSimpleNotificationService AmazonSimpleNotificationService { get; } + + [Fact] + public async Task SnsService_Should_Create_A_Sns_Topic_Async() + { + var topicName = Guid.NewGuid().ToString(); + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); + + Assert.Equal(HttpStatusCode.OK, createTopicResponse.HttpStatusCode); + + 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); //Cleanup + } + + [Fact] + public async Task SnsService_Should_Delete_A_Sns_Topic_Async() + { + var topicName = Guid.NewGuid().ToString(); + + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); + DeleteTopicResponse deleteTopicResponse = await DeleteSnsTopicAsync(createTopicResponse.TopicArn); + + Assert.Equal(HttpStatusCode.OK, deleteTopicResponse.HttpStatusCode); + + ListTopicsResponse listTopicsResponse = await AmazonSimpleNotificationService.ListTopicsAsync(); + bool hasAny = listTopicsResponse.Topics?.Exists(topic => topic.TopicArn == createTopicResponse.TopicArn) ?? false; + + Assert.False(hasAny); + } + + [Fact] + public async Task SnsService_Should_Send_Publish_A_Message_Async() + { + var topicName = Guid.NewGuid().ToString(); + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); + + var jobCreatedEvent = new JobCreatedEvent(423565221, 191, 125522, "Painting Service"); + string serializedObject = JsonSerializer.Serialize(jobCreatedEvent); + + var messageAttributes = new Dictionary(StringComparer.Ordinal) + { + [nameof(jobCreatedEvent.EventName)] = new() { DataType = "String", StringValue = jobCreatedEvent.EventName }, + }; + + var publishRequest = new PublishRequest + { + Message = serializedObject, TopicArn = createTopicResponse.TopicArn, Subject = jobCreatedEvent.EventName, MessageAttributes = messageAttributes + }; + + PublishResponse publishResponse = await AmazonSimpleNotificationService.PublishAsync(publishRequest); + + Assert.Equal(HttpStatusCode.OK, publishResponse.HttpStatusCode); + + 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"), + InlineData("eu-west-2"), InlineData("sa-east-1")] + public virtual async Task Multi_Region_Tests_Async(string systemName) + { + var sessionReflection = ServiceProvider.GetRequiredService(); + var amazonSimpleNotificationService = ServiceProvider.GetRequiredService(); + + sessionReflection.SetClientRegion((AmazonSimpleNotificationServiceClient)amazonSimpleNotificationService, systemName); + + Assert.Equal(RegionEndpoint.GetBySystemName(systemName), amazonSimpleNotificationService.Config.RegionEndpoint); + + var topicName = Guid.NewGuid().ToString(); + CreateTopicResponse createTopicResponse = await CreateSnsTopicAsync(topicName); + + Assert.Equal(HttpStatusCode.OK, createTopicResponse.HttpStatusCode); + + var topicArn = $"arn:aws:sns:{systemName}:000000000000:{topicName}"; + + 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); //Cleanup + } + + protected async Task CreateSnsTopicAsync(string topic) + { + var createTopicRequest = new CreateTopicRequest(topic); + + CreateTopicResponse createTopicResponse = await AmazonSimpleNotificationService.CreateTopicAsync(createTopicRequest); + + return createTopicResponse; + } + + protected async Task DeleteSnsTopicAsync(string topic) + { + var deleteTopicRequest = new DeleteTopicRequest(topic); + + DeleteTopicResponse deleteTopicResponse = await AmazonSimpleNotificationService.DeleteTopicAsync(deleteTopicRequest); + + return deleteTopicResponse; + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/Models/JobCreatedEvent.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/Models/JobCreatedEvent.cs new file mode 100644 index 0000000..efa55d1 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/Models/JobCreatedEvent.cs @@ -0,0 +1,3 @@ +namespace LocalStack.Client.Functional.Tests.Scenarios.SNS.Models; + +internal sealed record JobCreatedEvent(long JobId, int ServiceId, int UserId, string Description, string EventName = nameof(JobCreatedEvent)); \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/SnsScenarios.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/SnsScenarios.cs new file mode 100644 index 0000000..8576088 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SNS/SnsScenarios.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.SNS; + +[Collection(nameof(LocalStackCollectionV37))] +public sealed class SnsScenarioV37 : BaseSnsScenario +{ + public SnsScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) + { + } +} + +[Collection(nameof(LocalStackCollectionV46))] +public sealed class SnsScenarioV46 : BaseSnsScenario +{ + public SnsScenarioV46(TestFixture testFixture, LocalStackFixtureV46 localStackFixtureV46) : base(testFixture, localStackFixtureV46) + { + } +} \ 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 new file mode 100644 index 0000000..a5020bd --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/BaseSqsScenario.cs @@ -0,0 +1,147 @@ +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace LocalStack.Client.Functional.Tests.Scenarios.SQS; + +public abstract class BaseSqsScenario : BaseScenario +{ + protected const string TestDlQueueName = "ArmutLocalStack-Test-DLQ.fifo"; + protected const string TestQueueName = "ArmutLocalStack-Test.fifo"; + + protected BaseSqsScenario(TestFixture testFixture, ILocalStackFixture localStackFixture, string configFile = TestConstants.LocalStackConfig, + bool useServiceUrl = false) : base(testFixture, localStackFixture, configFile, useServiceUrl) + { + AmazonSqs = ServiceProvider.GetRequiredService(); + } + + protected IAmazonSQS AmazonSqs { get; set; } + + [Fact] + 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); + + Assert.Equal(HttpStatusCode.OK, createQueueResponse.HttpStatusCode); + } + + [Fact] + 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); + DeleteQueueResponse deleteQueueResponse = await DeleteQueueAsync(createQueueResponse.QueueUrl); + + Assert.Equal(HttpStatusCode.OK, deleteQueueResponse.HttpStatusCode); + } + + [Fact] + 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); + + var commentModel = new Fixture().Create(); + string serializedModel = JsonSerializer.Serialize(commentModel); + + var sendMessageRequest = new SendMessageRequest + { + QueueUrl = createQueueResponse.QueueUrl, + MessageGroupId = commentModel.MovieId.ToString(), + MessageDeduplicationId = Guid.NewGuid().ToString(), + MessageBody = serializedModel, + }; + + SendMessageResponse messageResponse = await AmazonSqs.SendMessageAsync(sendMessageRequest); + + Assert.Equal(HttpStatusCode.OK, messageResponse.HttpStatusCode); + } + + [Fact] + 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); + + var commentModel = new Fixture().Create(); + string serializedModel = JsonSerializer.Serialize(commentModel); + + var sendMessageRequest = new SendMessageRequest + { + QueueUrl = createQueueResponse.QueueUrl, + MessageGroupId = commentModel.MovieId.ToString(), + MessageDeduplicationId = Guid.NewGuid().ToString(), + MessageBody = serializedModel + }; + + await AmazonSqs.SendMessageAsync(sendMessageRequest); + + var req = new ReceiveMessageRequest { MaxNumberOfMessages = 1, QueueUrl = createQueueResponse.QueueUrl }; + + ReceiveMessageResponse receiveMessages = await AmazonSqs.ReceiveMessageAsync(req); + Assert.Equal(HttpStatusCode.OK, receiveMessages.HttpStatusCode); + + Message? currentMessage = receiveMessages.Messages.FirstOrDefault(); + Assert.NotNull(currentMessage); + + var deserializedComment = JsonSerializer.Deserialize(currentMessage.Body); + Assert.NotNull(deserializedComment); + Assert.True(commentModel.DeepEquals(deserializedComment)); + } + + protected async Task CreateFifoQueueWithRedriveAsync(string? queueName = null, string? dlQueueName = null) + { + var createDlqRequest = new CreateQueueRequest + { + QueueName = dlQueueName ?? TestDlQueueName, Attributes = new Dictionary(StringComparer.Ordinal) { { "FifoQueue", "true" }, }, + }; + + CreateQueueResponse createDlqResult = await AmazonSqs.CreateQueueAsync(createDlqRequest); + + GetQueueAttributesResponse attributes = await AmazonSqs.GetQueueAttributesAsync(new GetQueueAttributesRequest + { + QueueUrl = createDlqResult.QueueUrl, + AttributeNames = new List { "QueueArn" }, + }) + ; + + var redrivePolicy = new { maxReceiveCount = "1", deadLetterTargetArn = attributes.Attributes["QueueArn"] }; + + var createQueueRequest = new CreateQueueRequest + { + QueueName = queueName ?? TestQueueName, + Attributes = new Dictionary(StringComparer.Ordinal) + { + { "FifoQueue", "true" }, { "RedrivePolicy", JsonSerializer.Serialize(redrivePolicy) }, + }, + }; + + return await AmazonSqs.CreateQueueAsync(createQueueRequest); + } + + protected async Task CreateQueueAsync(string? queueName = null) + { + var createQueueRequest = new CreateQueueRequest(queueName ?? TestQueueName); + + return await AmazonSqs.CreateQueueAsync(createQueueRequest); + } + + [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings")] + protected async Task DeleteQueueAsync(string queueUrl) + { + var deleteQueueRequest = new DeleteQueueRequest(queueUrl); + + return await AmazonSqs.DeleteQueueAsync(deleteQueueRequest); + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/Models/CommentModel.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/Models/CommentModel.cs new file mode 100644 index 0000000..bf22010 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/Models/CommentModel.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + +namespace LocalStack.Client.Functional.Tests.Scenarios.SQS.Models; + +public class CommentModel +{ + public Guid MovieId { get; set; } + + public string Comment { get; set; } + + public string CreateDate { get; set; } + + public Guid CommentId { get; set; } +} diff --git a/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/SqsScenarios.cs b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/SqsScenarios.cs new file mode 100644 index 0000000..ecdb77e --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/Scenarios/SQS/SqsScenarios.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.SQS; + +[Collection(nameof(LocalStackCollectionV37))] +public sealed class SqsScenarioV37 : BaseSqsScenario +{ + public SqsScenarioV37(TestFixture testFixture, LocalStackFixtureV37 localStackFixtureV37) : base(testFixture, localStackFixtureV37) + { + } +} + +[Collection(nameof(LocalStackCollectionV46))] +public sealed class SqsScenarioV46 : BaseSqsScenario +{ + 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 new file mode 100644 index 0000000..a623dc3 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/TestConstants.cs @@ -0,0 +1,11 @@ +namespace LocalStack.Client.Functional.Tests; + +public static class TestConstants +{ + public const string LocalStackConfig = "appsettings.LocalStack.json"; + + 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 new file mode 100644 index 0000000..9e9fc4f --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/TestContainers.cs @@ -0,0 +1,17 @@ +#pragma warning disable CA1304,CA1311,MA0011 + +namespace LocalStack.Client.Functional.Tests; + +internal static class TestContainers +{ + public static LocalStackBuilder LocalStackBuilder(string version) + { + return new LocalStackBuilder().WithImage($"localstack/localstack:{version}") + .WithName($"localStack-{version}-{Guid.NewGuid().ToString().ToLower()}") + .WithEnvironment("DOCKER_HOST", "unix:///var/run/docker.sock") + .WithEnvironment("DEBUG", "1") + .WithEnvironment("LS_LOG", "trace-internal") + .WithPortBinding(4566, assignRandomHostPort: true) + .WithCleanUp(true); + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/appsettings.LocalStack.json b/tests/LocalStack.Client.Functional.Tests/appsettings.LocalStack.json new file mode 100644 index 0000000..3ae025d --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/appsettings.LocalStack.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "SampleS3Service": "Debug" + } + }, + "LocalStack": { + "UseLocalStack": true, + "Session": { + "AwsAccessKeyId": "my-AwsAccessKeyId", + "AwsAccessKey": "my-AwsAccessKey", + "AwsSessionToken": "my-AwsSessionToken", + "RegionName": "eu-central-1" + }, + "Config": { + "LocalStackHost": "localhost.localstack.cloud", + "UseSsl": false, + "UseLegacyPorts": false, + "EdgePort": 4566 + } + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/appsettings.LocalStackLegacy.json b/tests/LocalStack.Client.Functional.Tests/appsettings.LocalStackLegacy.json new file mode 100644 index 0000000..8fe7de7 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/appsettings.LocalStackLegacy.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "SampleS3Service": "Debug" + } + }, + "LocalStack": { + "UseLocalStack": true, + "Session": { + "AwsAccessKeyId": "my-AwsAccessKeyId", + "AwsAccessKey": "my-AwsAccessKey", + "AwsSessionToken": "my-AwsSessionToken", + "RegionName": "eu-central-1" + }, + "Config": { + "LocalStackHost": "localhost", + "UseSsl": false, + "UseLegacyPorts": true + } + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Functional.Tests/appsettings.json b/tests/LocalStack.Client.Functional.Tests/appsettings.json new file mode 100644 index 0000000..895bee3 --- /dev/null +++ b/tests/LocalStack.Client.Functional.Tests/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Error", + "System": "Information", + "Microsoft": "Information" + } + }, + "LocalStack": { + "UseLocalStack": 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 new file mode 100644 index 0000000..0f645a7 --- /dev/null +++ b/tests/LocalStack.Client.Integration.Tests/AssertAmazonClient.cs @@ -0,0 +1,46 @@ +namespace LocalStack.Client.Integration.Tests; + +internal static class AssertAmazonClient +{ + public const string TestAwsRegion = "eu-central-1"; + public const bool UseSsl = true; + + [SuppressMessage("Test", "CA1508: Avoid dead conditional code", Justification = "False positive")] + public static void AssertClientConfiguration(AmazonServiceClient amazonServiceClient) + { + if (amazonServiceClient == null) + { + throw new ArgumentNullException(nameof(amazonServiceClient)); + } + + IClientConfig clientConfig = amazonServiceClient.Config; + + if (clientConfig.ServiceURL != null) + { + string protocol = clientConfig.UseHttp ? "http" : "https"; + Assert.Equal($"{protocol}://{Constants.LocalStackHost}:{Constants.EdgePort}/", clientConfig.ServiceURL); + } + else if (clientConfig.ServiceURL == null) + { + Assert.Equal(RegionEndpoint.GetBySystemName(TestAwsRegion), amazonServiceClient.Config.RegionEndpoint); + } + else + { + throw new MisconfiguredClientException( + "Both ServiceURL and RegionEndpoint properties are null. Under normal conditions, one of these two properties must be a set. This means either something has changed in the new version of the Amazon Client library or there is a bug in the LocalStack.NET Client that we could not detect before. Please open an issue on the subject."); + } + + Assert.Equal(UseSsl, !clientConfig.UseHttp); + + PropertyInfo? forcePathStyleProperty = clientConfig.GetType().GetProperty("ForcePathStyle", BindingFlags.Public | BindingFlags.Instance); + + if (forcePathStyleProperty != null) + { + bool useForcePathStyle = forcePathStyleProperty.GetValue(clientConfig) is bool && (bool)forcePathStyleProperty.GetValue(clientConfig)!; + Assert.True(useForcePathStyle); + } + + Assert.Equal(Constants.LocalStackHost, clientConfig.ProxyHost); + Assert.Equal(Constants.EdgePort, clientConfig.ProxyPort); + } +} \ No newline at end of file diff --git a/tests/LocalStack.Client.Integration.Tests/ClientCreationTests.cs b/tests/LocalStack.Client.Integration.Tests/ClientCreationTests.cs deleted file mode 100644 index a075c2c..0000000 --- a/tests/LocalStack.Client.Integration.Tests/ClientCreationTests.cs +++ /dev/null @@ -1,476 +0,0 @@ -using Amazon.APIGateway; -using Amazon.ApiGatewayV2; -using Amazon.AppSync; -using Amazon.Athena; -using Amazon.AutoScaling; -using Amazon.Batch; -using Amazon.CloudFormation; -using Amazon.CloudFront; -using Amazon.CloudSearch; -using Amazon.CloudTrail; -using Amazon.CloudWatch; -using Amazon.CloudWatchEvents; -using Amazon.CloudWatchLogs; -using Amazon.CognitoIdentity; -using Amazon.CognitoIdentityProvider; -using Amazon.DynamoDBv2; -using Amazon.EC2; -using Amazon.ECR; -using Amazon.ECS; -using Amazon.EKS; -using Amazon.ElastiCache; -using Amazon.ElasticBeanstalk; -using Amazon.ElasticLoadBalancing; -using Amazon.ElasticMapReduce; -using Amazon.Elasticsearch; -using Amazon.Glue; -using Amazon.IdentityManagement; -using Amazon.IoT; -using Amazon.KeyManagementService; -using Amazon.KinesisFirehose; -using Amazon.Lambda; -using Amazon.MediaStore; -using Amazon.MediaStoreData; -using Amazon.Organizations; -using Amazon.QLDB; -using Amazon.RDS; -using Amazon.RDSDataService; -using Amazon.Redshift; -using Amazon.Route53; -using Amazon.S3; -using Amazon.SageMaker; -using Amazon.SageMakerRuntime; -using Amazon.SecretsManager; -using Amazon.SecurityToken; -using Amazon.SimpleEmail; -using Amazon.SimpleNotificationService; -using Amazon.SimpleSystemsManagement; -using Amazon.SimpleWorkflow; -using Amazon.SQS; -using Amazon.StepFunctions; -using Amazon.XRay; - -using LocalStack.Client.Contracts; - -using Xunit; - -namespace LocalStack.Client.Integration.Tests -{ - public class ClientCreationTests - { - private static readonly ISession Session; - - static ClientCreationTests() - { - Session = SessionStandalone.Init().Create(); - } - - [Fact] - public void Should_Able_To_Create_AmazonAPIGatewayClient() - { - var amazonApiGatewayClient = Session.CreateClient(); - - Assert.NotNull(amazonApiGatewayClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonApiGatewayV2Client() - { - var amazonApiGatewayV2Client = Session.CreateClient(); - - Assert.NotNull(amazonApiGatewayV2Client); - } - - [Fact] - public void Should_Able_To_Create_AmazonS3Client() - { - var amazonS3Client = Session.CreateClient(); - - Assert.NotNull(amazonS3Client); - } - - [Fact] - public void Should_Able_To_Create_AmazonDynamoDBClient() - { - var amazonDynamoDbClient = Session.CreateClient(); - - Assert.NotNull(amazonDynamoDbClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonElasticsearchClient() - { - var amazonElasticsearchClient = Session.CreateClient(); - - Assert.NotNull(amazonElasticsearchClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonKinesisFirehoseClient() - { - var amazonKinesisFirehoseClient = Session.CreateClient(); - - Assert.NotNull(amazonKinesisFirehoseClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonLambdaClient() - { - var amazonLambdaClient = Session.CreateClient(); - - Assert.NotNull(amazonLambdaClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSimpleNotificationServiceClient() - { - var amazonSimpleNotificationServiceClient = Session.CreateClient(); - - Assert.NotNull(amazonSimpleNotificationServiceClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSQSClient() - { - var amazonSqsClient = Session.CreateClient(); - - Assert.NotNull(amazonSqsClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonRedshiftClient() - { - var amazonRedshiftClient = Session.CreateClient(); - - Assert.NotNull(amazonRedshiftClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSimpleEmailServiceClient() - { - var amazonSimpleEmailServiceClient = Session.CreateClient(); - - Assert.NotNull(amazonSimpleEmailServiceClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonRoute53Client() - { - var amazonRoute53Client = Session.CreateClient(); - - Assert.NotNull(amazonRoute53Client); - } - - [Fact] - public void Should_Able_To_Create_AmazonCloudFormationClient() - { - var amazonCloudFormationClient = Session.CreateClient(); - - Assert.NotNull(amazonCloudFormationClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCloudWatchClient() - { - var amazonCloudWatchClient = Session.CreateClient(); - - Assert.NotNull(amazonCloudWatchClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSimpleSystemsManagementClient() - { - var amazonSimpleSystemsManagementClient = Session.CreateClient(); - - Assert.NotNull(amazonSimpleSystemsManagementClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSecretsManagerClient() - { - var amazonSecretsManagerClient = Session.CreateClient(); - - Assert.NotNull(amazonSecretsManagerClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonStepFunctionsClient() - { - var amazonSecretsManagerClient = Session.CreateClient(); - - Assert.NotNull(amazonSecretsManagerClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCloudWatchLogsClient() - { - var amazonCloudWatchLogsClient = Session.CreateClient(); - - Assert.NotNull(amazonCloudWatchLogsClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCloudWatchEventsClient() - { - var amazonCloudWatchEventsClient = Session.CreateClient(); - - Assert.NotNull(amazonCloudWatchEventsClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonElasticLoadBalancingClient() - { - var amazonElasticLoadBalancingClient = Session.CreateClient(); - - Assert.NotNull(amazonElasticLoadBalancingClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonIoTClient() - { - var amazonIoTClient = Session.CreateClient(); - - Assert.NotNull(amazonIoTClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCognitoIdentityProviderClient() - { - var amazonCognitoIdentityProviderClient = Session.CreateClient(); - - Assert.NotNull(amazonCognitoIdentityProviderClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCognitoIdentityClient() - { - var amazonCognitoIdentityClient = Session.CreateClient(); - - Assert.NotNull(amazonCognitoIdentityClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSecurityTokenServiceClient() - { - var amazonSecurityTokenServiceClient = Session.CreateClient(); - - Assert.NotNull(amazonSecurityTokenServiceClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonIdentityManagementServiceClient() - { - var amazonIdentityManagementServiceClient = Session.CreateClient(); - - Assert.NotNull(amazonIdentityManagementServiceClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonRDSClient() - { - var amazonRdsClient = Session.CreateClient(); - - Assert.NotNull(amazonRdsClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonRDSDataServiceClient() - { - var amazonRdsDataServiceClient = Session.CreateClient(); - - Assert.NotNull(amazonRdsDataServiceClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCloudSearchClient() - { - var amazonCloudSearchClient = Session.CreateClient(); - - Assert.NotNull(amazonCloudSearchClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSimpleWorkflowClient() - { - var amazonSimpleWorkflowClient = Session.CreateClient(); - - Assert.NotNull(amazonSimpleWorkflowClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonEC2Client() - { - var amazonEc2Client = Session.CreateClient(); - - Assert.NotNull(amazonEc2Client); - } - - [Fact] - public void Should_Able_To_Create_AmazonElastiCacheClient() - { - var amazonElastiCacheClient = Session.CreateClient(); - - Assert.NotNull(amazonElastiCacheClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonKeyManagementServiceClient() - { - var amazonKeyManagementServiceClient = Session.CreateClient(); - - Assert.NotNull(amazonKeyManagementServiceClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonElasticMapReduceClient() - { - var amazonElasticMapReduceClient = Session.CreateClient(); - - Assert.NotNull(amazonElasticMapReduceClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonECSClient() - { - var amazonEcsClient = Session.CreateClient(); - - Assert.NotNull(amazonEcsClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonEKSClient() - { - var amazonEksClient = Session.CreateClient(); - - Assert.NotNull(amazonEksClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonXRayClient() - { - var amazonXRayClient = Session.CreateClient(); - - Assert.NotNull(amazonXRayClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonElasticBeanstalkClient() - { - var amazonElasticBeanstalkClient = Session.CreateClient(); - - Assert.NotNull(amazonElasticBeanstalkClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonAppSyncClient() - { - var amazonAppSyncClient = Session.CreateClient(); - - Assert.NotNull(amazonAppSyncClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCloudFrontClient() - { - var amazonCloudFrontClient = Session.CreateClient(); - - Assert.NotNull(amazonCloudFrontClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonAthenaClient() - { - var amazonAthenaClient = Session.CreateClient(); - - Assert.NotNull(amazonAthenaClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonGlueClient() - { - var amazonGlueClient = Session.CreateClient(); - - Assert.NotNull(amazonGlueClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSageMakerClient() - { - var amazonSageMakerClient = Session.CreateClient(); - - Assert.NotNull(amazonSageMakerClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonSageMakerRuntimeClient() - { - var amazonSageMakerRuntimeClient = Session.CreateClient(); - - Assert.NotNull(amazonSageMakerRuntimeClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonECRClient() - { - var amazonEcrClient = Session.CreateClient(); - - Assert.NotNull(amazonEcrClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonQLDBClient() - { - var amazonQldbClient = Session.CreateClient(); - - Assert.NotNull(amazonQldbClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonCloudTrailClient() - { - var amazonCloudTrailClient = Session.CreateClient(); - - Assert.NotNull(amazonCloudTrailClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonBatchClientClient() - { - var amazonBatchClient = Session.CreateClient(); - - Assert.NotNull(amazonBatchClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonOrganizationsClient() - { - var amazonOrganizationsClient = Session.CreateClient(); - - Assert.NotNull(amazonOrganizationsClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonAutoScalingClient() - { - var amazonAutoScalingClient = Session.CreateClient(); - - Assert.NotNull(amazonAutoScalingClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonMediaStoreClient() - { - var amazonMediaStoreClient = Session.CreateClient(); - - Assert.NotNull(amazonMediaStoreClient); - } - - [Fact] - public void Should_Able_To_Create_AmazonMediaStoreDataClient() - { - var amazonMediaStoreDataClient = Session.CreateClient(); - - Assert.NotNull(amazonMediaStoreDataClient); - } - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Integration.Tests/CreateClientByImplementationTests.cs b/tests/LocalStack.Client.Integration.Tests/CreateClientByImplementationTests.cs new file mode 100644 index 0000000..bbba6ec --- /dev/null +++ b/tests/LocalStack.Client.Integration.Tests/CreateClientByImplementationTests.cs @@ -0,0 +1,1147 @@ +namespace LocalStack.Client.Integration.Tests; + +public class CreateClientByImplementationTests +{ + private static readonly ISession Session = SessionStandalone.Init() + .WithSessionOptions(new SessionOptions(regionName: AssertAmazonClient.TestAwsRegion)) + .WithConfigurationOptions(new ConfigOptions(useSsl: AssertAmazonClient.UseSsl)) + .Create(); + + static CreateClientByImplementationTests() + { + } + + [Fact] + public void Should_Able_To_Create_AmazonAPIGatewayClient() + { + var amazonApiGatewayClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonApiGatewayClient); + AssertAmazonClient.AssertClientConfiguration(amazonApiGatewayClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonApiGatewayV2Client() + { + var amazonApiGatewayV2Client = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonApiGatewayV2Client); + AssertAmazonClient.AssertClientConfiguration(amazonApiGatewayV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonS3Client() + { + var amazonS3Client = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonS3Client); + AssertAmazonClient.AssertClientConfiguration(amazonS3Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonDynamoDBClient() + { + var amazonDynamoDbClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonDynamoDbClient); + AssertAmazonClient.AssertClientConfiguration(amazonDynamoDbClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticsearchClient() + { + var amazonElasticsearchClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElasticsearchClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticsearchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonOpenSearchServiceClient() + { + var amazonOpenSearchServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonOpenSearchServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonOpenSearchServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEventBridgeClient() + { + var amazonEventBridgeClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonEventBridgeClient); + AssertAmazonClient.AssertClientConfiguration(amazonEventBridgeClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKinesisFirehoseClient() + { + var amazonKinesisFirehoseClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonKinesisFirehoseClient); + AssertAmazonClient.AssertClientConfiguration(amazonKinesisFirehoseClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonLambdaClient() + { + var amazonLambdaClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonLambdaClient); + AssertAmazonClient.AssertClientConfiguration(amazonLambdaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleNotificationServiceClient() + { + var amazonSimpleNotificationServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSimpleNotificationServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleNotificationServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSQSClient() + { + var amazonSqsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSqsClient); + AssertAmazonClient.AssertClientConfiguration(amazonSqsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRedshiftClient() + { + var amazonRedshiftClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRedshiftClient); + AssertAmazonClient.AssertClientConfiguration(amazonRedshiftClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleEmailServiceClient() + { + var amazonSimpleEmailServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSimpleEmailServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleEmailServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleEmailServiceV2Client() + { + var simpleEmailServiceV2Client = Session.CreateClientByImplementation(); + + Assert.NotNull(simpleEmailServiceV2Client); + AssertAmazonClient.AssertClientConfiguration(simpleEmailServiceV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonRoute53Client() + { + var amazonRoute53Client = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRoute53Client); + AssertAmazonClient.AssertClientConfiguration(amazonRoute53Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonRoute53ResolverClient() + { + var amazonRoute53ResolverClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRoute53ResolverClient); + AssertAmazonClient.AssertClientConfiguration(amazonRoute53ResolverClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudFormationClient() + { + var amazonCloudFormationClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudFormationClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudFormationClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudWatchClient() + { + var amazonCloudWatchClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudWatchClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudWatchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleSystemsManagementClient() + { + var amazonSimpleSystemsManagementClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSimpleSystemsManagementClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleSystemsManagementClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSecretsManagerClient() + { + var amazonSecretsManagerClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSecretsManagerClient); + AssertAmazonClient.AssertClientConfiguration(amazonSecretsManagerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonStepFunctionsClient() + { + var amazonStepFunctionsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonStepFunctionsClient); + AssertAmazonClient.AssertClientConfiguration(amazonStepFunctionsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudWatchLogsClient() + { + var amazonCloudWatchLogsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudWatchLogsClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudWatchLogsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudWatchEventsClient() + { + var amazonCloudWatchEventsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudWatchEventsClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudWatchEventsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticLoadBalancingClient() + { + var amazonElasticLoadBalancingClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElasticLoadBalancingClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticLoadBalancingClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTClient() + { + var amazonIoTClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonIoTClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTAnalyticsClient() + { + var amazonIoTAnalyticsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonIoTAnalyticsClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTAnalyticsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTEventsClient() + { + var amazonIoTEventsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonIoTEventsClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTEventsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTEventsDataClient() + { + var amazonIoTEventsDataClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonIoTEventsDataClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTEventsDataClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTWirelessClient() + { + var amazonIoTWirelessClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonIoTWirelessClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTWirelessClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTDataClient_With_ServiceUrl() + { + var amazonIoTDataClient = Session.CreateClientByImplementation(useServiceUrl: true); + + Assert.NotNull(amazonIoTDataClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTDataClient); + } + + [Fact] + public void Should_Throw_AmazonClientException_When_Creating_AmazonIoTDataClient_If_RegionEndpoint_Used() + { + try + { + Session.CreateClientByImplementation(); + } + catch (Exception e) + { + Exception? ex = e; + + while (ex != null) + { + if (ex is AmazonClientException) + { + return; + } + + ex = ex.InnerException; + } + + throw; + } + + Assert.Fail("Exception has not thrown"); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTJobsDataPlaneClient_With_ServiceUr() + { + var amazonIoTJobsDataPlaneClient = Session.CreateClientByImplementation(useServiceUrl: true); + + Assert.NotNull(amazonIoTJobsDataPlaneClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTJobsDataPlaneClient); + } + + [Fact] + public void Should_Throw_AmazonClientException_When_Creating_AmazonIoTJobsDataPlaneClient_If_RegionEndpoint_Used() + { + try + { + Session.CreateClientByImplementation(); + } + catch (Exception e) + { + Exception? ex = e; + + while (ex != null) + { + if (ex is AmazonClientException) + { + return; + } + + ex = ex.InnerException; + } + + throw; + } + + Assert.Fail("Exception has not thrown"); + } + + [Fact] + public void Should_Able_To_Create_AmazonCognitoIdentityProviderClient() + { + var amazonCognitoIdentityProviderClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCognitoIdentityProviderClient); + AssertAmazonClient.AssertClientConfiguration(amazonCognitoIdentityProviderClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCognitoIdentityClient() + { + var amazonCognitoIdentityClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCognitoIdentityClient); + AssertAmazonClient.AssertClientConfiguration(amazonCognitoIdentityClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSecurityTokenServiceClient() + { + var amazonSecurityTokenServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSecurityTokenServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonSecurityTokenServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIdentityManagementServiceClient() + { + var amazonIdentityManagementServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonIdentityManagementServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonIdentityManagementServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRDSClient() + { + var amazonRdsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRdsClient); + AssertAmazonClient.AssertClientConfiguration(amazonRdsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRDSDataServiceClient() + { + var amazonRdsDataServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRdsDataServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonRdsDataServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudSearchClient() + { + var amazonCloudSearchClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudSearchClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudSearchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleWorkflowClient() + { + var amazonSimpleWorkflowClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSimpleWorkflowClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleWorkflowClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEC2Client() + { + var amazonEc2Client = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonEc2Client); + AssertAmazonClient.AssertClientConfiguration(amazonEc2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonElastiCacheClient() + { + var amazonElastiCacheClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElastiCacheClient); + AssertAmazonClient.AssertClientConfiguration(amazonElastiCacheClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKeyManagementServiceClient() + { + var amazonKeyManagementServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonKeyManagementServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonKeyManagementServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticMapReduceClient() + { + var amazonElasticMapReduceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElasticMapReduceClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticMapReduceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonECSClient() + { + var amazonEcsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonEcsClient); + AssertAmazonClient.AssertClientConfiguration(amazonEcsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEKSClient() + { + var amazonEksClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonEksClient); + AssertAmazonClient.AssertClientConfiguration(amazonEksClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonXRayClient() + { + var amazonXRayClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonXRayClient); + AssertAmazonClient.AssertClientConfiguration(amazonXRayClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticBeanstalkClient() + { + var amazonElasticBeanstalkClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElasticBeanstalkClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticBeanstalkClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppSyncClient() + { + var amazonAppSyncClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAppSyncClient); + AssertAmazonClient.AssertClientConfiguration(amazonAppSyncClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudFrontClient() + { + var amazonCloudFrontClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudFrontClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudFrontClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAthenaClient() + { + var amazonAthenaClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAthenaClient); + AssertAmazonClient.AssertClientConfiguration(amazonAthenaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonGlueClient() + { + var amazonGlueClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonGlueClient); + AssertAmazonClient.AssertClientConfiguration(amazonGlueClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSageMakerClient() + { + var amazonSageMakerClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSageMakerClient); + AssertAmazonClient.AssertClientConfiguration(amazonSageMakerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSageMakerRuntimeClient() + { + var amazonSageMakerRuntimeClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSageMakerRuntimeClient); + AssertAmazonClient.AssertClientConfiguration(amazonSageMakerRuntimeClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonECRClient() + { + var amazonEcrClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonEcrClient); + AssertAmazonClient.AssertClientConfiguration(amazonEcrClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonQLDBClient() + { + var amazonQldbClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonQldbClient); + AssertAmazonClient.AssertClientConfiguration(amazonQldbClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonQLDBSessionClient() + { + var amazonQldbSessionClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonQldbSessionClient); + AssertAmazonClient.AssertClientConfiguration(amazonQldbSessionClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudTrailClient() + { + var amazonCloudTrailClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCloudTrailClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudTrailClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonBatchClientClient() + { + var amazonBatchClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonBatchClient); + AssertAmazonClient.AssertClientConfiguration(amazonBatchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonOrganizationsClient() + { + var amazonOrganizationsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonOrganizationsClient); + AssertAmazonClient.AssertClientConfiguration(amazonOrganizationsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAutoScalingClient() + { + var amazonAutoScalingClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAutoScalingClient); + AssertAmazonClient.AssertClientConfiguration(amazonAutoScalingClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMediaStoreClient() + { + var amazonMediaStoreClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonMediaStoreClient); + AssertAmazonClient.AssertClientConfiguration(amazonMediaStoreClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMediaStoreDataClient_With_ServiceUrl() + { + var amazonMediaStoreDataClient = Session.CreateClientByImplementation(useServiceUrl: true); + + Assert.NotNull(amazonMediaStoreDataClient); + AssertAmazonClient.AssertClientConfiguration(amazonMediaStoreDataClient); + } + + [Fact] + public void Should_Throw_AmazonClientException_When_Creating_AmazonMediaStoreDataClient_If_RegionEndpoint_Used() + { + try + { + Session.CreateClientByImplementation(); + } + catch (Exception e) + { + Exception? ex = e; + + while (ex != null) + { + if (ex is AmazonClientException) + { + return; + } + + ex = ex.InnerException; + } + + throw; + } + + Assert.Fail("Exception has not thrown"); + } + + [Fact] + public void Should_Able_To_Create_AmazonTransferClient() + { + var amazonTransferClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonTransferClient); + AssertAmazonClient.AssertClientConfiguration(amazonTransferClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCertificateManagerClient() + { + var amazonCertificateManagerClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCertificateManagerClient); + AssertAmazonClient.AssertClientConfiguration(amazonCertificateManagerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodeCommitClient() + { + var amazonCodeCommitClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCodeCommitClient); + AssertAmazonClient.AssertClientConfiguration(amazonCodeCommitClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKinesisAnalyticsClient() + { + var amazonKinesisAnalyticsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonKinesisAnalyticsClient); + AssertAmazonClient.AssertClientConfiguration(amazonKinesisAnalyticsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKinesisAnalyticsV2Client() + { + var kinesisAnalyticsV2Client = Session.CreateClientByImplementation(); + + Assert.NotNull(kinesisAnalyticsV2Client); + AssertAmazonClient.AssertClientConfiguration(kinesisAnalyticsV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonAmplifyClient() + { + var amazonAmplifyClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAmplifyClient); + AssertAmazonClient.AssertClientConfiguration(amazonAmplifyClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKafkaClient() + { + var amazonKafkaClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonKafkaClient); + AssertAmazonClient.AssertClientConfiguration(amazonKafkaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRedshiftDataAPIServiceClient() + { + var amazonRedshiftDataApiServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRedshiftDataApiServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonRedshiftDataApiServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonApiGatewayManagementApiClient() + { + var amazonApiGatewayManagementApiClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonApiGatewayManagementApiClient); + AssertAmazonClient.AssertClientConfiguration(amazonApiGatewayManagementApiClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonTimestreamQueryClient() + { + var amazonTimestreamQueryClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonTimestreamQueryClient); + AssertAmazonClient.AssertClientConfiguration(amazonTimestreamQueryClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonTimestreamWriteClient() + { + var amazonTimestreamWriteClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonTimestreamWriteClient); + AssertAmazonClient.AssertClientConfiguration(amazonTimestreamWriteClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonS3ControlClient() + { + var amazonS3ControlClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonS3ControlClient); + AssertAmazonClient.AssertClientConfiguration(amazonS3ControlClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticLoadBalancingV2Client() + { + var amazonElasticLoadBalancingV2Client = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElasticLoadBalancingV2Client); + AssertAmazonClient.AssertClientConfiguration(amazonElasticLoadBalancingV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonSupportClient() + { + var amazonSupportClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonSupportClient); + AssertAmazonClient.AssertClientConfiguration(amazonSupportClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonNeptuneClient() + { + var amazonNeptuneClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonNeptuneClient); + AssertAmazonClient.AssertClientConfiguration(amazonNeptuneClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonDocDBClient() + { + var amazonDocDbClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonDocDbClient); + AssertAmazonClient.AssertClientConfiguration(amazonDocDbClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonServiceDiscoveryClient() + { + var amazonServiceDiscoveryClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonServiceDiscoveryClient); + AssertAmazonClient.AssertClientConfiguration(amazonServiceDiscoveryClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonServerlessApplicationRepositoryClient() + { + var amazonServiceServerlessApplicationRepositoryClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonServiceServerlessApplicationRepositoryClient); + AssertAmazonClient.AssertClientConfiguration(amazonServiceServerlessApplicationRepositoryClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppConfigClient() + { + var amazonAppConfigClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAppConfigClient); + AssertAmazonClient.AssertClientConfiguration(amazonAppConfigClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCostExplorerClient() + { + var amazonCostExplorerClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonCostExplorerClient); + AssertAmazonClient.AssertClientConfiguration(amazonCostExplorerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMediaConvertClient() + { + var amazonMediaConvertClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonMediaConvertClient); + AssertAmazonClient.AssertClientConfiguration(amazonMediaConvertClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonResourceGroupsTaggingAPIClient() + { + var amazonResourceGroupsTaggingApiClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonResourceGroupsTaggingApiClient); + AssertAmazonClient.AssertClientConfiguration(amazonResourceGroupsTaggingApiClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonResourceGroupsClient() + { + var amazonResourceGroupsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonResourceGroupsClient); + AssertAmazonClient.AssertClientConfiguration(amazonResourceGroupsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticFileSystemClient() + { + var amazonElasticFileSystemClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonElasticFileSystemClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticFileSystemClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonBackupClient() + { + var amazonBackupClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonBackupClient); + AssertAmazonClient.AssertClientConfiguration(amazonBackupClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonLakeFormationClient() + { + var amazonLakeFormationClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonLakeFormationClient); + AssertAmazonClient.AssertClientConfiguration(amazonLakeFormationClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonWAFClient() + { + var amazonWafClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonWafClient); + AssertAmazonClient.AssertClientConfiguration(amazonWafClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonWAFV2Client() + { + var amazonWafV2Client = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonWafV2Client); + AssertAmazonClient.AssertClientConfiguration(amazonWafV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonConfigServiceClient() + { + var amazonConfigServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonConfigServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonConfigServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMWAAClient() + { + var amazonMwaaClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonMwaaClient); + AssertAmazonClient.AssertClientConfiguration(amazonMwaaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonFISClient() + { + var amazonFisClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonFisClient); + AssertAmazonClient.AssertClientConfiguration(amazonFisClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAWSMarketplaceMeteringClient() + { + var awsMarketplaceMeteringClient = Session.CreateClientByImplementation(); + + Assert.NotNull(awsMarketplaceMeteringClient); + AssertAmazonClient.AssertClientConfiguration(awsMarketplaceMeteringClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonTranscribeServiceClient() + { + var amazonTranscribeServiceClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonTranscribeServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonTranscribeServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMQClient() + { + var amazonMqClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonMqClient); + AssertAmazonClient.AssertClientConfiguration(amazonMqClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEMRServerlessClient() + { + var amazonEmrServerlessClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonEmrServerlessClient); + AssertAmazonClient.AssertClientConfiguration(amazonEmrServerlessClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppflowClient() + { + var amazonAppflowClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonAppflowClient); + AssertAmazonClient.AssertClientConfiguration(amazonAppflowClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRoute53DomainsClient() + { + var amazonRoute53DomainsClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonRoute53DomainsClient); + AssertAmazonClient.AssertClientConfiguration(amazonRoute53DomainsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKeyspacesClient() + { + var amazonKeyspacesClient = Session.CreateClientByImplementation(); + + Assert.NotNull(amazonKeyspacesClient); + AssertAmazonClient.AssertClientConfiguration(amazonKeyspacesClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSchedulerClient() + { + var amazonSchedulerClient = Session.CreateClientByImplementation(); + + 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 new file mode 100644 index 0000000..fbfdaf4 --- /dev/null +++ b/tests/LocalStack.Client.Integration.Tests/CreateClientByInterfaceTests.cs @@ -0,0 +1,1138 @@ +namespace LocalStack.Client.Integration.Tests; + +public class CreateClientByInterfaceTests +{ + private static readonly ISession Session = SessionStandalone.Init() + .WithSessionOptions(new SessionOptions(regionName: AssertAmazonClient.TestAwsRegion)) + .WithConfigurationOptions(new ConfigOptions(useSsl: AssertAmazonClient.UseSsl)) + .Create(); + + static CreateClientByInterfaceTests() + { + } + + [Fact] + public void Should_Able_To_Create_AmazonAPIGatewayClient() + { + AmazonServiceClient amazonApiGatewayClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonApiGatewayClient); + AssertAmazonClient.AssertClientConfiguration(amazonApiGatewayClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonApiGatewayV2Client() + { + AmazonServiceClient amazonApiGatewayV2Client = Session.CreateClientByInterface(); + + Assert.NotNull(amazonApiGatewayV2Client); + AssertAmazonClient.AssertClientConfiguration(amazonApiGatewayV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonS3Client() + { + AmazonServiceClient amazonS3Client = Session.CreateClientByInterface(); + + Assert.NotNull(amazonS3Client); + AssertAmazonClient.AssertClientConfiguration(amazonS3Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonDynamoDBClient() + { + AmazonServiceClient amazonDynamoDbClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonDynamoDbClient); + AssertAmazonClient.AssertClientConfiguration(amazonDynamoDbClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticsearchClient() + { + AmazonServiceClient amazonElasticsearchClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElasticsearchClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticsearchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonOpenSearchServiceClient() + { + AmazonServiceClient amazonOpenSearchServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonOpenSearchServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonOpenSearchServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEventBridgeClient() + { + AmazonServiceClient amazonEventBridgeClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonEventBridgeClient); + AssertAmazonClient.AssertClientConfiguration(amazonEventBridgeClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKinesisFirehoseClient() + { + AmazonServiceClient amazonKinesisFirehoseClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonKinesisFirehoseClient); + AssertAmazonClient.AssertClientConfiguration(amazonKinesisFirehoseClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonLambdaClient() + { + AmazonServiceClient amazonLambdaClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonLambdaClient); + AssertAmazonClient.AssertClientConfiguration(amazonLambdaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleNotificationServiceClient() + { + AmazonServiceClient amazonSimpleNotificationServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSimpleNotificationServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleNotificationServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSQSClient() + { + AmazonServiceClient amazonSqsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSqsClient); + AssertAmazonClient.AssertClientConfiguration(amazonSqsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRedshiftClient() + { + AmazonServiceClient amazonRedshiftClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRedshiftClient); + AssertAmazonClient.AssertClientConfiguration(amazonRedshiftClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleEmailServiceClient() + { + AmazonServiceClient amazonSimpleEmailServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSimpleEmailServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleEmailServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleEmailServiceV2Client() + { + AmazonServiceClient simpleEmailServiceV2Client = Session.CreateClientByInterface(); + + Assert.NotNull(simpleEmailServiceV2Client); + AssertAmazonClient.AssertClientConfiguration(simpleEmailServiceV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonRoute53Client() + { + AmazonServiceClient amazonRoute53Client = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRoute53Client); + AssertAmazonClient.AssertClientConfiguration(amazonRoute53Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonRoute53ResolverClient() + { + AmazonServiceClient amazonRoute53ResolverClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRoute53ResolverClient); + AssertAmazonClient.AssertClientConfiguration(amazonRoute53ResolverClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudFormationClient() + { + AmazonServiceClient amazonCloudFormationClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudFormationClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudFormationClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudWatchClient() + { + AmazonServiceClient amazonCloudWatchClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudWatchClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudWatchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleSystemsManagementClient() + { + AmazonServiceClient amazonSimpleSystemsManagementClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSimpleSystemsManagementClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleSystemsManagementClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSecretsManagerClient() + { + AmazonServiceClient amazonSecretsManagerClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSecretsManagerClient); + AssertAmazonClient.AssertClientConfiguration(amazonSecretsManagerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonStepFunctionsClient() + { + AmazonServiceClient amazonSecretsManagerClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSecretsManagerClient); + AssertAmazonClient.AssertClientConfiguration(amazonSecretsManagerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudWatchLogsClient() + { + AmazonServiceClient amazonCloudWatchLogsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudWatchLogsClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudWatchLogsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudWatchEventsClient() + { + AmazonServiceClient amazonCloudWatchEventsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudWatchEventsClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudWatchEventsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticLoadBalancingClient() + { + AmazonServiceClient amazonElasticLoadBalancingClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElasticLoadBalancingClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticLoadBalancingClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTClient() + { + AmazonServiceClient amazonIoTClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonIoTClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTAnalyticsClient() + { + AmazonServiceClient amazonIoTAnalyticsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonIoTAnalyticsClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTAnalyticsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTEventsClient() + { + AmazonServiceClient amazonIoTEventsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonIoTEventsClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTEventsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTEventsDataClient() + { + AmazonServiceClient amazonIoTEventsDataClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonIoTEventsDataClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTEventsDataClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTWirelessClient() + { + AmazonServiceClient amazonIoTWirelessClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonIoTWirelessClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTWirelessClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTDataClient_With_ServiceUrl() + { + AmazonServiceClient amazonIoTDataClient = Session.CreateClientByInterface(useServiceUrl: true); + + Assert.NotNull(amazonIoTDataClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTDataClient); + } + + [Fact] + public void Should_Throw_AmazonClientException_When_Creating_AmazonIoTDataClient_If_RegionEndpoint_Used() + { + try + { + Session.CreateClientByInterface(); + } + catch (Exception e) + { + Exception? ex = e; + + while (ex != null) + { + if (ex is AmazonClientException) + { + return; + } + + ex = ex.InnerException; + } + + throw; + } + + Assert.Fail("Exception has not thrown"); + } + + [Fact] + public void Should_Able_To_Create_AmazonIoTJobsDataPlaneClient_With_ServiceUrl() + { + AmazonServiceClient amazonIoTJobsDataPlaneClient = Session.CreateClientByInterface(useServiceUrl: true); + + Assert.NotNull(amazonIoTJobsDataPlaneClient); + AssertAmazonClient.AssertClientConfiguration(amazonIoTJobsDataPlaneClient); + } + + [Fact] + public void Should_Throw_AmazonClientException_When_Creating_AmazonIoTJobsDataPlaneClient_If_RegionEndpoint_Used() + { + try + { + Session.CreateClientByInterface(); + } + catch (Exception e) + { + Exception? ex = e; + + while (ex != null) + { + if (ex is AmazonClientException) + { + return; + } + + ex = ex.InnerException; + } + + throw; + } + + Assert.Fail("Exception has not thrown"); + } + + [Fact] + public void Should_Able_To_Create_AmazonCognitoIdentityProviderClient() + { + AmazonServiceClient amazonCognitoIdentityProviderClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCognitoIdentityProviderClient); + AssertAmazonClient.AssertClientConfiguration(amazonCognitoIdentityProviderClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCognitoIdentityClient() + { + AmazonServiceClient amazonCognitoIdentityClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCognitoIdentityClient); + AssertAmazonClient.AssertClientConfiguration(amazonCognitoIdentityClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSecurityTokenServiceClient() + { + AmazonServiceClient amazonSecurityTokenServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSecurityTokenServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonSecurityTokenServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonIdentityManagementServiceClient() + { + AmazonServiceClient amazonIdentityManagementServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonIdentityManagementServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonIdentityManagementServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRDSClient() + { + AmazonServiceClient amazonRdsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRdsClient); + AssertAmazonClient.AssertClientConfiguration(amazonRdsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRDSDataServiceClient() + { + AmazonServiceClient amazonRdsDataServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRdsDataServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonRdsDataServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudSearchClient() + { + AmazonServiceClient amazonCloudSearchClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudSearchClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudSearchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSimpleWorkflowClient() + { + AmazonServiceClient amazonSimpleWorkflowClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSimpleWorkflowClient); + AssertAmazonClient.AssertClientConfiguration(amazonSimpleWorkflowClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEC2Client() + { + AmazonServiceClient amazonEc2Client = Session.CreateClientByInterface(); + + Assert.NotNull(amazonEc2Client); + AssertAmazonClient.AssertClientConfiguration(amazonEc2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonElastiCacheClient() + { + AmazonServiceClient amazonElastiCacheClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElastiCacheClient); + AssertAmazonClient.AssertClientConfiguration(amazonElastiCacheClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKeyManagementServiceClient() + { + AmazonServiceClient amazonKeyManagementServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonKeyManagementServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonKeyManagementServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticMapReduceClient() + { + AmazonServiceClient amazonElasticMapReduceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElasticMapReduceClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticMapReduceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonECSClient() + { + AmazonServiceClient amazonEcsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonEcsClient); + AssertAmazonClient.AssertClientConfiguration(amazonEcsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEKSClient() + { + AmazonServiceClient amazonEksClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonEksClient); + AssertAmazonClient.AssertClientConfiguration(amazonEksClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonXRayClient() + { + AmazonServiceClient amazonXRayClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonXRayClient); + AssertAmazonClient.AssertClientConfiguration(amazonXRayClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticBeanstalkClient() + { + AmazonServiceClient amazonElasticBeanstalkClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElasticBeanstalkClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticBeanstalkClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppSyncClient() + { + AmazonServiceClient amazonAppSyncClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAppSyncClient); + AssertAmazonClient.AssertClientConfiguration(amazonAppSyncClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudFrontClient() + { + AmazonServiceClient amazonCloudFrontClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudFrontClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudFrontClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAthenaClient() + { + AmazonServiceClient amazonAthenaClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAthenaClient); + AssertAmazonClient.AssertClientConfiguration(amazonAthenaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonGlueClient() + { + AmazonServiceClient amazonGlueClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonGlueClient); + AssertAmazonClient.AssertClientConfiguration(amazonGlueClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSageMakerClient() + { + AmazonServiceClient amazonSageMakerClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSageMakerClient); + AssertAmazonClient.AssertClientConfiguration(amazonSageMakerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSageMakerRuntimeClient() + { + AmazonServiceClient amazonSageMakerRuntimeClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSageMakerRuntimeClient); + AssertAmazonClient.AssertClientConfiguration(amazonSageMakerRuntimeClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonECRClient() + { + AmazonServiceClient amazonEcrClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonEcrClient); + AssertAmazonClient.AssertClientConfiguration(amazonEcrClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonQLDBClient() + { + AmazonServiceClient amazonQldbClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonQldbClient); + AssertAmazonClient.AssertClientConfiguration(amazonQldbClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonQLDBSessionClient() + { + AmazonServiceClient amazonQldbSessionClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonQldbSessionClient); + AssertAmazonClient.AssertClientConfiguration(amazonQldbSessionClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCloudTrailClient() + { + AmazonServiceClient amazonCloudTrailClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCloudTrailClient); + AssertAmazonClient.AssertClientConfiguration(amazonCloudTrailClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonBatchClientClient() + { + AmazonServiceClient amazonBatchClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonBatchClient); + AssertAmazonClient.AssertClientConfiguration(amazonBatchClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonOrganizationsClient() + { + AmazonServiceClient amazonOrganizationsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonOrganizationsClient); + AssertAmazonClient.AssertClientConfiguration(amazonOrganizationsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAutoScalingClient() + { + AmazonServiceClient amazonAutoScalingClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAutoScalingClient); + AssertAmazonClient.AssertClientConfiguration(amazonAutoScalingClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMediaStoreClient() + { + AmazonServiceClient amazonMediaStoreClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonMediaStoreClient); + AssertAmazonClient.AssertClientConfiguration(amazonMediaStoreClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMediaStoreDataClient_With_ServiceUrl() + { + AmazonServiceClient amazonMediaStoreDataClient = Session.CreateClientByInterface(useServiceUrl: true); + + Assert.NotNull(amazonMediaStoreDataClient); + AssertAmazonClient.AssertClientConfiguration(amazonMediaStoreDataClient); + } + + [Fact] + public void Should_Throw_AmazonClientException_When_Creating_AmazonMediaStoreDataClient_If_RegionEndpoint_Used() + { + try + { + Session.CreateClientByInterface(); + } + catch (Exception e) + { + Exception? ex = e; + + while (ex != null) + { + if (ex is AmazonClientException) + { + return; + } + + ex = ex.InnerException; + } + + throw; + } + + Assert.Fail("Exception has not thrown"); + } + + [Fact] + public void Should_Able_To_Create_AmazonTransferClient() + { + AmazonServiceClient amazonTransferClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonTransferClient); + AssertAmazonClient.AssertClientConfiguration(amazonTransferClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCertificateManagerClient() + { + AmazonServiceClient amazonCertificateManagerClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCertificateManagerClient); + AssertAmazonClient.AssertClientConfiguration(amazonCertificateManagerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCodeCommitClient() + { + AmazonServiceClient amazonCodeCommitClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCodeCommitClient); + AssertAmazonClient.AssertClientConfiguration(amazonCodeCommitClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKinesisAnalyticsClient() + { + AmazonServiceClient amazonKinesisAnalyticsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonKinesisAnalyticsClient); + AssertAmazonClient.AssertClientConfiguration(amazonKinesisAnalyticsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKinesisAnalyticsV2Client() + { + AmazonServiceClient kinesisAnalyticsV2Client = Session.CreateClientByInterface(); + + Assert.NotNull(kinesisAnalyticsV2Client); + AssertAmazonClient.AssertClientConfiguration(kinesisAnalyticsV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonAmplifyClient() + { + AmazonServiceClient amazonAmplifyClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAmplifyClient); + AssertAmazonClient.AssertClientConfiguration(amazonAmplifyClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKafkaClient() + { + AmazonServiceClient amazonKafkaClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonKafkaClient); + AssertAmazonClient.AssertClientConfiguration(amazonKafkaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRedshiftDataAPIServiceClient() + { + AmazonServiceClient amazonRedshiftDataApiServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRedshiftDataApiServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonRedshiftDataApiServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonApiGatewayManagementApiClient() + { + AmazonServiceClient amazonApiGatewayManagementApiClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonApiGatewayManagementApiClient); + AssertAmazonClient.AssertClientConfiguration(amazonApiGatewayManagementApiClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonTimestreamQueryClient() + { + AmazonServiceClient amazonTimestreamQueryClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonTimestreamQueryClient); + AssertAmazonClient.AssertClientConfiguration(amazonTimestreamQueryClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonTimestreamWriteClient() + { + AmazonServiceClient amazonTimestreamWriteClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonTimestreamWriteClient); + AssertAmazonClient.AssertClientConfiguration(amazonTimestreamWriteClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonS3ControlClient() + { + AmazonServiceClient amazonS3ControlClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonS3ControlClient); + AssertAmazonClient.AssertClientConfiguration(amazonS3ControlClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticLoadBalancingV2Client() + { + AmazonServiceClient amazonElasticLoadBalancingV2Client = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElasticLoadBalancingV2Client); + AssertAmazonClient.AssertClientConfiguration(amazonElasticLoadBalancingV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonSupportClient() + { + AmazonServiceClient amazonSupportClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonSupportClient); + AssertAmazonClient.AssertClientConfiguration(amazonSupportClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonNeptuneClient() + { + AmazonServiceClient amazonNeptuneClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonNeptuneClient); + AssertAmazonClient.AssertClientConfiguration(amazonNeptuneClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonDocDBClient() + { + AmazonServiceClient amazonDocDbClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonDocDbClient); + AssertAmazonClient.AssertClientConfiguration(amazonDocDbClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonServiceDiscoveryClient() + { + AmazonServiceClient amazonServiceDiscoveryClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonServiceDiscoveryClient); + AssertAmazonClient.AssertClientConfiguration(amazonServiceDiscoveryClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonServerlessApplicationRepositoryClient() + { + AmazonServiceClient amazonServiceServerlessApplicationRepositoryClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonServiceServerlessApplicationRepositoryClient); + AssertAmazonClient.AssertClientConfiguration(amazonServiceServerlessApplicationRepositoryClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppConfigClient() + { + AmazonServiceClient amazonAppConfigClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAppConfigClient); + AssertAmazonClient.AssertClientConfiguration(amazonAppConfigClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonCostExplorerClient() + { + AmazonServiceClient amazonCostExplorerClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonCostExplorerClient); + AssertAmazonClient.AssertClientConfiguration(amazonCostExplorerClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMediaConvertClient() + { + AmazonServiceClient amazonMediaConvertClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonMediaConvertClient); + AssertAmazonClient.AssertClientConfiguration(amazonMediaConvertClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonResourceGroupsTaggingAPIClient() + { + AmazonServiceClient amazonResourceGroupsTaggingApiClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonResourceGroupsTaggingApiClient); + AssertAmazonClient.AssertClientConfiguration(amazonResourceGroupsTaggingApiClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonResourceGroupsClient() + { + AmazonServiceClient amazonResourceGroupsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonResourceGroupsClient); + AssertAmazonClient.AssertClientConfiguration(amazonResourceGroupsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonElasticFileSystemClient() + { + AmazonServiceClient amazonElasticFileSystemClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonElasticFileSystemClient); + AssertAmazonClient.AssertClientConfiguration(amazonElasticFileSystemClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonBackupClient() + { + AmazonServiceClient amazonBackupClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonBackupClient); + AssertAmazonClient.AssertClientConfiguration(amazonBackupClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonLakeFormationClient() + { + AmazonServiceClient amazonLakeFormationClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonLakeFormationClient); + AssertAmazonClient.AssertClientConfiguration(amazonLakeFormationClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonWAFClient() + { + AmazonServiceClient amazonWafClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonWafClient); + AssertAmazonClient.AssertClientConfiguration(amazonWafClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonWAFV2Client() + { + AmazonServiceClient amazonWafV2Client = Session.CreateClientByInterface(); + + Assert.NotNull(amazonWafV2Client); + AssertAmazonClient.AssertClientConfiguration(amazonWafV2Client); + } + + [Fact] + public void Should_Able_To_Create_AmazonConfigServiceClient() + { + AmazonServiceClient amazonConfigServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonConfigServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonConfigServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMWAAClient() + { + AmazonServiceClient amazonMwaaClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonMwaaClient); + AssertAmazonClient.AssertClientConfiguration(amazonMwaaClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonFISClient() + { + AmazonServiceClient amazonFisClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonFisClient); + AssertAmazonClient.AssertClientConfiguration(amazonFisClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAWSMarketplaceMeteringClient() + { + AmazonServiceClient awsMarketplaceMeteringClient = Session.CreateClientByInterface(); + + Assert.NotNull(awsMarketplaceMeteringClient); + AssertAmazonClient.AssertClientConfiguration(awsMarketplaceMeteringClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonTranscribeServiceClient() + { + AmazonServiceClient amazonTranscribeServiceClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonTranscribeServiceClient); + AssertAmazonClient.AssertClientConfiguration(amazonTranscribeServiceClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonMQClient() + { + AmazonServiceClient amazonMqClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonMqClient); + AssertAmazonClient.AssertClientConfiguration(amazonMqClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonEMRServerlessClient() + { + AmazonServiceClient amazonEmrServerlessClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonEmrServerlessClient); + AssertAmazonClient.AssertClientConfiguration(amazonEmrServerlessClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonAppflowClient() + { + AmazonServiceClient amazonAppflowClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonAppflowClient); + AssertAmazonClient.AssertClientConfiguration(amazonAppflowClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonRoute53DomainsClient() + { + AmazonServiceClient amazonRoute53DomainsClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonRoute53DomainsClient); + AssertAmazonClient.AssertClientConfiguration(amazonRoute53DomainsClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonKeyspacesClient() + { + AmazonServiceClient amazonKeyspacesClient = Session.CreateClientByInterface(); + + Assert.NotNull(amazonKeyspacesClient); + AssertAmazonClient.AssertClientConfiguration(amazonKeyspacesClient); + } + + [Fact] + public void Should_Able_To_Create_AmazonSchedulerClient() + { + AmazonServiceClient amazonSchedulerClient = Session.CreateClientByInterface(); + + 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 new file mode 100644 index 0000000..8bc1d15 --- /dev/null +++ b/tests/LocalStack.Client.Integration.Tests/GlobalUsings.cs @@ -0,0 +1,129 @@ +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.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; +global using Amazon.CloudTrail; +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; +global using Amazon.CostExplorer; +global using Amazon.DocDB; +global using Amazon.DynamoDBv2; +global using Amazon.EC2; +global using Amazon.ECR; +global using Amazon.ECS; +global using Amazon.EKS; +global using Amazon.ElastiCache; +global using Amazon.ElasticBeanstalk; +global using Amazon.ElasticFileSystem; +global using Amazon.ElasticLoadBalancing; +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; +global using Amazon.IdentityManagement; +global using Amazon.IoT; +global using Amazon.IoTAnalytics; +global using Amazon.IotData; +global using Amazon.IoTEvents; +global using Amazon.IoTEventsData; +global using Amazon.IoTJobsDataPlane; +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; +global using Amazon.LakeFormation; +global using Amazon.Lambda; +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; +global using Amazon.RedshiftDataAPIService; +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; +global using Amazon.SimpleSystemsManagement; +global using Amazon.SimpleWorkflow; +global using Amazon.SQS; +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 LocalStack.Client.Contracts; +global using LocalStack.Client.Exceptions; +global using LocalStack.Client.Models; +global using LocalStack.Client.Options; + +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 9849ecb..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,105 +1,151 @@  - - net461;netcoreapp2.1;netcoreapp3.1 - true - latest + + net472;net8.0;net9.0 + $(NoWarn);CA1707;MA0006;CA1510 + - - - true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - /Library/Frameworks/Mono.framework/Versions/Current/lib/mono - /usr/lib/mono - /usr/local/lib/mono + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + - - $(BaseFrameworkPathOverrideForMono)/4.6.1-api + + + - true - - - $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - + + + \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/ConfigTests/ConfigOptionsTests.cs b/tests/LocalStack.Client.Tests/ConfigTests/ConfigOptionsTests.cs new file mode 100644 index 0000000..fc17c3e --- /dev/null +++ b/tests/LocalStack.Client.Tests/ConfigTests/ConfigOptionsTests.cs @@ -0,0 +1,82 @@ +namespace LocalStack.Client.Tests.ConfigTests; + +public class ConfigOptionsTests +{ + [Fact] + public void ConfigOptions_Should_Created_With_Default_Parameters_If_It_Created_By_Default_Constructor() + { + var configOptions = new ConfigOptions(); + + Assert.Equal(Constants.LocalStackHost, configOptions.LocalStackHost); + Assert.Equal(Constants.UseSsl, configOptions.UseSsl); + Assert.Equal(Constants.UseLegacyPorts, configOptions.UseLegacyPorts); + Assert.Equal(Constants.EdgePort, configOptions.EdgePort); + } + + [Fact] + public void ConfigOptions_Should_Created_With_Default_Parameters_If_It_Created_By_Parameterized_Constructor_And_Non_Of_The_Parameters_Has_Set() + { + ConstructorInfo? constructor = typeof(ConfigOptions).GetConstructor(new[] { typeof(string), typeof(bool), typeof(bool), typeof(int) }); + + Assert.NotNull(constructor); + + var configOptions = (ConfigOptions)constructor.Invoke(new[] { Type.Missing, Type.Missing, Type.Missing, Type.Missing }); + + Assert.Equal(Constants.LocalStackHost, configOptions.LocalStackHost); + Assert.Equal(Constants.UseSsl, configOptions.UseSsl); + Assert.Equal(Constants.UseLegacyPorts, configOptions.UseLegacyPorts); + Assert.Equal(Constants.EdgePort, configOptions.EdgePort); + } + + [Fact] + public void LocalStackHost_Property_Of_ConfigOptions_Should_Equal_To_Given_LocalStackHost_Constructor_Parameter() + { + const string localStackHost = "myhost"; + + var configOptions = new ConfigOptions(localStackHost); + + Assert.Equal(localStackHost, configOptions.LocalStackHost); + Assert.Equal(Constants.UseSsl, configOptions.UseSsl); + Assert.Equal(Constants.UseLegacyPorts, configOptions.UseLegacyPorts); + Assert.Equal(Constants.EdgePort, configOptions.EdgePort); + } + + [Fact] + public void UseSsl_Property_Of_ConfigOptions_Should_Equal_To_Given_UseSsl_Constructor_Parameter() + { + const bool useSsl = true; + + var configOptions = new ConfigOptions(useSsl: useSsl); + + Assert.Equal(Constants.LocalStackHost, configOptions.LocalStackHost); + Assert.Equal(useSsl, configOptions.UseSsl); + Assert.Equal(Constants.UseLegacyPorts, configOptions.UseLegacyPorts); + Assert.Equal(Constants.EdgePort, configOptions.EdgePort); + } + + [Fact] + public void UseLegacyPorts_Property_Of_ConfigOptions_Should_Equal_To_Given_UseLegacyPorts_Constructor_Parameter() + { + const bool useLegacyPorts = true; + + var configOptions = new ConfigOptions(useLegacyPorts: useLegacyPorts); + + Assert.Equal(Constants.LocalStackHost, configOptions.LocalStackHost); + Assert.Equal(Constants.UseSsl, configOptions.UseSsl); + Assert.Equal(useLegacyPorts, configOptions.UseLegacyPorts); + Assert.Equal(Constants.EdgePort, configOptions.EdgePort); + } + + [Fact] + public void EdgePort_Property_Of_ConfigOptions_Should_Equal_To_Given_EdgePort_Constructor_Parameter() + { + const int edgePort = 4212; + + var configOptions = new ConfigOptions(edgePort: edgePort); + + Assert.Equal(Constants.LocalStackHost, configOptions.LocalStackHost); + Assert.Equal(Constants.UseSsl, configOptions.UseSsl); + Assert.Equal(Constants.UseLegacyPorts, configOptions.UseLegacyPorts); + Assert.Equal(edgePort, configOptions.EdgePort); + } +} diff --git a/tests/LocalStack.Client.Tests/ConfigTests/ConfigurationTests.cs b/tests/LocalStack.Client.Tests/ConfigTests/ConfigurationTests.cs new file mode 100644 index 0000000..e986c32 --- /dev/null +++ b/tests/LocalStack.Client.Tests/ConfigTests/ConfigurationTests.cs @@ -0,0 +1,236 @@ +namespace LocalStack.Client.Tests.ConfigTests; + +public class ConfigurationTests +{ + [Fact] + public void GetAwsServiceEndpoint_Should_Return_AwsServiceEndpoint_That_Host_Property_Equals_Set_LocalStackHost_Property_Of_ConfigOptions() + { + const string localStackHost = "myLocalHost"; + + var config = new Config(new ConfigOptions(localStackHost)); + AwsServiceEndpoint? awsServiceEndpoint = config.GetAwsServiceEndpoint(AwsService.ApiGateway); + + Assert.NotNull(awsServiceEndpoint); + Assert.Equal(localStackHost, awsServiceEndpoint.Host); + + awsServiceEndpoint = config.GetAwsServiceEndpoint("ApiGatewayV2"); + + Assert.NotNull(awsServiceEndpoint); + Assert.Equal(localStackHost, awsServiceEndpoint.Host); + } + + [Fact] + public void GetAwsServiceEndpoints_Should_Return_List_Of_AwsServiceEndpoint_That_Host_Property_Of_Every_Item_Equals_By_Set_LocalStackHost_Property_Of_ConfigOptions() + { + const string localStackHost = "myLocalHost"; + + var config = new Config(new ConfigOptions(localStackHost)); + IList awsServiceEndpoints = config.GetAwsServiceEndpoints().ToList(); + + Assert.NotNull(awsServiceEndpoints); + Assert.NotEmpty(awsServiceEndpoints); + Assert.All(awsServiceEndpoints, endpoint => Assert.Equal(localStackHost, endpoint.Host)); + } + + [Theory, InlineData(true, "https"), InlineData(false, "http")] + public void GetAwsServiceEndpoint_Should_Return_AwsServiceEndpoint_That_Protocol_Property_Equals_To_Https_Or_Http_If_Set_UseSsl_Property_Of_ConfigOptions_Equals_True_Or_False( + bool useSsl, string expectedProtocol) + { + var config = new Config(new ConfigOptions(useSsl: useSsl)); + AwsServiceEndpoint? awsServiceEndpoint = config.GetAwsServiceEndpoint(AwsService.ApiGateway); + + Assert.NotNull(awsServiceEndpoint); + Assert.NotNull(awsServiceEndpoint.ServiceUrl.Scheme); + Assert.Equal(expectedProtocol, awsServiceEndpoint.ServiceUrl.Scheme); + + awsServiceEndpoint = config.GetAwsServiceEndpoint("ApiGatewayV2"); + + Assert.NotNull(awsServiceEndpoint); + Assert.NotNull(awsServiceEndpoint.ServiceUrl.Scheme); + Assert.StartsWith(expectedProtocol, awsServiceEndpoint.ServiceUrl.Scheme, StringComparison.Ordinal); + } + + [Theory, InlineData(true, "https"), InlineData(false, "http")] + public void + GetAwsServiceEndpoint_Should_Return_AwsServiceEndpoint_That_Protocol_Property_Of_Every_Item_Equals_To_Https_Or_Http_If_Set_UseSsl_Property_Of_ConfigOptions_Equals_True_Or_False( + bool useSsl, string expectedProtocol) + { + var config = new Config(new ConfigOptions(useSsl: useSsl)); + IList awsServiceEndpoints = config.GetAwsServiceEndpoints().ToList(); + + Assert.NotNull(awsServiceEndpoints); + Assert.NotEmpty(awsServiceEndpoints); + Assert.All(awsServiceEndpoints, endpoint => Assert.Equal(expectedProtocol, endpoint.ServiceUrl.Scheme)); + } + + [Fact] + public void GetAwsServiceEndpoint_Should_Return_AwsServiceEndpoint_That_Port_Property_Equals_To_Set_EdgePort_Property_Of_ConfigOptions_If_UseLegacyPorts_Property_Is_False() + { + const int edgePort = 1234; + + var config = new Config(new ConfigOptions(useLegacyPorts: false, edgePort: edgePort)); + AwsServiceEndpoint? awsServiceEndpoint = config.GetAwsServiceEndpoint(AwsService.ApiGateway); + + Assert.NotNull(awsServiceEndpoint); + Assert.Equal(edgePort, awsServiceEndpoint.Port); + + awsServiceEndpoint = config.GetAwsServiceEndpoint("ApiGatewayV2"); + + Assert.NotNull(awsServiceEndpoint); + Assert.Equal(edgePort, awsServiceEndpoint.Port); + } + + [Theory, + InlineData(true, false, "localhost", 1111, "http://localhost"), + InlineData(true, true, "localhost", 1111, "https://localhost"), + InlineData(false, false, "localhost", 1111, "http://localhost:1111"), + InlineData(false, true, "localhost", 1111, "https://localhost:1111"), + InlineData(true, false, "myHost", 2222, "http://myHost"), + InlineData(true, true, "myHost2", 3334, "https://myHost2"), + InlineData(false, false, "myHost3", 4432, "http://myHost3:4432"), + InlineData(false, true, "myHost4", 2124, "https://myHost4:2124"), + SuppressMessage("Test", "CA1054: URI parameters should not be strings", Justification = "Test case parameter.")] + public void GetAwsServiceEndpoint_Should_Return_AwsServiceEndpoint_That_ServiceUrl_Property_Equals_To_Combination_Of_Host_Protocol_And_Port(bool useLegacyPorts, bool useSsl, string localStackHost, int edgePort, string expectedServiceUrl) + { + var config = new Config(new ConfigOptions(localStackHost, useLegacyPorts: useLegacyPorts, edgePort: edgePort, useSsl: useSsl)); + AwsServiceEndpoint? awsServiceEndpoint = config.GetAwsServiceEndpoint(AwsService.ApiGatewayV2); + + Assert.NotNull(awsServiceEndpoint); + + if (useLegacyPorts) + { + expectedServiceUrl = $"{expectedServiceUrl}:{awsServiceEndpoint.Port.ToString(CultureInfo.InvariantCulture)}"; + } + + Uri serviceUrl = awsServiceEndpoint.ServiceUrl; + var expectedUrl = new Uri(expectedServiceUrl); + + Assert.Equal(expectedUrl, serviceUrl); + + awsServiceEndpoint = config.GetAwsServiceEndpoint("ApiGatewayV2"); + + Assert.NotNull(awsServiceEndpoint); + Assert.Equal(expectedUrl, serviceUrl); + } + + [Theory, + InlineData(true, false, "localhost", 1111, "http://localhost"), + InlineData(true, true, "localhost", 1111, "https://localhost"), + InlineData(false, false, "localhost", 1111, "http://localhost:1111"), + InlineData(false, true, "localhost", 1111, "https://localhost:1111"), + InlineData(true, false, "myHost", 2222, "http://myHost"), + InlineData(true, true, "myHost2", 3334, "https://myHost2"), + InlineData(false, false, "myHost3", 4432, "http://myHost3:4432"), + InlineData(false, true, "myHost4", 2124, "https://myHost4:2124"), + SuppressMessage("Test", "CA1054: URI parameters should not be strings", Justification = "Test case parameter.")] + public void GetAwsServiceEndpoint_Should_Return_AwsServiceEndpoint_That_ServiceUrl_Property_Property_Of_Every_Item_Equals_To_Combination_Of_Host_Protocol_And_Port(bool useLegacyPorts, bool useSsl, string localStackHost, int edgePort, string expectedServiceUrl) + { + var config = new Config(new ConfigOptions(localStackHost, useLegacyPorts: useLegacyPorts, edgePort: edgePort, useSsl: useSsl)); + + IList awsServiceEndpoints = config.GetAwsServiceEndpoints().ToList(); + + Assert.NotNull(awsServiceEndpoints); + Assert.NotEmpty(awsServiceEndpoints); + Assert.All(awsServiceEndpoints, endpoint => + { + string serviceUrl = useLegacyPorts ? $"{expectedServiceUrl}:{endpoint.Port.ToString(CultureInfo.InvariantCulture)}" : expectedServiceUrl; + var expectedUrl = new Uri(serviceUrl); + + Assert.Equal(expectedUrl, endpoint.ServiceUrl); + }); + } + + [Fact] + public void GetAwsServiceEndpoints_Should_Return_List_Of_AwsServiceEndpoint_That_Port_Property_Of_Every_Item_Equals_To_Set_EdgePort_Property_Of_ConfigOptions_If_UseLegacyPorts_Property_Is_False() + { + const int edgePort = 1234; + + var config = new Config(new ConfigOptions(useLegacyPorts: false, edgePort: edgePort)); + + IList awsServiceEndpoints = config.GetAwsServiceEndpoints().ToList(); + + Assert.NotNull(awsServiceEndpoints); + Assert.NotEmpty(awsServiceEndpoints); + Assert.All(awsServiceEndpoints, endpoint => Assert.Equal(edgePort, endpoint.Port)); + } + + [Fact] + public void GetAwsServicePort_Should_Return_Integer_Port_Value_That_Equals_To_Port_Property_Of_Related_AwsServiceEndpoint_If_UseLegacyPorts_Property_Is_True() + { + var config = new Config(new ConfigOptions(useLegacyPorts: true)); + + foreach (AwsServiceEndpointMetadata awsServiceEndpointMetadata in AwsServiceEndpointMetadata.All) + { + int awsServicePort = config.GetAwsServicePort(awsServiceEndpointMetadata.Enum); + + Assert.Equal(awsServiceEndpointMetadata.Port, awsServicePort); + } + } + + [Fact] + public void GetAwsServicePort_Should_Return_Integer_Port_Value_That_Equals_To_Set_EdgePort_Property_Of_ConfigOptions_If_UseLegacyPorts_Property_Is_False() + { + const int edgePort = 1234; + + var config = new Config(new ConfigOptions(useLegacyPorts: false, edgePort: edgePort)); + + foreach (AwsServiceEndpointMetadata awsServiceEndpointMetadata in AwsServiceEndpointMetadata.All) + { + int awsServicePort = config.GetAwsServicePort(awsServiceEndpointMetadata.Enum); + + Assert.Equal(edgePort, awsServicePort); + } + } + + [Fact] + public void + GetAwsServicePorts_Should_Return_AwsServiceEnum_And_Integer_Port_Value_Pair_That_Port_Property_Of_The_Pair_Equals_To_Port_Property_Of_Related_AwsServiceEndpoint_If_UseLegacyPorts_Property_Is_True() + { + var config = new Config(new ConfigOptions(useLegacyPorts: true)); + + IDictionary awsServicePorts = config.GetAwsServicePorts(); + + foreach (AwsServiceEndpointMetadata awsServiceEndpointMetadata in AwsServiceEndpointMetadata.All) + { + KeyValuePair keyValuePair = awsServicePorts.First(pair => pair.Key == awsServiceEndpointMetadata.Enum); + + Assert.Equal(awsServiceEndpointMetadata.Enum, keyValuePair.Key); + Assert.Equal(awsServiceEndpointMetadata.Port, keyValuePair.Value); + } + } + + [Fact] + public void GetAwsServicePorts_Should_Return_AwsServiceEnum_And_Integer_Port_Value_Pair_That_Port_Property_Of_The_Pair_Equals_To_Set_EdgePort_Property_Of_ConfigOptions_If_UseLegacyPorts_Property_Is_False() + { + const int edgePort = 1234; + var config = new Config(new ConfigOptions(useLegacyPorts: false, edgePort: edgePort)); + + IDictionary awsServicePorts = config.GetAwsServicePorts(); + + foreach (AwsServiceEndpointMetadata awsServiceEndpointMetadata in AwsServiceEndpointMetadata.All) + { + KeyValuePair keyValuePair = awsServicePorts.First(pair => pair.Key == awsServiceEndpointMetadata.Enum); + + Assert.Equal(awsServiceEndpointMetadata.Enum, keyValuePair.Key); + Assert.Equal(edgePort, keyValuePair.Value); + } + } + + [Fact] + + public void GetConfigOptions_Should_Return_Given_ConfigOptions() + { + const string localStackHost = Constants.LocalStackHost; + const bool useSsl = true; + const bool useLegacyPorts = false; + const int edgePort = Constants.EdgePort; + + var configOptions = new ConfigOptions(localStackHost, useSsl, useLegacyPorts, edgePort); + var config = new Config(configOptions); + + Assert.Equal(configOptions.LocalStackHost, config.GetConfigOptions().LocalStackHost); + Assert.Equal(configOptions.UseSsl, config.GetConfigOptions().UseSsl); + Assert.Equal(configOptions.UseLegacyPorts, config.GetConfigOptions().UseLegacyPorts); + Assert.Equal(configOptions.EdgePort, config.GetConfigOptions().EdgePort); + } +} diff --git a/tests/LocalStack.Client.Tests/ConstantsTests.cs b/tests/LocalStack.Client.Tests/ConstantsTests.cs new file mode 100644 index 0000000..6fc1118 --- /dev/null +++ b/tests/LocalStack.Client.Tests/ConstantsTests.cs @@ -0,0 +1,17 @@ +namespace LocalStack.Client.Tests; + +public class ConstantsTests +{ + [Fact] + public void All_Constants_Should_Be_Equal_Appropriate_Defaults() + { + Assert.Equal("localhost.localstack.cloud", Constants.LocalStackHost); + Assert.False(Constants.UseSsl); + Assert.False(Constants.UseLegacyPorts); + Assert.Equal(4566, Constants.EdgePort); + Assert.Equal("accessKey", Constants.AwsAccessKeyId); + Assert.Equal("secretKey", Constants.AwsAccessKey); + Assert.Equal("token", Constants.AwsSessionToken); + Assert.Equal("us-east-1", Constants.RegionName); + } +} diff --git a/tests/LocalStack.Client.Tests/GlobalUsings.cs b/tests/LocalStack.Client.Tests/GlobalUsings.cs new file mode 100644 index 0000000..19a1c57 --- /dev/null +++ b/tests/LocalStack.Client.Tests/GlobalUsings.cs @@ -0,0 +1,22 @@ +global using System; +global using System.Collections.Generic; +global using System.Diagnostics.CodeAnalysis; +global using System.Globalization; +global using System.Reflection; +global using System.Linq; + +global using Amazon; +global using Amazon.Runtime; +global using Amazon.Runtime.Internal; + +global using LocalStack.Client.Enums; +global using LocalStack.Client.Exceptions; +global using LocalStack.Client.Models; +global using LocalStack.Client.Options; +global using LocalStack.Tests.Common.Mocks; +global using LocalStack.Tests.Common.Mocks.MockServiceClients; +global using LocalStack.Client.Utils; + +global using Moq; + +global using Xunit; diff --git a/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj b/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj index 1fc4d78..e59c51e 100644 --- a/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj +++ b/tests/LocalStack.Client.Tests/LocalStack.Client.Tests.csproj @@ -1,54 +1,32 @@  - - net461;netcoreapp2.1;netcoreapp3.1 - true - latest - - - - true - - - /Library/Frameworks/Mono.framework/Versions/Current/lib/mono - /usr/lib/mono - /usr/local/lib/mono - - - $(BaseFrameworkPathOverrideForMono)/4.6.1-api - - true - - - $(FrameworkPathOverride)/Facades;$(AssemblySearchPaths) - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - + + net472;net8.0;net9.0 + $(NoWarn);CA1707;MA0006 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAmazonServiceClient.cs b/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAmazonServiceClient.cs deleted file mode 100644 index 39ed80f..0000000 --- a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAmazonServiceClient.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Amazon.Runtime; -using Amazon.Runtime.Internal.Auth; - -namespace LocalStack.Client.Tests.Mocks.MockServiceClients -{ - public class MockAmazonServiceClient : AmazonServiceClient, IAmazonService - { - public MockAmazonServiceClient() : base(FallbackCredentialsFactory.GetCredentials(), new MockClientConfig()) - { - } - - public MockAmazonServiceClient(AWSCredentials credentials, MockClientConfig clientConfig) : base(credentials, clientConfig) - { - } - - public MockAmazonServiceClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, ClientConfig clientConfig) : base( - awsAccessKeyId, awsSecretAccessKey, awsSessionToken, clientConfig) - { - } - - public MockAmazonServiceClient(string awsAccessKeyId, string awsSecretAccessKey, ClientConfig clientConfig) : base( - awsAccessKeyId, awsSecretAccessKey, clientConfig) - { - } - - public AWSCredentials AwsCredentials => Credentials; - - protected override AbstractAWSSigner CreateSigner() - { - return new NullSigner(); - } - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAmazonServiceClientWithServiceMetadata.cs b/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAmazonServiceClientWithServiceMetadata.cs deleted file mode 100644 index 6198145..0000000 --- a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAmazonServiceClientWithServiceMetadata.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Amazon.Runtime.Internal; - -namespace LocalStack.Client.Tests.Mocks.MockServiceClients -{ - public class MockAmazonServiceClientWithServiceMetadata : MockAmazonServiceClient - { - private static IServiceMetadata serviceMetadata = new MockServiceMetadata(); - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAwsServiceEndpoint.cs b/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAwsServiceEndpoint.cs deleted file mode 100644 index b391da3..0000000 --- a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockAwsServiceEndpoint.cs +++ /dev/null @@ -1,12 +0,0 @@ -using LocalStack.Client.Enums; -using LocalStack.Client.Models; - -namespace LocalStack.Client.Tests.Mocks.MockServiceClients -{ - public class MockAwsServiceEndpoint : AwsServiceEndpoint - { - public MockAwsServiceEndpoint() : base(MockServiceMetadata.MockServiceId, "mockService", AwsServiceEnum.ApiGateway, 1234, "localhost", "http://localhost:1234") - { - } - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockClientConfig.cs b/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockClientConfig.cs deleted file mode 100644 index c1dc123..0000000 --- a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockClientConfig.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Amazon.Runtime; -using Amazon.Util.Internal; - -namespace LocalStack.Client.Tests.Mocks.MockServiceClients -{ - public class MockClientConfig : ClientConfig - { - public override string ServiceVersion => "1.0.0.0"; - - public override string UserAgent => InternalSDKUtils.BuildUserAgentString(ServiceVersion); - - public override string RegionEndpointServiceName => "mock-service"; - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs b/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs deleted file mode 100644 index 2d8cb39..0000000 --- a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LocalStack.Client.Tests.Mocks.MockServiceClients -{ - public class MockClientConfigWithForcePathStyle : MockClientConfig - { - public bool ForcePathStyle { get; set; } = false; - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockServiceMetadata.cs b/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockServiceMetadata.cs deleted file mode 100644 index fbd6175..0000000 --- a/tests/LocalStack.Client.Tests/Mocks/MockServiceClients/MockServiceMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Amazon.Runtime.Internal; - -using System.Collections.Generic; - -namespace LocalStack.Client.Tests.Mocks.MockServiceClients -{ - public class MockServiceMetadata : IServiceMetadata - { - public const string MockServiceId = "Mock Amazon Service"; - - public string ServiceId => MockServiceId; - - public IDictionary OperationNameMapping { get; } - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/Mocks/MockSession.cs b/tests/LocalStack.Client.Tests/Mocks/MockSession.cs deleted file mode 100644 index 08ba3f2..0000000 --- a/tests/LocalStack.Client.Tests/Mocks/MockSession.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LocalStack.Client.Contracts; - -using Moq; - -namespace LocalStack.Client.Tests.Mocks -{ - public class MockSession : Session - { - private MockSession(Mock sessionOptionsMock, Mock configMock, Mock sessionReflectionMock) : base( - sessionOptionsMock.Object, configMock.Object, sessionReflectionMock.Object) - { - SessionOptionsMock = sessionOptionsMock; - ConfigMock = configMock; - SessionReflectionMock = sessionReflectionMock; - } - - public Mock SessionOptionsMock { get; } - - public Mock ConfigMock { get; } - - public Mock SessionReflectionMock { get; } - - public static MockSession Create() - { - return new MockSession(new Mock(MockBehavior.Strict), new Mock(MockBehavior.Strict), - new Mock(MockBehavior.Strict)); - } - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/SessionReflectionTests.cs b/tests/LocalStack.Client.Tests/SessionReflectionTests.cs deleted file mode 100644 index ac2928e..0000000 --- a/tests/LocalStack.Client.Tests/SessionReflectionTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Amazon.Runtime; -using Amazon.Runtime.Internal; - -using LocalStack.Client.Tests.Mocks.MockServiceClients; -using LocalStack.Client.Utils; - -using System; - -using Xunit; - -namespace LocalStack.Client.Tests -{ - public class SessionReflectionTests - { - [Fact] - public void ExtractServiceMetadata_Should_Throw_InvalidOperationException_If_Given_Generic_Service_Client_Type_Has_Not_Service_Metadata_Field() - { - var sessionReflection = new SessionReflection(); - - Assert.Throws(() => sessionReflection.ExtractServiceMetadata()); - } - - [Fact] - public void ExtractServiceMetadata_Should_Return_Extracted_ServiceMetadata() - { - var sessionReflection = new SessionReflection(); - - IServiceMetadata serviceMetadata = sessionReflection.ExtractServiceMetadata(); - - Assert.NotNull(serviceMetadata); - Assert.Equal(MockServiceMetadata.MockServiceId, serviceMetadata.ServiceId); - } - - [Fact] - public void CreateClientConfig_Should_Create_ClientConfig_By_Given_Generic_Service_Client_Type() - { - var sessionReflection = new SessionReflection(); - - ClientConfig clientConfig = sessionReflection.CreateClientConfig(); - - Assert.NotNull(clientConfig); - Assert.IsType(clientConfig); - } - - [Fact] - public void SetForcePathStyle_Should_Return_False_If_Given_ClientConfig_Does_Not_Have_ForcePathStyle() - { - var sessionReflection = new SessionReflection(); - var clientConfig = new MockClientConfig(); - - bool set = sessionReflection.SetForcePathStyle(clientConfig, true); - - Assert.False(set); - } - - [Fact] - public void SetForcePathStyle_Should_Set_ForcePathStyle_Of_ClientConfig_If_It_Exists() - { - var sessionReflection = new SessionReflection(); - var clientConfig = new MockClientConfigWithForcePathStyle(); - - Assert.False(clientConfig.ForcePathStyle); - - bool set = sessionReflection.SetForcePathStyle(clientConfig, true); - - Assert.True(set); - Assert.True(clientConfig.ForcePathStyle); - } - } -} \ No newline at end of file diff --git a/tests/LocalStack.Client.Tests/SessionTests.cs b/tests/LocalStack.Client.Tests/SessionTests.cs deleted file mode 100644 index 2d31180..0000000 --- a/tests/LocalStack.Client.Tests/SessionTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Amazon.Runtime; -using Amazon.Runtime.Internal; - -using LocalStack.Client.Tests.Mocks; -using LocalStack.Client.Tests.Mocks.MockServiceClients; - -using Moq; - -using System; - -using Xunit; - -namespace LocalStack.Client.Tests -{ - public class SessionTests - { - [Fact] - public void CreateClient_Should_Throw_InvalidOperationException_If_Given_ServiceId_Is_Not_Supported() - { - MockSession mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); - - mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata()).Returns(() => mockServiceMetadata); - mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => null); - - Assert.Throws(() => mockSession.CreateClient()); - - mockSession.ConfigMock.Verify(config => config.GetAwsServiceEndpoint(It.Is(serviceId => serviceId == mockServiceMetadata.ServiceId)), Times.Once); - } - - [Fact] - public void CreateClient_Should_Create_SessionAWSCredentials_With_AwsAccessKeyId_And_AwsAccessKey_And_AwsSessionToken() - { - MockSession mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); - var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); - const string awsAccessKeyId = "AwsAccessKeyId"; - const string awsAccessKey = "AwsAccessKey"; - const string awsSessionToken = "AwsSessionToken"; - - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKeyId).Returns(awsAccessKeyId); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKey).Returns(awsAccessKey); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsSessionToken).Returns(awsSessionToken); - mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata()).Returns(() => mockServiceMetadata); - mockSession.SessionReflectionMock.Setup(reflection => reflection.CreateClientConfig()).Returns(() => mockClientConfig); - mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); - mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); - - var mockAmazonServiceClient = mockSession.CreateClient(); - Assert.NotNull(mockAmazonServiceClient); - - AWSCredentials awsCredentials = mockAmazonServiceClient.AwsCredentials; - Assert.NotNull(awsCredentials); - Assert.IsType(awsCredentials); - - var sessionAwsCredentials = (SessionAWSCredentials) awsCredentials; - ImmutableCredentials immutableCredentials = sessionAwsCredentials.GetCredentials(); - Assert.Equal(awsAccessKeyId, immutableCredentials.AccessKey); - Assert.Equal(awsAccessKey, immutableCredentials.SecretKey); - Assert.Equal(awsSessionToken, immutableCredentials.Token); - } - - [Fact] - public void CreateClient_Should_Create_ClientConfig_With_ServiceURL_UseHttp_ProxyHost_ProxyPort() - { - MockSession mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); - var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); - - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKeyId).Returns("AwsAccessKeyId"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKey).Returns("AwsAccessKey"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsSessionToken).Returns("AwsSessionToken"); - mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata()).Returns(() => mockServiceMetadata); - mockSession.SessionReflectionMock.Setup(reflection => reflection.CreateClientConfig()).Returns(() => mockClientConfig); - mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); - mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); - - var mockAmazonServiceClient = mockSession.CreateClient(); - Assert.NotNull(mockAmazonServiceClient); - - IClientConfig clientConfig = mockAmazonServiceClient.Config; - Assert.Equal(mockAwsServiceEndpoint.ServiceUrl, clientConfig.ServiceURL); - Assert.True(clientConfig.UseHttp); - Assert.Equal(mockAwsServiceEndpoint.Host, clientConfig.ProxyHost); - Assert.Equal(mockAwsServiceEndpoint.Port, clientConfig.ProxyPort); - } - - [Fact] - public void CreateClient_Should_Pass_The_ClientConfig_To_SetForcePathStyle() - { - MockSession mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); - var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); - - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKeyId).Returns("AwsAccessKeyId"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKey).Returns("AwsAccessKey"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsSessionToken).Returns("AwsSessionToken"); - mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata()).Returns(() => mockServiceMetadata); - mockSession.SessionReflectionMock.Setup(reflection => reflection.CreateClientConfig()).Returns(() => mockClientConfig); - mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); - mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); - - mockSession.CreateClient(); - - mockSession.SessionReflectionMock.Verify(reflection => reflection.SetForcePathStyle(It.Is(config => config == mockClientConfig), true), - Times.Once); - } - - [Fact] - public void CreateClient_Should_Create_AmazonServiceClient_By_Given_Generic_Type() - { - MockSession mockSession = MockSession.Create(); - IServiceMetadata mockServiceMetadata = new MockServiceMetadata(); - var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); - var mockClientConfig = new MockClientConfig(); - - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKeyId).Returns("AwsAccessKeyId"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKey).Returns("AwsAccessKey"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsSessionToken).Returns("AwsSessionToken"); - - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKeyId).Returns("AwsAccessKeyId"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsAccessKey).Returns("AwsAccessKey"); - mockSession.SessionOptionsMock.SetupGet(options => options.AwsSessionToken).Returns("AwsSessionToken"); - mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata()).Returns(() => mockServiceMetadata); - mockSession.SessionReflectionMock.Setup(reflection => reflection.CreateClientConfig()).Returns(() => mockClientConfig); - mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); - mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); - - var mockAmazonServiceClient = mockSession.CreateClient(); - Assert.NotNull(mockAmazonServiceClient); - - mockSession.ConfigMock.Verify(config => config.GetAwsServiceEndpoint(It.Is(serviceId => serviceId == mockServiceMetadata.ServiceId)), Times.Once); - mockSession.SessionReflectionMock.Verify(reflection => reflection.ExtractServiceMetadata(), Times.Once); - mockSession.SessionReflectionMock.Verify(reflection => reflection.CreateClientConfig(), 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/SessionLocalStackTests.cs b/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs new file mode 100644 index 0000000..b28ba87 --- /dev/null +++ b/tests/LocalStack.Client.Tests/SessionTests/SessionLocalStackTests.cs @@ -0,0 +1,502 @@ +#pragma warning disable CA2263 // Prefer generic overload when type is known + +namespace LocalStack.Client.Tests.SessionTests; + +public class SessionLocalStackTests +{ + [Fact] + public void CreateClientByImplementation_Should_Throw_NotSupportedClientException_If_Given_ServiceId_Is_Not_Supported() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + + mockSession.SessionOptionsMock.SetupDefault(); + mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockServiceMetadata); + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => null); + + Assert.Throws(() => mockSession.CreateClientByImplementation()); + + mockSession.ConfigMock.Verify(config => config.GetAwsServiceEndpoint(It.Is(serviceId => serviceId == mockServiceMetadata.ServiceId)), Times.Once); + } + + [Fact] + public void CreateClientByImplementation_Should_Throw_MisconfiguredClientException_If_Given_RegionName_Property_Of_SessionOptions_IsNullOrEmpty_And_Given_UseServiceUrl_Is_False() + { + var mockSession = MockSession.Create(); + + mockSession.SessionOptionsMock.SetupGet(options => options.RegionName).Returns((string)null!); + Assert.Throws(() => mockSession.CreateClientByImplementation(false)); + + mockSession.SessionOptionsMock.SetupGet(options => options.RegionName).Returns(string.Empty); + Assert.Throws(() => mockSession.CreateClientByImplementation(false)); + } + + [Fact] + public void CreateClientByImplementation_Should_Create_SessionAWSCredentials_With_AwsAccessKeyId_And_AwsAccessKey_And_AwsSessionToken() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + (string awsAccessKeyId, string awsAccessKey, string awsSessionToken, _) = mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + var mockAmazonServiceClient = mockSession.CreateClientByImplementation(); + Assert.NotNull(mockAmazonServiceClient); + + AWSCredentials awsCredentials = mockAmazonServiceClient.AwsCredentials; + Assert.NotNull(awsCredentials); + Assert.IsType(awsCredentials); + + var sessionAwsCredentials = (SessionAWSCredentials)awsCredentials; + ImmutableCredentials immutableCredentials = sessionAwsCredentials.GetCredentials(); + + Assert.Equal(awsAccessKeyId, immutableCredentials.AccessKey); + Assert.Equal(awsAccessKey, immutableCredentials.SecretKey); + Assert.Equal(awsSessionToken, immutableCredentials.Token); + } + + [Theory, + InlineData(true), + InlineData(false)] + public void CreateClientByImplementation_Should_Create_ClientConfig_With_UseHttp_Set_Bey_ConfigOptions_UseSsl(bool useSsl) + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions(useSsl: useSsl)); + + var mockAmazonServiceClient = mockSession.CreateClientByImplementation(); + Assert.NotNull(mockAmazonServiceClient); + + IClientConfig clientConfig = mockAmazonServiceClient.Config; + + Assert.Equal(useSsl, !clientConfig.UseHttp); + + mockSession.ConfigMock.Verify(config => config.GetConfigOptions(), Times.Once); + } + + [Fact] + public void CreateClientByImplementation_Should_Create_ClientConfig_With_UseHttp_And_ProxyHost_And_ProxyPort_By_ServiceEndpoint_Configuration() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + var configOptions = new ConfigOptions(); + + mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => configOptions); + + var mockAmazonServiceClient = mockSession.CreateClientByImplementation(); + Assert.NotNull(mockAmazonServiceClient); + + IClientConfig clientConfig = mockAmazonServiceClient.Config; + + Assert.Equal(configOptions.UseSsl, !clientConfig.UseHttp); + Assert.Equal(mockAwsServiceEndpoint.Host, clientConfig.ProxyHost); + Assert.Equal(mockAwsServiceEndpoint.Port, clientConfig.ProxyPort); + } + + [Theory, + InlineData("eu-central-1"), + InlineData("us-west-1"), + InlineData("af-south-1"), + InlineData("ap-southeast-1"), + InlineData("ca-central-1"), + InlineData("eu-west-2"), + InlineData("sa-east-1")] + public void CreateClientByImplementation_Should_Set_RegionEndpoint_By_RegionName_Property_Of_SessionOptions_And_ServiceUrl_To_Null_If_RegionName_IsNotNull_Or_Empty(string systemName) + { + var mockSession = MockSession.Create(); + + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + var mockAmazonServiceClient = mockSession.CreateClientByImplementation(); + + Assert.NotNull(mockAmazonServiceClient); + Assert.Null(mockAmazonServiceClient.Config.ServiceURL); + Assert.Equal(RegionEndpoint.GetBySystemName(regionName), mockAmazonServiceClient.Config.RegionEndpoint); + } + + [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) + { + var mockSession = MockSession.Create(); + + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + var mockAmazonServiceClient = mockSession.CreateClientByImplementation(useServiceUrl: true); + + Assert.NotNull(mockAmazonServiceClient); + Assert.Null(mockAmazonServiceClient.Config.RegionEndpoint); + Assert.Equal(mockAwsServiceEndpoint.ServiceUrl.AbsoluteUri, mockAmazonServiceClient.Config.ServiceURL); + } + + [Fact] + public void CreateClientByImplementation_Should_Pass_The_ClientConfig_To_SetForcePathStyle() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + mockSession.CreateClientByImplementation(); + + mockSession.SessionReflectionMock.Verify(reflection => reflection.SetForcePathStyle(It.Is(config => config == mockClientConfig), true), Times.Once); + } + + [Theory, + InlineData(false), + InlineData(true)] + 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(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + var mockAmazonServiceClient = mockSession.CreateClientByImplementation(useServiceUrl); + + Assert.NotNull(mockAmazonServiceClient); + + if (useServiceUrl) + { + Assert.Null(mockAmazonServiceClient.Config.RegionEndpoint); + Assert.NotNull(mockAmazonServiceClient.Config.ServiceURL); + Assert.Equal(mockAwsServiceEndpoint.ServiceUrl.AbsoluteUri, mockAmazonServiceClient.Config.ServiceURL); + } + else + { + Assert.Null(mockAmazonServiceClient.Config.ServiceURL); + Assert.NotNull(mockAmazonServiceClient.Config.RegionEndpoint); + Assert.Equal(RegionEndpoint.GetBySystemName(regionName), mockAmazonServiceClient.Config.RegionEndpoint); + } + + mockSession.ConfigMock.Verify(config => config.GetAwsServiceEndpoint(It.Is(serviceId => serviceId == mockServiceMetadata.ServiceId)), Times.Once); + mockSession.SessionReflectionMock.Verify(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient))), Times.Once); + 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); + } + + [Fact] + public void CreateClientByInterface_Should_Throw_AmazonClientException_If_Given_Generic_AmazonService_Could_Not_Found_In_Aws_Extension_Assembly() + { + var mockSession = MockSession.Create(); + + Assert.Throws(() => mockSession.CreateClientByInterface(typeof(MockAmazonServiceClient))); + } + + [Fact] + public void CreateClientByInterface_Should_Throw_NotSupportedClientException_If_Given_ServiceId_Is_Not_Supported() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + + mockSession.SessionOptionsMock.SetupDefault(); + mockSession.SessionReflectionMock.Setup(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient)))).Returns(() => mockServiceMetadata); + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => null); + + Assert.Throws(() => mockSession.CreateClientByInterface()); + + mockSession.ConfigMock.Verify(config => config.GetAwsServiceEndpoint(It.Is(serviceId => serviceId == mockServiceMetadata.ServiceId)), Times.Once); + } + + [Fact] + public void CreateClientByInterface_Should_Throw_MisconfiguredClientException_If_Given_RegionName_Property_Of_SessionOptions_IsNullOrEmpty_And_Given_UseServiceUrl_Is_False() + { + var mockSession = MockSession.Create(); + + mockSession.SessionOptionsMock.SetupGet(options => options.RegionName).Returns(default(string)!); + Assert.Throws(() => mockSession.CreateClientByInterface(false)); + + mockSession.SessionOptionsMock.SetupGet(options => options.RegionName).Returns(string.Empty); + Assert.Throws(() => mockSession.CreateClientByInterface(false)); + } + + [Theory, + InlineData(true), + InlineData(false)] + public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_Set_Bey_ConfigOptions_UseSsl(bool useSsl) + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions(useSsl: useSsl)); + + var mockAmazonServiceClient = mockSession.CreateClientByInterface() as MockAmazonServiceClient; + Assert.NotNull(mockAmazonServiceClient); + + IClientConfig clientConfig = mockAmazonServiceClient.Config; + + Assert.Equal(useSsl, !clientConfig.UseHttp); + + mockSession.ConfigMock.Verify(config => config.GetConfigOptions(), Times.Once); + } + + [Fact] + public void CreateClientByInterface_Should_Create_SessionAWSCredentials_With_AwsAccessKeyId_And_AwsAccessKey_And_AwsSessionToken() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + (string awsAccessKeyId, string awsAccessKey, string awsSessionToken, _) = mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + var mockAmazonServiceClient = mockSession.CreateClientByInterface() as MockAmazonServiceClient; + Assert.NotNull(mockAmazonServiceClient); + + AWSCredentials awsCredentials = mockAmazonServiceClient.AwsCredentials; + Assert.NotNull(awsCredentials); + Assert.IsType(awsCredentials); + + var sessionAwsCredentials = (SessionAWSCredentials)awsCredentials; + ImmutableCredentials immutableCredentials = sessionAwsCredentials.GetCredentials(); + + Assert.Equal(awsAccessKeyId, immutableCredentials.AccessKey); + Assert.Equal(awsAccessKey, immutableCredentials.SecretKey); + Assert.Equal(awsSessionToken, immutableCredentials.Token); + } + + [Fact] + public void CreateClientByInterface_Should_Create_ClientConfig_With_UseHttp_And_ProxyHost_And_ProxyPort_By_ServiceEndpoint_Configuration() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + var configOptions = new ConfigOptions(); + + mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => configOptions); + + var mockAmazonServiceClient = mockSession.CreateClientByInterface() as MockAmazonServiceClient; + Assert.NotNull(mockAmazonServiceClient); + + IClientConfig clientConfig = mockAmazonServiceClient.Config; + + Assert.Equal(configOptions.UseSsl, !clientConfig.UseHttp); + Assert.Equal(mockAwsServiceEndpoint.Host, clientConfig.ProxyHost); + Assert.Equal(mockAwsServiceEndpoint.Port, clientConfig.ProxyPort); + + mockSession.ConfigMock.Verify(config => config.GetConfigOptions(), Times.Once); + } + + [Theory, + InlineData("eu-central-1"), + InlineData("us-west-1"), + InlineData("af-south-1"), + InlineData("ap-southeast-1"), + InlineData("ca-central-1"), + InlineData("eu-west-2"), + InlineData("sa-east-1")] + public void CreateClientByInterface_Should_Set_RegionEndpoint_By_RegionName_Property_Of_SessionOptions_And_ServiceUrl_To_Null_If_RegionName_IsNotNull_Or_Empty(string systemName) + { + var mockSession = MockSession.Create(); + + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(regionName: systemName); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + var mockAmazonServiceClient = mockSession.CreateClientByInterface() as MockAmazonServiceClient; + + Assert.NotNull(mockAmazonServiceClient); + Assert.Null(mockAmazonServiceClient.Config.ServiceURL); + Assert.Equal(RegionEndpoint.GetBySystemName(regionName), mockAmazonServiceClient.Config.RegionEndpoint); + } + + + [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) + { + var mockSession = MockSession.Create(); + + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + var mockAmazonServiceClient = mockSession.CreateClientByInterface(useServiceUrl: true) as MockAmazonServiceClient; + + Assert.NotNull(mockAmazonServiceClient); + Assert.Null(mockAmazonServiceClient.Config.RegionEndpoint); + Assert.Equal(mockAwsServiceEndpoint.ServiceUrl.AbsoluteUri, mockAmazonServiceClient.Config.ServiceURL); + } + + [Fact] + public void CreateClientByInterface_Should_Pass_The_ClientConfig_To_SetForcePathStyle() + { + var mockSession = MockSession.Create(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => new ConfigOptions()); + + mockSession.CreateClientByInterface(); + + mockSession.SessionReflectionMock.Verify(reflection => reflection.SetForcePathStyle(It.Is(config => config == mockClientConfig), true), Times.Once); + mockSession.ConfigMock.Verify(config => config.GetConfigOptions(), Times.Once); + } + + [Theory, + InlineData(false), + InlineData(true)] + 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(); + var mockServiceMetadata = new MockServiceMetadata(); + var mockAwsServiceEndpoint = new MockAwsServiceEndpoint(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + var configOptions = new ConfigOptions(); + + (_, _, _, string regionName) = mockSession.SessionOptionsMock.SetupDefault(); + + 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); + mockSession.SessionReflectionMock.Setup(reflection => reflection.SetForcePathStyle(mockClientConfig, true)).Returns(() => true); + + mockSession.ConfigMock.Setup(config => config.GetAwsServiceEndpoint(It.IsAny())).Returns(() => mockAwsServiceEndpoint); + mockSession.ConfigMock.Setup(config => config.GetConfigOptions()).Returns(() => configOptions); + + var mockAmazonServiceClient = mockSession.CreateClientByInterface(useServiceUrl) as MockAmazonServiceClient; + + Assert.NotNull(mockAmazonServiceClient); + + if (useServiceUrl) + { + Assert.Null(mockAmazonServiceClient.Config.RegionEndpoint); + Assert.NotNull(mockAmazonServiceClient.Config.ServiceURL); + Assert.Equal(mockAwsServiceEndpoint.ServiceUrl.AbsoluteUri, mockAmazonServiceClient.Config.ServiceURL); + } + else + { + Assert.Null(mockAmazonServiceClient.Config.ServiceURL); + Assert.NotNull(mockAmazonServiceClient.Config.RegionEndpoint); + Assert.Equal(RegionEndpoint.GetBySystemName(regionName), mockAmazonServiceClient.Config.RegionEndpoint); + } + + mockSession.ConfigMock.Verify(config => config.GetAwsServiceEndpoint(It.Is(serviceId => serviceId == mockServiceMetadata.ServiceId)), Times.Once); + mockSession.ConfigMock.Verify(config => config.GetConfigOptions(), Times.Once); + mockSession.SessionReflectionMock.Verify(reflection => reflection.ExtractServiceMetadata(It.Is(type => type == typeof(MockAmazonServiceClient))), Times.Once); + 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/SessionOptionsTests.cs b/tests/LocalStack.Client.Tests/SessionTests/SessionOptionsTests.cs new file mode 100644 index 0000000..12b768c --- /dev/null +++ b/tests/LocalStack.Client.Tests/SessionTests/SessionOptionsTests.cs @@ -0,0 +1,82 @@ +namespace LocalStack.Client.Tests.SessionTests; + +public class SessionOptionsTests +{ + [Fact] + public void SessionOptions_Should_Created_With_Default_Parameters_If_It_Created_By_Default_Constructor() + { + var sessionOptions = new SessionOptions(); + + Assert.Equal(Constants.AwsAccessKeyId, sessionOptions.AwsAccessKeyId); + Assert.Equal(Constants.AwsAccessKey, sessionOptions.AwsAccessKey); + Assert.Equal(Constants.AwsSessionToken, sessionOptions.AwsSessionToken); + Assert.Equal(Constants.RegionName, sessionOptions.RegionName); + } + + [Fact] + public void SessionOptions_Should_Created_With_Default_Parameters_If_It_Created_By_Parameterized_Constructor_And_Non_Of_The_Parameters_Has_Set() + { + ConstructorInfo? constructor = typeof(SessionOptions).GetConstructor(new[] { typeof(string), typeof(string), typeof(string), typeof(string) }); + + Assert.NotNull(constructor); + + var sessionOptions = (SessionOptions)constructor.Invoke(new[] { Type.Missing, Type.Missing, Type.Missing, Type.Missing }); + + Assert.Equal(Constants.AwsAccessKeyId, sessionOptions.AwsAccessKeyId); + Assert.Equal(Constants.AwsAccessKey, sessionOptions.AwsAccessKey); + Assert.Equal(Constants.AwsSessionToken, sessionOptions.AwsSessionToken); + Assert.Equal(Constants.RegionName, sessionOptions.RegionName); + } + + [Fact] + public void AwsAccessKeyId_Property_Of_ConfigOptions_Should_Equal_To_Given_AwsAccessKeyId_Constructor_Parameter() + { + const string awsAccessKeyId = "myAwsAccessKeyId"; + + var sessionOptions = new SessionOptions(awsAccessKeyId); + + Assert.Equal(awsAccessKeyId, sessionOptions.AwsAccessKeyId); + Assert.Equal(Constants.AwsAccessKey, sessionOptions.AwsAccessKey); + Assert.Equal(Constants.AwsSessionToken, sessionOptions.AwsSessionToken); + Assert.Equal(Constants.RegionName, sessionOptions.RegionName); + } + + [Fact] + public void AwsAccessKey_Property_Of_ConfigOptions_Should_Equal_To_Given_AwsAccessKey_Constructor_Parameter() + { + const string awsAccessKey = "myAwsAccessKey"; + + var sessionOptions = new SessionOptions(awsAccessKey: awsAccessKey); + + Assert.Equal(Constants.AwsAccessKeyId, sessionOptions.AwsAccessKeyId); + Assert.Equal(awsAccessKey, sessionOptions.AwsAccessKey); + Assert.Equal(Constants.AwsSessionToken, sessionOptions.AwsSessionToken); + Assert.Equal(Constants.RegionName, sessionOptions.RegionName); + } + + [Fact] + public void AwsSessionToken_Property_Of_ConfigOptions_Should_Equal_To_Given_AwsSessionToken_Constructor_Parameter() + { + const string awsSessionToken = "myAwsSessionToken"; + + var sessionOptions = new SessionOptions(awsSessionToken: awsSessionToken); + + Assert.Equal(Constants.AwsAccessKeyId, sessionOptions.AwsAccessKeyId); + Assert.Equal(Constants.AwsAccessKey, sessionOptions.AwsAccessKey); + Assert.Equal(awsSessionToken, sessionOptions.AwsSessionToken); + Assert.Equal(Constants.RegionName, sessionOptions.RegionName); + } + + [Fact] + public void RegionName_Property_Of_ConfigOptions_Should_Equal_To_Given_RegionName_Constructor_Parameter() + { + const string regionName = "myRegionName"; + + var sessionOptions = new SessionOptions(regionName: regionName); + + Assert.Equal(Constants.AwsAccessKeyId, sessionOptions.AwsAccessKeyId); + Assert.Equal(Constants.AwsAccessKey, sessionOptions.AwsAccessKey); + Assert.Equal(Constants.AwsSessionToken, sessionOptions.AwsSessionToken); + Assert.Equal(regionName, sessionOptions.RegionName); + } +} diff --git a/tests/LocalStack.Client.Tests/SessionTests/SessionReflectionTests.cs b/tests/LocalStack.Client.Tests/SessionTests/SessionReflectionTests.cs new file mode 100644 index 0000000..90778e7 --- /dev/null +++ b/tests/LocalStack.Client.Tests/SessionTests/SessionReflectionTests.cs @@ -0,0 +1,80 @@ +namespace LocalStack.Client.Tests.SessionTests; + +public class SessionReflectionTests +{ + [Fact] + public void ExtractServiceMetadata_Should_Throw_InvalidOperationException_If_Given_Generic_Service_Client_Type_Has_Not_Service_Metadata_Field() + { + var sessionReflection = new SessionReflection(); + + Assert.Throws(() => sessionReflection.ExtractServiceMetadata()); + } + + [Fact] + public void ExtractServiceMetadata_Should_Return_Extracted_ServiceMetadata() + { + var sessionReflection = new SessionReflection(); + + IServiceMetadata serviceMetadata = sessionReflection.ExtractServiceMetadata(); + + Assert.NotNull(serviceMetadata); + Assert.Equal(MockServiceMetadata.MockServiceId, serviceMetadata.ServiceId); + } + + [Fact] + public void CreateClientConfig_Should_Create_ClientConfig_By_Given_Generic_Service_Client_Type() + { + var sessionReflection = new SessionReflection(); + + ClientConfig clientConfig = sessionReflection.CreateClientConfig(); + + Assert.NotNull(clientConfig); + Assert.IsType(clientConfig); + } + + [Fact] + public void SetForcePathStyle_Should_Return_False_If_Given_ClientConfig_Does_Not_Have_ForcePathStyle() + { + var sessionReflection = new SessionReflection(); + var mockClientConfig = MockClientConfig.CreateDefaultMockClientConfig(); + + bool set = sessionReflection.SetForcePathStyle(mockClientConfig, true); + + Assert.False(set); + } + + [Fact] + public void SetForcePathStyle_Should_Set_ForcePathStyle_Of_ClientConfig_If_It_Exists() + { + var sessionReflection = new SessionReflection(); + var clientConfig = MockClientConfigWithForcePathStyle.CreateDefaultMockClientConfigWithForcePathStyle(); + + Assert.False(clientConfig.ForcePathStyle); + + bool set = sessionReflection.SetForcePathStyle(clientConfig, true); + + Assert.True(set); + Assert.True(clientConfig.ForcePathStyle); + } + + [Theory, + InlineData("eu-central-1"), + InlineData("us-west-1"), + InlineData("af-south-1"), + InlineData("ap-southeast-1"), + InlineData("ca-central-1"), + InlineData("eu-west-2"), + InlineData("sa-east-1")] + public void SetClientRegion_Should_Set_RegionEndpoint_Of_The_Given_Client_By_System_Name(string systemName) + { + var sessionReflection = new SessionReflection(); + using var mockAmazonServiceClient = new MockAmazonServiceClient(); + + Assert.Null(mockAmazonServiceClient.Config.RegionEndpoint); + + sessionReflection.SetClientRegion(mockAmazonServiceClient, systemName); + + 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/GlobalUsings.cs b/tests/common/LocalStack.Tests.Common/GlobalUsings.cs new file mode 100644 index 0000000..d9555d1 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/GlobalUsings.cs @@ -0,0 +1,15 @@ +// Global using directives + +global using System; +global using System.Collections.Generic; + +global using Amazon.Runtime; +global using Amazon.Runtime.Internal; +global using Amazon.Runtime.Internal.Auth; +global using Amazon.Util.Internal; + +global using LocalStack.Client; +global using LocalStack.Client.Contracts; +global using LocalStack.Client.Models; + +global using Moq; \ 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 new file mode 100644 index 0000000..d7d39e1 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/LocalStack.Tests.Common.csproj @@ -0,0 +1,24 @@ + + + + net472;net8.0;net9.0 + $(NoWarn);CA1707;MA0006;CA1510 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/Extensions.cs b/tests/common/LocalStack.Tests.Common/Mocks/Extensions.cs new file mode 100644 index 0000000..7081efe --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/Extensions.cs @@ -0,0 +1,21 @@ +namespace LocalStack.Tests.Common.Mocks; + +public static class Extensions +{ + public static (string awsAccessKeyId, string awsAccessKey, string awsSessionToken, string regionName) SetupDefault( + this Mock mock, string awsAccessKeyId = "AwsAccessKeyId", string awsAccessKey = "AwsAccessKey", string awsSessionToken = "AwsSessionToken", + string regionName = "eu-central-1") + { + if (mock == null) + { + throw new ArgumentNullException(nameof(mock)); + } + + mock.SetupGet(options => options.AwsAccessKeyId).Returns(awsAccessKeyId); + mock.SetupGet(options => options.AwsAccessKey).Returns(awsAccessKey); + mock.SetupGet(options => options.AwsSessionToken).Returns(awsSessionToken); + mock.SetupGet(options => options.RegionName).Returns(regionName); + + return (awsAccessKeyId, awsAccessKey, awsSessionToken, regionName); + } +} \ 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 new file mode 100644 index 0000000..d5c2a9c --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonService.cs @@ -0,0 +1,15 @@ +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +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 new file mode 100644 index 0000000..c1dbf29 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/IMockAmazonServiceWithServiceMetadata.cs @@ -0,0 +1,15 @@ +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +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 new file mode 100644 index 0000000..01b2e73 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceClient.cs @@ -0,0 +1,38 @@ +#pragma warning disable S2325,CA1822 + +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public class MockAmazonServiceClient : AmazonServiceClient, IMockAmazonService +{ + public MockAmazonServiceClient() : base(new MockCredentials(), new MockClientConfig(new MockConfigurationProvider())) + { + } + + public MockAmazonServiceClient(AWSCredentials credentials, MockClientConfig clientConfig) : base(credentials, clientConfig) + { + } + + public MockAmazonServiceClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, ClientConfig clientConfig) : base( + awsAccessKeyId, awsSecretAccessKey, awsSessionToken, clientConfig) + { + } + + public MockAmazonServiceClient(string awsAccessKeyId, string awsSecretAccessKey, ClientConfig clientConfig) : base( + awsAccessKeyId, awsSecretAccessKey, clientConfig) + { + } + + public AWSCredentials AwsCredentials => Config.DefaultAWSCredentials; + +#if NET8_0_OR_GREATER + public static ClientConfig CreateDefaultClientConfig() + { + return MockClientConfig.CreateDefaultMockClientConfig(); + } + + public static IAmazonService CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) + { + 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 new file mode 100644 index 0000000..05d93b0 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAmazonServiceWithServiceMetadataClient.cs @@ -0,0 +1,39 @@ +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(DefaultAWSCredentialsIdentityResolver.GetCredentials(), MockClientConfig.CreateDefaultMockClientConfig()) + { + } + + public MockAmazonServiceWithServiceMetadataClient(AWSCredentials credentials, MockClientConfig clientConfig) : base(credentials, clientConfig) + { + } + + public MockAmazonServiceWithServiceMetadataClient(string awsAccessKeyId, string awsSecretAccessKey, string awsSessionToken, ClientConfig clientConfig) : base( + awsAccessKeyId, awsSecretAccessKey, awsSessionToken, clientConfig) + { + } + + public MockAmazonServiceWithServiceMetadataClient(string awsAccessKeyId, string awsSecretAccessKey, ClientConfig clientConfig) : base( + awsAccessKeyId, awsSecretAccessKey, clientConfig) + { + } + +#if NET8_0_OR_GREATER + public static ClientConfig CreateDefaultClientConfig() + { + return MockClientConfig.CreateDefaultMockClientConfig(); + } + + public static IAmazonService CreateDefaultServiceClient(AWSCredentials awsCredentials, ClientConfig clientConfig) + { + return new MockAmazonServiceWithServiceMetadataClient(awsCredentials, MockClientConfig.CreateDefaultMockClientConfig()); + } +#endif +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAwsServiceEndpoint.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAwsServiceEndpoint.cs new file mode 100644 index 0000000..ada0af3 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockAwsServiceEndpoint.cs @@ -0,0 +1,4 @@ +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public record MockAwsServiceEndpoint() : AwsServiceEndpoint(MockServiceMetadata.MockServiceId, "mockService", Client.Enums.AwsService.ApiGateway, 1234, "localhost", + new Uri("http://localhost:1234/")); \ 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 new file mode 100644 index 0000000..d19c23e --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfig.cs @@ -0,0 +1,28 @@ +using Amazon.Runtime.Endpoints; + +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public class MockClientConfig : ClientConfig, IClientConfig +{ + public MockClientConfig() : this(new MockConfigurationProvider()) + { + } + + public MockClientConfig(IDefaultConfigurationProvider configurationProvider) : base(configurationProvider) + { + ServiceURL = "http://localhost"; + } + + public override string ServiceVersion => "1.0.0.0"; + + 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 new file mode 100644 index 0000000..688b506 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockClientConfigWithForcePathStyle.cs @@ -0,0 +1,13 @@ +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +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/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockCredentials.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockCredentials.cs new file mode 100644 index 0000000..ca36539 --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockCredentials.cs @@ -0,0 +1,9 @@ +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +internal sealed class MockCredentials : BasicAWSCredentials +{ + public MockCredentials() + : base("testkey", "testsecret") + { + } +} diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockServiceMetadata.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockServiceMetadata.cs new file mode 100644 index 0000000..7e6b36b --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockServiceClients/MockServiceMetadata.cs @@ -0,0 +1,10 @@ +namespace LocalStack.Tests.Common.Mocks.MockServiceClients; + +public class MockServiceMetadata : IServiceMetadata +{ + public const string MockServiceId = "Mock Amazon Service"; + + public string ServiceId => MockServiceId; + + public IDictionary OperationNameMapping { get; } = new Dictionary(StringComparer.Ordinal); +} \ No newline at end of file diff --git a/tests/common/LocalStack.Tests.Common/Mocks/MockSession.cs b/tests/common/LocalStack.Tests.Common/Mocks/MockSession.cs new file mode 100644 index 0000000..7fdf8ea --- /dev/null +++ b/tests/common/LocalStack.Tests.Common/Mocks/MockSession.cs @@ -0,0 +1,23 @@ +namespace LocalStack.Tests.Common.Mocks; + +public class MockSession : Session +{ + private MockSession(Mock sessionOptionsMock, Mock configMock, Mock sessionReflectionMock) : base( + sessionOptionsMock.Object, configMock.Object, sessionReflectionMock.Object) + { + SessionOptionsMock = sessionOptionsMock; + ConfigMock = configMock; + SessionReflectionMock = sessionReflectionMock; + } + + public Mock SessionOptionsMock { get; } + + public Mock ConfigMock { get; } + + public Mock SessionReflectionMock { get; } + + public static MockSession Create() + { + return new MockSession(new Mock(MockBehavior.Strict), new Mock(MockBehavior.Strict), new Mock(MockBehavior.Strict)); + } +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/GlobalUsings.cs b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/GlobalUsings.cs new file mode 100644 index 0000000..5f68fda --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/GlobalUsings.cs @@ -0,0 +1,18 @@ +// Global using directives + +global using System; +global using System.IO; +global using System.Threading.Tasks; + +global using Amazon.S3; +global using Amazon.S3.Model; +global using Amazon.S3.Transfer; + +global using LocalStack.Client; +global using LocalStack.Client.Contracts; +global using LocalStack.Client.Options; +global using LocalStack.Client.Utils; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Options; \ 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 dabe085..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,25 +1,44 @@  - - Exe - netcoreapp2.1;netcoreapp3.1 - true - latest - + + Exe + net8.0;net9.0 + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 + - - - - + + + + - - - + + + 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 4c0cf66..81d8dfa 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/Program.cs +++ b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/Program.cs @@ -1,83 +1,106 @@ -using Amazon.S3; -using Amazon.S3.Model; -using Amazon.S3.Transfer; -using Amazon.S3.Util; +var collection = new ServiceCollection(); +var builder = new ConfigurationBuilder(); + +builder.SetBasePath(Directory.GetCurrentDirectory()); +builder.AddJsonFile("appsettings.json", true); +builder.AddJsonFile("appsettings.Development.json", true); +builder.AddEnvironmentVariables(); +builder.AddCommandLine(args); + +IConfiguration configuration = builder.Build(); + +collection.Configure(options => configuration.GetSection("LocalStack").Bind(options, c => c.BindNonPublicProperties = true)); +/* + * ==== Default Values ==== + * AwsAccessKeyId: accessKey (It doesn't matter to LocalStack) + * AwsAccessKey: secretKey (It doesn't matter to LocalStack) + * AwsSessionToken: token (It doesn't matter to LocalStack) + * RegionName: us-east-1 + */ +collection.Configure(options => configuration.GetSection("LocalStack") + .GetSection(nameof(LocalStackOptions.Session)) + .Bind(options, c => c.BindNonPublicProperties = true)); +/* + * ==== Default Values ==== + * LocalStackHost: localhost + * UseSsl: false + * UseLegacyPorts: false (Set true if your LocalStack version is 0.11.4 or below) + * EdgePort: 4566 (It doesn't matter if use legacy ports) + */ +collection.Configure(options => configuration.GetSection("LocalStack") + .GetSection(nameof(LocalStackOptions.Config)) + .Bind(options, c => c.BindNonPublicProperties = true)); + + +collection.AddTransient(provider => + { + ConfigOptions options = provider.GetRequiredService>().Value; + + return new Config(options); + }) + .AddSingleton() + .AddSingleton(provider => + { + SessionOptions sessionOptions = provider.GetRequiredService>().Value; + var config = provider.GetRequiredService(); + var sessionReflection = provider.GetRequiredService(); + + return new Session(sessionOptions, config, sessionReflection); + }) + .AddTransient(provider => + { + var session = provider.GetRequiredService(); + + return (IAmazonS3)session.CreateClientByInterface(); + }); + +ServiceProvider serviceProvider = collection.BuildServiceProvider(); + +var amazonS3Client = serviceProvider.GetRequiredService(); + +const string bucketName = "test-bucket-3"; +const string filePath = "SampleData.txt"; +const string key = "SampleData.txt"; + +Console.WriteLine("Press any key to start Sandbox application"); +Console.ReadLine(); + +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); -using LocalStack.Client.Contracts; -using LocalStack.Client.Models; -using LocalStack.Client.Utils; + Console.WriteLine("The bucket {0} created", bucketName); -using Microsoft.Extensions.DependencyInjection; + // Retrieve the bucket location. + string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName); + Console.WriteLine("The bucket's location: {0}", bucketLocation); -using System; -using System.Threading.Tasks; + using var fileTransferUtility = new TransferUtility(s3Client); -namespace LocalStack.Client.Sandbox.DependencyInjection -{ - internal static class Program + Console.WriteLine("Uploading the file {0}...", path); + await fileTransferUtility.UploadAsync(path, bucketName, key); + Console.WriteLine("The file {0} created", path); + } + catch (AmazonS3Exception e) { - private static async Task Main(string[] args) - { - var collection = new ServiceCollection(); - - var awsAccessKeyId = "Key Id"; - var awsAccessKey = "Secret Key"; - var awsSessionToken = "Token"; - var regionName = "us-west-1"; - var localStackHost = "localhost"; - - collection.AddScoped(provider => new SessionOptions(awsAccessKeyId, awsAccessKey, awsSessionToken, regionName)) - .AddScoped(provider => new Config(localStackHost)) - .AddScoped() - .AddScoped(); - - ServiceProvider serviceProvider = collection.BuildServiceProvider(); - var session = serviceProvider.GetRequiredService(); - - var amazonS3Client = session.CreateClient(); - - const string bucketName = "test-bucket-3"; - const string filePath = "SampleData.txt"; - const string key = "SampleData.txt"; - - await CreateBucketAndUploadFileAsync(amazonS3Client, bucketName, filePath, key); - } - - private static async Task CreateBucketAndUploadFileAsync(IAmazonS3 s3Client, string bucketName, string path, string key) - { - try - { - if (!await AmazonS3Util.DoesS3BucketExistV2Async(s3Client, bucketName)) - { - var putBucketRequest = new PutBucketRequest {BucketName = bucketName, UseClientRegion = true}; - - PutBucketResponse putBucketResponse = await s3Client.PutBucketAsync(putBucketRequest); - } - - // Retrieve the bucket location. - string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName); - - var fileTransferUtility = new TransferUtility(s3Client); - - await fileTransferUtility.UploadAsync(path, bucketName, key); - } - catch (AmazonS3Exception e) - { - Console.WriteLine("Error encountered on server. Message:'{0}' when writing an object", e.Message); - } - catch (Exception e) - { - Console.WriteLine("Unknown encountered on server. Message:'{0}' when writing an object", e.Message); - } - } - - private static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) - { - var request = new GetBucketLocationRequest() {BucketName = bucketName}; - GetBucketLocationResponse response = await client.GetBucketLocationAsync(request); - string bucketLocation = response.Location.ToString(); - - return bucketLocation; - } + Console.WriteLine("Error encountered on server. Message:'{0}' when writing an object", e.Message); } + catch (Exception e) + { + Console.WriteLine("Unknown encountered on server. Message:'{0}' when writing an object", e.Message); + } +} + +static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) +{ + var request = new GetBucketLocationRequest() { BucketName = bucketName }; + 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.DependencyInjection/appsettings.Development.json b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/appsettings.Development.json new file mode 100644 index 0000000..9abf55f --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/appsettings.Development.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "SampleS3Service": "Debug" + } + }, + "LocalStack": { + "UseLocalStack": true, + "Session": { + "AwsAccessKeyId": "my-AwsAccessKeyId", + "AwsAccessKey": "my-AwsAccessKey", + "AwsSessionToken": "my-AwsSessionToken", + "RegionName": "eu-central-1" + }, + "Config": { + "LocalStackHost": "localhost", + "UseSsl": false, + "UseLegacyPorts": false, + "EdgePort": 4566 + } + } +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/appsettings.json b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/appsettings.json new file mode 100644 index 0000000..3899b28 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.DependencyInjection/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Error", + "System": "Information", + "Microsoft": "Information" + } + }, + "LocalStack": { + "UseLocalStack" : false + } +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/GlobalUsings.cs b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/GlobalUsings.cs new file mode 100644 index 0000000..cc49ea2 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/GlobalUsings.cs @@ -0,0 +1,17 @@ +global using System; +global using System.IO; +global using System.Reflection; +global using System.Threading; +global using System.Threading.Tasks; + +global using Amazon.S3; +global using Amazon.S3.Model; +global using Amazon.S3.Transfer; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; + +global using LocalStack.Client.Extensions; +global using LocalStack.Client.Sandbox.WithGenericHost; 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 new file mode 100644 index 0000000..a6a6439 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/LocalStack.Client.Sandbox.WithGenericHost.csproj @@ -0,0 +1,40 @@ + + + + Exe + net8.0;net9.0 + latest + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 + + + + + PreserveNewest + + + PreserveNewest + appsettings.json + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000..49609d2 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Program.cs @@ -0,0 +1,39 @@ +Console.WriteLine("Press any key to start Sandbox application"); +Console.ReadLine(); + +Console.WriteLine(Environment.OSVersion.VersionString); +Console.WriteLine(GetNetCoreVersion()); + +await new HostBuilder().ConfigureHostConfiguration(configHost => configHost.AddEnvironmentVariables()) + .ConfigureAppConfiguration((hostContext, builder) => + { + builder.SetBasePath(Directory.GetCurrentDirectory()); + builder.AddJsonFile("appsettings.json", optional: true); + builder.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); + builder.AddEnvironmentVariables(); + builder.AddCommandLine(args); + }) + .ConfigureServices((hostContext, services) => + { + services.AddLogging() + .AddLocalStack(hostContext.Configuration) + .AddAwsService() + .AddHostedService(); + }) + .ConfigureLogging((_, configLogging) => { configLogging.AddConsole(); }) + .UseConsoleLifetime() + .RunConsoleAsync(); + +static string? GetNetCoreVersion() +{ + Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + 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) + { + return assemblyPath[netCoreAppIndex + 1]; + } + + return null; +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Properties/launchSettings.json b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Properties/launchSettings.json new file mode 100644 index 0000000..97cacdb --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "ENVIRONMENT": "Development" + } + }, + "Production": { + "commandName": "Project", + "environmentVariables": { + "ENVIRONMENT": "Production" + } + }, + "WSL 2": { + "commandName": "WSL2", + "environmentVariables": { + "ENVIRONMENT": "Development" + }, + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleData.txt b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleData.txt new file mode 100644 index 0000000..b3cdee7 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleData.txt @@ -0,0 +1,14 @@ +The standard Lorem Ipsum passage, used since the 1500s +"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + +Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC +"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?" + +1914 translation by H. Rackham +"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?" + +Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC +"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat." + +1914 translation by H. Rackham +"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains." \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleS3Service.cs b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleS3Service.cs new file mode 100644 index 0000000..e77f03f --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/SampleS3Service.cs @@ -0,0 +1,69 @@ +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + +namespace LocalStack.Client.Sandbox.WithGenericHost; + +internal sealed class SampleS3Service : IHostedService +{ + private const string BucketName = "localstack-sandbox-with-host"; + private const string FilePath = "SampleData.txt"; + private const string Key = "SampleData.txt"; + + private readonly ILogger _logger; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly IAmazonS3 _amazonS3; + + public SampleS3Service(ILogger logger, IHostApplicationLifetime hostApplicationLifetime, IAmazonS3 amazonS3) + { + _logger = logger; + _hostApplicationLifetime = hostApplicationLifetime; + _amazonS3 = amazonS3; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + var putBucketRequest = new PutBucketRequest { BucketName = BucketName }; + await _amazonS3.PutBucketAsync(putBucketRequest, cancellationToken); + + _logger.LogInformation("The bucket {BucketName} created", BucketName); + + // Retrieve the bucket location. + 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); + _logger.LogInformation("The file {FilePath} created", FilePath); + } + catch (AmazonS3Exception e) + { + _logger.LogError(e, "Error encountered on server. Message:'{EMessage}' when writing an object", e.Message); + } + catch (Exception e) + { + _logger.LogError(e, "Unknown encountered on server. Message:'{EMessage}' when writing an object", e.Message); + } + finally + { + _hostApplicationLifetime.StopApplication(); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Stopping the application"); + return Task.CompletedTask; + } + + private static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) + { + var request = new GetBucketLocationRequest() { BucketName = bucketName }; + 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.WithGenericHost/appsettings.Development.json b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.Development.json new file mode 100644 index 0000000..16c4cd3 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.Development.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information", + "SampleS3Service": "Information" + } + }, + "LocalStack": { + "UseLocalStack": true, + "Session": { + "AwsAccessKeyId": "my-AwsAccessKeyId", + "AwsAccessKey": "my-AwsAccessKey", + "AwsSessionToken": "my-AwsSessionToken", + "RegionName": "eu-central-1" + }, + "Config": { + "LocalStackHost": "localhost", + "UseSsl": false, + "UseLegacyPorts": false, + "EdgePort": 4566 + } + } +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.Production.json b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.Production.json new file mode 100644 index 0000000..ae63812 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.Production.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Error", + "System": "Information", + "Microsoft": "Information", + "SampleS3Service": "Error" + } + }, + "LocalStack": { + "UseLocalStack": false + }, + "AWS": { + "Profile": "deniz-ev", + "Region": "eu-central-1" + } +} diff --git a/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.json b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.json new file mode 100644 index 0000000..a484d36 --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox.WithGenericHost/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Error", + "System": "Information", + "Microsoft": "Information" + } + }, + "LocalStack": { + "UseLocalStack": false + } +} \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Client.Sandbox/GlobalUsings.cs b/tests/sandboxes/LocalStack.Client.Sandbox/GlobalUsings.cs new file mode 100644 index 0000000..a9b075a --- /dev/null +++ b/tests/sandboxes/LocalStack.Client.Sandbox/GlobalUsings.cs @@ -0,0 +1,12 @@ +// Global using directives + +global using System; +global using System.Threading.Tasks; + +global using Amazon.S3; +global using Amazon.S3.Model; +global using Amazon.S3.Transfer; + +global using LocalStack.Client; +global using LocalStack.Client.Contracts; +global using LocalStack.Client.Options; \ 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 7659bb1..a35f787 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj +++ b/tests/sandboxes/LocalStack.Client.Sandbox/LocalStack.Client.Sandbox.csproj @@ -1,24 +1,23 @@  - - Exe - netcoreapp2.1;netcoreapp3.1 - true - latest - + + Exe + net472;net8.0;net9.0 + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 + - - - + + + - - - + + + - - - Always - - + + + Always + + - + \ 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 3464a80..96e4bf2 100644 --- a/tests/sandboxes/LocalStack.Client.Sandbox/Program.cs +++ b/tests/sandboxes/LocalStack.Client.Sandbox/Program.cs @@ -1,71 +1,73 @@ -using Amazon.S3; -using Amazon.S3.Model; -using Amazon.S3.Transfer; -using Amazon.S3.Util; +/* + * ==== Default Values ==== + * AwsAccessKeyId: accessKey (It doesn't matter to LocalStack) + * AwsAccessKey: secretKey (It doesn't matter to LocalStack) + * AwsSessionToken: token (It doesn't matter to LocalStack) + * RegionName: us-east-1 + * ==== Custom Values ==== + * var sessionOptions = new SessionOptions("someAwsAccessKeyId", "someAwsAccessKey", "someAwsSessionToken", "eu-central-"); + */ -using LocalStack.Client.Contracts; +var sessionOptions = new SessionOptions(); -using System; -using System.Threading.Tasks; +/* + * ==== Default Values ==== + * LocalStackHost: localhost + * UseSsl: false + * UseLegacyPorts: false (Set true if your LocalStack version is 0.11.4 or below) + * EdgePort: 4566 (It doesn't matter if use legacy ports) + * ==== Custom Values ==== + * var configOptions = new ConfigOptions("mylocalhost", false, false, 4566); + */ +var configOptions = new ConfigOptions(); -namespace LocalStack.Client.Sandbox -{ - internal class Program - { - private static async Task Main(string[] args) - { - var awsAccessKeyId = "Key Id"; - var awsAccessKey = "Secret Key"; - var awsSessionToken = "Token"; - var regionName = "us-west-1"; - var localStackHost = "localhost"; +ISession session = SessionStandalone.Init().WithSessionOptions(sessionOptions).WithConfigurationOptions(configOptions).Create(); - ISession session = SessionStandalone.Init().WithSessionOptions(awsAccessKeyId, awsAccessKey, awsSessionToken, regionName).WithConfig(localStackHost).Create(); +var amazonS3Client = session.CreateClientByImplementation(); - var amazonS3Client = session.CreateClient(); +const string bucketName = "test-bucket-3"; +const string filePath = "SampleData.txt"; +const string key = "SampleData.txt"; - const string bucketName = "test-bucket-3"; - const string filePath = "SampleData.txt"; - const string key = "SampleData.txt"; +Console.WriteLine("Press any key to start Sandbox application"); +Console.ReadLine(); - await CreateBucketAndUploadFileAsync(amazonS3Client, bucketName, filePath, key); - } +await CreateBucketAndUploadFileAsync(amazonS3Client, bucketName, filePath, key); - private static async Task CreateBucketAndUploadFileAsync(IAmazonS3 s3Client, string bucketName, string path, string key) - { - try - { - if (!await AmazonS3Util.DoesS3BucketExistV2Async(s3Client, bucketName)) - { - var putBucketRequest = new PutBucketRequest {BucketName = bucketName, UseClientRegion = true}; +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); - PutBucketResponse putBucketResponse = await s3Client.PutBucketAsync(putBucketRequest); - } + Console.WriteLine("The bucket {0} created", bucketName); - // Retrieve the bucket location. - string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName); + // Retrieve the bucket location. + string bucketLocation = await FindBucketLocationAsync(s3Client, bucketName); + Console.WriteLine("The bucket's location: {0}", bucketLocation); - var fileTransferUtility = new TransferUtility(s3Client); + using var fileTransferUtility = new TransferUtility(s3Client); - await fileTransferUtility.UploadAsync(path, bucketName, key); - } - catch (AmazonS3Exception e) - { - Console.WriteLine("Error encountered on server. Message:'{0}' when writing an object", e.Message); - } - catch (Exception e) - { - Console.WriteLine("Unknown encountered on server. Message:'{0}' when writing an object", e.Message); - } - } + Console.WriteLine("Uploading the file {0}...", path); + await fileTransferUtility.UploadAsync(path, bucketName, key); + Console.WriteLine("The file {0} created", path); + } + catch (AmazonS3Exception e) + { + Console.WriteLine("Error encountered on server. Message:'{0}' when writing an object", e.Message); + } + catch (Exception e) + { + Console.WriteLine("Unknown encountered on server. Message:'{0}' when writing an object", e.Message); + } +} - private static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) - { - var request = new GetBucketLocationRequest() {BucketName = bucketName}; - GetBucketLocationResponse response = await client.GetBucketLocationAsync(request); - string bucketLocation = response.Location.ToString(); +static async Task FindBucketLocationAsync(IAmazonS3 client, string bucketName) +{ + var request = new GetBucketLocationRequest() { BucketName = bucketName }; + GetBucketLocationResponse response = await client.GetBucketLocationAsync(request); + var bucketLocation = response.Location.ToString(); - return bucketLocation; - } - } + return bucketLocation; } \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Container/GlobalUsings.cs b/tests/sandboxes/LocalStack.Container/GlobalUsings.cs new file mode 100644 index 0000000..b638deb --- /dev/null +++ b/tests/sandboxes/LocalStack.Container/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Global using directives + +global using System; + +global using Testcontainers.LocalStack; \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Container/LocalStack.Container.csproj b/tests/sandboxes/LocalStack.Container/LocalStack.Container.csproj new file mode 100644 index 0000000..8e92cd2 --- /dev/null +++ b/tests/sandboxes/LocalStack.Container/LocalStack.Container.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + latest + $(NoWarn);CS0246;S125;CA1305;CA1031;CA1303;CA1848;MA0004;CA2007 + + + + + + + + \ No newline at end of file diff --git a/tests/sandboxes/LocalStack.Container/Program.cs b/tests/sandboxes/LocalStack.Container/Program.cs new file mode 100644 index 0000000..0359e91 --- /dev/null +++ b/tests/sandboxes/LocalStack.Container/Program.cs @@ -0,0 +1,24 @@ +Console.WriteLine("Press any key to start LocalStack container"); +Console.ReadLine(); + +string containerId = Guid.NewGuid().ToString().ToUpperInvariant(); +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") + .WithEnvironment("LS_LOG", "trace-internal") + .WithPortBinding(4566, 4566) + .WithCleanUp(true); + +LocalStackContainer container = localStackBuilder.Build(); + +Console.WriteLine("Starting LocalStack Container"); +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(); +Console.WriteLine("LocalStack Container stopped"); \ No newline at end of file