diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 713aae6..8bb1ac6 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,12 +1,12 @@ -name: .NET Core +name: .NET on: push: - branches: [ main ] - paths-ignore: - - 'README.md' + paths-ignore: + - '**.md' - '**.jpg' - + - '**.png' + pull_request: branches: [ main ] @@ -15,30 +15,39 @@ jobs: runs-on: ${{ matrix.os }} strategy: - matrix: + matrix: os: [ ubuntu-latest ] steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + - uses: actions/checkout@v4 + - name: Setup .NET 8.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Setup .NET 9.0 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 3.1.301 + dotnet-version: 9.0.x - name: Install dependencies run: dotnet restore - name: Build run: dotnet build --configuration Release --no-restore - name: Test run: dotnet test --no-restore --verbosity normal - - name: Pack + - name: Pack SmokeMe if: matrix.os == 'ubuntu-latest' - run: | - arrTag=(${GITHUB_REF//\// }) - VERSION="${arrTag[2]}" - VERSION="${VERSION//v}" - dotnet pack --configuration Release - - name: Upload Artifact + run: dotnet pack SmokeMe/SmokeMe.csproj --configuration Release --no-build + - name: Pack SmokeMe.AspNetCore + if: matrix.os == 'ubuntu-latest' + run: dotnet pack SmokeMe.AspNetCore/SmokeMe.AspNetCore.csproj --configuration Release --no-build + - name: Upload SmokeMe NuGet + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: smokeme-nupkg + path: SmokeMe/bin/Release/*.nupkg + - name: Upload SmokeMe.AspNetCore NuGet if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: nupkg - path: /home/runner/work/SmokeMe/SmokeMe/SmokeMe/bin/Release/*.nupkg + name: smokeme-aspnetcore-nupkg + path: SmokeMe.AspNetCore/bin/Release/*.nupkg diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..23e761d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**SmokeMe** is a convention-based .NET library (v3.0.0) split into two NuGet packages: +- **SmokeMe** (core, netstandard2.0) — framework-agnostic: reflection discovery, execution, reporting. Zero ASP.NET dependency. +- **SmokeMe.AspNetCore** (net8.0/net9.0/net10.0) — ASP.NET Core integration via `AddSmokeMe()` + `MapSmokeEndpoint()`. + +Author: Thomas PIERRAIN (42 skillz). + +## Build & Test Commands + +```bash +# Restore + build +dotnet build SmokeMe.sln + +# Run all tests +dotnet test SmokeMe.sln + +# Run a single test by name +dotnet test SmokeMe.Tests/SmokeMe.Tests.csproj --filter "FullyQualifiedName~SmokeControllerShould.Return_OK_200" + +# Pack both NuGet packages +dotnet pack SmokeMe/SmokeMe.csproj --configuration Release +dotnet pack SmokeMe.AspNetCore/SmokeMe.AspNetCore.csproj --configuration Release +``` + +## Architecture + +### Core Flow + +``` +SmokeTestAutoFinder (reflection discovery + DI instantiation) + │ + ▼ +MapSmokeEndpoint (GET /smoke?categories=X) ──► SmokeTestRunner (parallel TPL execution with global timeout) + │ │ + ▼ ▼ +SmokeTestSessionResultAdapter ◄──── SmokeTestsSessionReport / TimeoutSmokeTestsSessionReport + │ + ▼ +SmokeTestsSessionReportDto (HTTP response) +``` + +### Key Types + +**SmokeMe (core):** +- **`SmokeTest`** — Abstract base class. Consumers implement `SmokeTestName`, `Description`, `Scenario()`. Optional: `HasToBeDiscarded()` for feature-toggle integration. +- **`ISmokeTestConfiguration`** — Abstraction for timeout + enabled flag. Decoupled from ASP.NET's `IConfiguration`. +- **`SmokeMeOptions`** — Default implementation of `ISmokeTestConfiguration`. +- **`SmokeTestAutoFinder`** (`IFindSmokeTests`) — Scans all loaded assemblies for `SmokeTest` subclasses, filters by `[Category]`/`[Ignore]`, instantiates via `IServiceProvider`. +- **`SmokeTestRunner`** — Static `ExecuteAllSmokeTestsInParallel()`. Uses `Task.WhenAny` for global timeout enforcement. + +**SmokeMe.AspNetCore:** +- **`SmokeMeServiceCollectionExtensions.AddSmokeMe()`** — Registers `ISmokeTestConfiguration` + `IFindSmokeTests` in DI. +- **`SmokeMeEndpointRouteBuilderExtensions.MapSmokeEndpoint()`** — Minimal API endpoint. HTTP status codes: 200 (all passed), 500 (failures), 504 (timeout), 501 (no tests found), 503 (disabled via config). +- **`SmokeMeConfigurationAdapter`** — Bridges ASP.NET `IConfiguration` to `ISmokeTestConfiguration`. +- **`SmokeController`** — Legacy MVC controller (marked `[Obsolete]`, will be removed in v4). + +### Configuration Keys (appsettings.json) + +```json +{ + "Smoke": { + "GlobalTimeoutInMsec": 30000, + "IsSmokeTestExecutionEnabled": true + } +} +``` + +Defaults defined in `Constants.cs`. + +## Solution Structure + +| Project | Target | Purpose | +|---------|--------|---------| +| `SmokeMe/` | netstandard2.0 | Core library (NuGet package) | +| `SmokeMe.AspNetCore/` | net8.0;net9.0;net10.0 | ASP.NET Core integration (NuGet package) | +| `SmokeMe.Tests/` | net10.0 | Tests (NUnit + NFluent + NSubstitute + Diverse) | +| `Samples/Sample.Api/` | net10.0 | Example API using `AddSmokeMe()` + `MapSmokeEndpoint()` | +| `Samples/Sample.ExternalSmokeTests/` | netstandard2.0 | Smoke tests in a separate assembly | + +## Testing Conventions + +- **NUnit** framework, **NFluent** assertions (`Check.That(...)`), **NSubstitute** for mocking, **Diverse** (`Fuzzer`) for test data generation. +- Acceptance tests dominate — they exercise `SmokeController` end-to-end with stubbed dependencies. +- Test fixtures use a **`Stub`** helper class (in `SmokeMe.Tests/Helpers/Stub.cs`) to build `ISmokeTestConfiguration`, `IConfiguration` and `IFindSmokeTests` instances. +- Tests are fast (no I/O), isolated (no shared `[SetUp]` state). + +## CI + +GitHub Actions (`.github/workflows/dotnet.yml`): builds on Ubuntu with .NET 8 + 9, runs tests, creates both NuGet packages on push to main. diff --git a/Images/Bluesky_icon.png b/Images/Bluesky_icon.png new file mode 100644 index 0000000..6ecb0a6 Binary files /dev/null and b/Images/Bluesky_icon.png differ diff --git a/Images/Twitter_icon.gif b/Images/Twitter_icon.gif deleted file mode 100644 index e8b1432..0000000 Binary files a/Images/Twitter_icon.gif and /dev/null differ diff --git a/Images/breaking-news-v3.png b/Images/breaking-news-v3.png new file mode 100644 index 0000000..9c9da2a Binary files /dev/null and b/Images/breaking-news-v3.png differ diff --git a/Images/breakingChanges.jpg b/Images/breakingChanges.jpg deleted file mode 100644 index f7a00f4..0000000 Binary files a/Images/breakingChanges.jpg and /dev/null differ diff --git a/MIGRATION-v2-to-v3.md b/MIGRATION-v2-to-v3.md new file mode 100644 index 0000000..1ee0788 --- /dev/null +++ b/MIGRATION-v2-to-v3.md @@ -0,0 +1,172 @@ +# Migration Guide: SmokeMe v2 to v3 + +## Why v3? + +SmokeMe v2 was a single NuGet package targeting `netcoreapp3.1` (EOL since December 2022). It embedded an MVC controller, Swagger/OpenAPI dependencies, Newtonsoft.Json, and API versioning — all baked in, whether you needed them or not. + +v3 fixes this by: + +1. **Splitting into two packages** — a framework-agnostic core and an ASP.NET Core integration layer +2. **Targeting supported runtimes** — `netstandard2.0` (core) + `net8.0`/`net9.0` (ASP.NET integration) +3. **Removing heavy dependencies** — no more Newtonsoft.Json, Swashbuckle, or API versioning pulled in automatically +4. **Making registration explicit** — you opt-in with `AddSmokeMe()` + `MapSmokeEndpoint()` instead of relying on convention-based controller discovery + +## What changed + +| Area | v2 | v3 | +|------|----|----| +| **NuGet packages** | 1 package: `SmokeMe` | 2 packages: `SmokeMe` (core) + `SmokeMe.AspNetCore` (integration) | +| **Target framework** | `netcoreapp3.1` | `netstandard2.0` (core) + `net8.0`/`net9.0` (ASP.NET) | +| **Registration** | Automatic (MVC controller discovered by convention) | Explicit: `AddSmokeMe()` + `MapSmokeEndpoint()` | +| **Endpoint style** | MVC Controller (`SmokeController`) | Minimal API endpoint (via `MapSmokeEndpoint()`) | +| **JSON serializer** | Newtonsoft.Json | System.Text.Json | +| **Embedded dependencies** | Swashbuckle, API Versioning, Newtonsoft.Json | None — only `System.Text.Json` (core) and `Microsoft.AspNetCore.App` framework reference | +| **Configuration** | `IConfiguration` extension methods (`GetSmokeMeGlobalTimeout()`, `IsSmokeTestExecutionEnabled()`) | `ISmokeTestConfiguration` interface (auto-bridged from `appsettings.json`) | +| **`ICheckSmoke` interface** | Present (deprecated since v2) | Removed | + +## Step-by-step migration + +### 1. Update NuGet references + +Remove the old `SmokeMe` package and install both v3 packages: + +```xml + + + + + + +``` + +> If you have smoke tests in a **separate assembly** (class library), that assembly only needs the `SmokeMe` package (core). Only your **web host** project needs `SmokeMe.AspNetCore`. + +### 2. Register services and map the endpoint + +**v2** — no registration needed (the `SmokeController` was discovered automatically by MVC): + +```csharp +// Startup.cs — v2: nothing special, the controller just worked +public void ConfigureServices(IServiceCollection services) +{ + services.AddControllers(); + // ... +} +``` + +**v3** — explicit registration required: + +```csharp +// Program.cs (Minimal API style) +using SmokeMe.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSmokeMe(); // registers smoke test discovery + configuration + +var app = builder.Build(); + +app.MapSmokeEndpoint(); // GET /smoke (default path) + +app.Run(); +``` + +Or if you use a custom path: + +```csharp +app.MapSmokeEndpoint("/healthcheck/smoke"); +``` + +### 3. Remove `ICheckSmoke` usages (if any remain) + +`ICheckSmoke` was deprecated in v2 and has been **removed** in v3. If you still have types implementing it: + +```csharp +// v2 (deprecated) +public class MyTest : ICheckSmoke { ... } + +// v3 +public class MyTest : SmokeTest +{ + public override string SmokeTestName => "My test"; + public override string Description => "Checks something"; + public override async Task Scenario() { ... } +} +``` + +### 4. Update configuration access (if you used extension methods) + +v2 provided extension methods on `IConfiguration`: + +```csharp +// v2 +var timeout = configuration.GetSmokeMeGlobalTimeout(); +var isEnabled = configuration.IsSmokeTestExecutionEnabled(); +``` + +These extension methods have been **removed**. In v3, configuration is read automatically from `appsettings.json` by the `AddSmokeMe()` registration. The configuration keys are unchanged: + +```json +{ + "Smoke": { + "GlobalTimeoutInMsec": 30000, + "IsSmokeTestExecutionEnabled": true + } +} +``` + +If you need programmatic configuration: + +```csharp +builder.Services.AddSmokeMe(options => +{ + options.GlobalTimeout = TimeSpan.FromSeconds(60); + options.IsExecutionEnabled = true; +}); +``` + +`appsettings.json` values take precedence over programmatic defaults when both are present. + +### 5. Handle JSON serialization differences + +v3 uses **System.Text.Json** instead of Newtonsoft.Json. This matters if you were relying on specific Newtonsoft behaviors in your smoke test responses: + +- Property names are now **camelCase by default** (System.Text.Json default) instead of the Newtonsoft default +- If you were parsing the `/smoke` response in external tools, verify the JSON shape still matches your expectations + +### 6. Remove Swagger/API versioning workarounds (if any) + +v2 pulled in Swashbuckle and `Microsoft.AspNetCore.Mvc.Versioning`. If you had added configurations or workarounds because of these transitive dependencies, you can safely remove them. v3 has no opinion on Swagger or API versioning. + +## What about the legacy `SmokeController`? + +The `SmokeMe.AspNetCore` package still ships a `SmokeController` marked `[Obsolete]`. This is a **temporary migration aid**: + +- If you call `AddSmokeMe()` + `MapSmokeEndpoint()`, the Minimal API endpoint is used (recommended) +- If you still use `AddControllers()` and MVC routing, the legacy `SmokeController` will also be discovered — both can coexist +- The `SmokeController` will be **removed in v4** + +**Recommendation:** migrate to `MapSmokeEndpoint()` now. If you see an `[Obsolete]` compiler warning about `SmokeController`, it means something is still referencing it. + +## Smoke test classes: no changes needed + +The `SmokeTest` abstract base class is **unchanged**. Your existing smoke test implementations (`SmokeTestName`, `Description`, `Scenario()`, `HasToBeDiscarded()`) work as-is. Constructor injection via `IServiceProvider` also works identically. + +## Compatibility matrix + +| .NET version | SmokeMe (core) | SmokeMe.AspNetCore | +|---|---|---| +| .NET 8 (LTS) | Yes (via netstandard2.0) | Yes | +| .NET 9 | Yes (via netstandard2.0) | Yes | +| .NET Framework 4.6.1+ | Yes (via netstandard2.0) | No (ASP.NET Core only) | +| .NET Core 3.1 / .NET 5-7 | Yes (via netstandard2.0) | No (EOL runtimes) | + +## TL;DR — minimal migration checklist + +- [ ] Replace `SmokeMe` v2 package with `SmokeMe` + `SmokeMe.AspNetCore` v3 +- [ ] Add `builder.Services.AddSmokeMe()` in your service registration +- [ ] Add `app.MapSmokeEndpoint()` in your endpoint configuration +- [ ] Replace any `ICheckSmoke` implementations with `SmokeTest` (if not already done) +- [ ] Remove any calls to `configuration.GetSmokeMeGlobalTimeout()` or `configuration.IsSmokeTestExecutionEnabled()` +- [ ] Verify your `appsettings.json` `Smoke:` section still works (keys unchanged) +- [ ] Run your smoke tests to confirm the `/smoke` endpoint responds correctly diff --git a/README.md b/README.md index 0e2366c..e23441c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,22 @@ # SmokeMe! (a.k.a. /smoke ) ![.NET Core](https://github.com/42skillz/Smoke/workflows/.NET%20Core/badge.svg) -A *convention-based* dotnet plugin that will automatically expose all your declared smoke tests behind a **/smoke** resource in your API. +A *convention-based* dotnet library that will automatically expose all your declared smoke tests behind a **/smoke** endpoint in your API. -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/smoke.jpg?raw=true) -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/breakingChanges.jpg?raw=true) - -# -![twitter icon](https://github.com/42skillz/Smoke/blob/main/Images/Twitter_icon.gif?raw=true) [use case driven on twitter](https://twitter.com/tpierrain) - (thomas@42skillz.com) +![twitter screen](./Images/smoke.jpg) +![V3 Released!](./Images/breaking-news-v3.png) + +> **Upgrading from v2?** See the [Migration Guide (v2 to v3)](./MIGRATION-v2-to-v3.md) for breaking changes and step-by-step instructions. + +# +![Bluesky icon](./Images/Bluesky_icon.png) [use case driven on Bluesky](https://bsky.app/profile/tpierrain.bsky.social) - (thomas.pierrain@shodo.io) ## Smoke tests anyone? -Smoke test is preliminary integration testing to reveal simple failures severe enough to, for example, reject a prospective software release. +Smoke test is preliminary integration testing to reveal simple failures severe enough to, for example, reject a prospective software release. The expression came from plumbing where a *smoke test* is a technique forcing non-toxic, artificially created smoke through waste and drain pipes under a slight pressure **to find leaks**. In software, we use *smoke tests* in order **to find basic issues in production**. -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/swaggered-crop.jpg?raw=true) +![twitter screen](./Images/swaggered-crop.jpg) This may differ from classical health checks: @@ -29,25 +31,64 @@ This may differ from classical health checks: ### *"Smoke tests can save your bacon when doing Continuous Delivery!"* -The idea of the **SmokeMe** plugin library is to save you times and let you only focus on writing your functional or technical smoke tests. +The idea of the **SmokeMe** library is to save you time and let you only focus on writing your functional or technical smoke tests. + +All the auto-discovery, infrastructure and plumbing things are done for you by the library. + + +## Packages -All the auto-discovery, infrastructure and plumbering things are done for you by the pico lib. +SmokeMe v3 is split into two NuGet packages: + +| Package | Target | Purpose | +|---------|--------|---------| +| **SmokeMe** | netstandard2.0 | Core library — smoke test base class, discovery, execution. No ASP.NET dependency. | +| **SmokeMe.AspNetCore** | net8.0 / net9.0 | ASP.NET Core integration — `AddSmokeMe()` + `MapSmokeEndpoint()` | + +If you have smoke tests in a **separate class library**, that project only needs the `SmokeMe` package. Only your **web host** project needs `SmokeMe.AspNetCore`. ## It couldn't be easier! -### A. While coding +### A. Setup your API + +1. Add both NuGet packages to your API project: + +```xml + + +``` + +2. Register and map the smoke endpoint in your `Program.cs`: + +```csharp +using SmokeMe.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSmokeMe(); // registers smoke test discovery + configuration + +var app = builder.Build(); + +app.MapSmokeEndpoint(); // GET /smoke (default path, customizable) + +app.Run(); +``` + +That's it. SmokeMe will automatically discover all `SmokeTest` classes across your loaded assemblies and run them when you hit `/smoke`. -1. You add the reference to the **SmokeMe** library in your API project -2. You code all the smoke tests scenario you want in your code base - - A Smoke test scenario **is just a class deriving from the SmokeTest abstract class** with 3 abstract members to be overidden and a few others virtual methods that you can optionally override (like the HasToBeDiscarded() method if you want to couple a smoke test to a toggled feature for instance). +### B. Write your smoke tests + +A smoke test scenario **is just a class deriving from the `SmokeTest` abstract class** with 3 abstract members to override and a few optional virtual methods (like `HasToBeDiscarded()` if you want to couple a smoke test to a feature toggle). + +All the dependencies you need will be automatically injected via constructor injection from your ASP.NET `IServiceProvider`. ```csharp /// /// Smoke test/scenario/code to be executed in order to check that a minimum /// viable capability of your system is working. -/// +/// /// Note: all the services and dependencies you need for it will be automatically /// injected by the SmokeMe framework via the ASP.NET IServiceProvider of your API /// (classical constructor-based injection). Can't be that easy, right? ;-) @@ -98,7 +139,7 @@ __Ignore__ ``` -or __Category__ to target one of more subset of Smoke tests. +or __Category__ to target one or more subsets of smoke tests. ```csharp @@ -112,9 +153,9 @@ or __Category__ to target one of more subset of Smoke tests. ``` -### B. While deploying or supporting your production +### C. While deploying or supporting your production -You just GET the (automatically added) **/smoke** ressource **at the root level of your API**. +You just GET the **/smoke** endpoint **at the root level of your API**. e.g.: @@ -132,108 +173,87 @@ And you check the HTTP response type you get: ### HTTP 200 (OK) -Means that all your smoke tests have been executed nicely and before the global timeout allowed by **SmokeMe** +Means that all your smoke tests have been executed successfully and before the global timeout. -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/HTTP-200.JPG?raw=true) +![twitter screen](./Images/HTTP-200.JPG) ### HTTP 504 (GatewayTimeout) -Means that one or more smoke tests have timeout (configurable global timeout is 20 seconds by default) +Means that one or more smoke tests have timed out (configurable global timeout is 30 seconds by default). -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/HTTP-504.JPG?raw=true) +![twitter screen](./Images/HTTP-504.JPG) ### HTTP 501 (Not implemented) -Means that **SmokeMe** could not find any **ITestSmoke** type within all the assemblies +Means that **SmokeMe** could not find any `SmokeTest` type within all the assemblies that have been loaded into the execution context of this API. -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/HTTP-501.JPG?raw=true) +![twitter screen](./Images/HTTP-501.JPG) ### HTTP 500 (Internal Server Error) -Means that **SmokeMe** has executed all your declared **ITestSmoke** type instances but there have been +Means that **SmokeMe** has executed all your declared `SmokeTest` instances but there has been at least one failing smoke test. -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/HTTP-500.JPG?raw=true) +![twitter screen](./Images/HTTP-500.JPG) ### HTTP 503 (Service Unavailable) Means that smoke test execution has been disabled via configuration. -![twitter screen](https://github.com/42skillz/Smoke/blob/main/Images/HTTP-503.JPG?raw=true) +![twitter screen](./Images/HTTP-503.JPG) --- -## FAQ - -### 0.1 Why did you break the core ICheckSmoke interface in SmokeMe version 2? - -``` -Relying on an interface was not a good idea for extensibility reason. -Indeed, when you want to add new characteristics (with default values) to existing smoke tests, -you are forced to rely on the will and the awareness of every consumer code that has to -reference a new extending interface. - -With v2 we took the decision to replace the former ICheckSmoke interface with a new -abstract class: SmokeTest. This will allow us to add more default behaviours and to support -new features for your smoke tests in the future without any other breaking change. +## Configuration -We realize that migrating your code from v1 to v2 is a major change for you and -we are sorry for that inconvenient. +SmokeMe reads its configuration from your `appsettings.json` under the `Smoke:` section: +```json +{ + "Smoke": { + "GlobalTimeoutInMsec": 30000, + "IsSmokeTestExecutionEnabled": true + } +} ``` -### 0.2 How can I migrate from SmokeMe v1.x to v2.x? +You can also configure programmatically: +```csharp +builder.Services.AddSmokeMe(options => +{ + options.GlobalTimeout = TimeSpan.FromSeconds(60); + options.IsExecutionEnabled = true; +}); ``` -1. Replace all your reference to ICheckSmoke with SmokeTest abstract class - -2. Add 'override' keyword to all your existing 'SmokeTestName', 'Description' properties -and to your 'Scenario()' methods. - -3. That's it ;-) - -``` +`appsettings.json` values take precedence over programmatic defaults when both are present. +--- -### 1. Does SmokeMe execute all your founded smoke tests in parallel? +## FAQ -``` -Yes. Every smoke test will run in a dedicated TPL's Task. -``` +### 1. Does SmokeMe execute all smoke tests in parallel? -### 2. Does SmokeMe have a global timeout for all smoke tests to be ran? +Yes. Every smoke test runs in a dedicated TPL Task. -``` -Yes. It's 20 seconds by default (20 *1000 milliseconds). But you can override this -default value by setting the **Smoke:GlobalTimeoutInMsec** configuration key -of your Web API project. +### 2. Does SmokeMe have a global timeout? -``` +Yes. It's 30 seconds by default. You can override this value by setting the `Smoke:GlobalTimeoutInMsec` configuration key in your `appsettings.json` or via `AddSmokeMe(options => ...)`. -### 3. How to make SmokeMe being able to execute all my smoke tests? +### 3. How does SmokeMe find my smoke tests? -``` -More than easy. All you have to do is to add a reference to the **SmokeMe** lib -in your API project for it to be able to find all of them. That's it! -``` +SmokeMe scans all loaded assemblies for types deriving from `SmokeTest`. As long as the assembly containing your smoke tests is loaded (referenced by your API project), they will be discovered automatically. ### 4. How to code and declare a smoke test? -``` -Easy, all you have to do is to add a reference to the **SmokeMe** lib in your -code and to code a smoke test by implementing a type deriving from the -SmokeMe.SmokeTest abstract class. - -``` - -e.g.: +Implement a class deriving from `SmokeMe.SmokeTest`: ```csharp @@ -245,22 +265,22 @@ public class AvailabilitiesSmokeTest : SmokeTest private readonly IAvailabilityService _availabilityService; public override string SmokeTestName => "Check Availabilities"; - public override string Description + public override string Description => "TBD: will check something like checking that one can find some availabilities around Marseille city next month."; /// /// Instantiates a /// - /// The we need (will be + /// The we need (will be /// automatically injected par the SmokeMe library) public AvailabilitiesSmokeTest(IAvailabilityService availabilityService) { - // availability service here is just an example of + // availability service here is just an example of // on of your own API-level registered service automatically // injected to your smoke test instance by the SmokeMe lib _availabilityService = availabilityService; } - + /// /// The implementation of this smoke test scenario. /// @@ -280,69 +300,26 @@ public class AvailabilitiesSmokeTest : SmokeTest ``` -### 5. How can I avoid the issue of having error: '"code": "ApiVersionUnspecified" ' when calling /smoke? - -``` -This issue is due to the fact that your API requires an explicit version for every Controller -whereas the SmokeMe.SmokeController does not have one on purpose (to avoid crashing -when one does not have an explicit versioning configuration nor references -to Microsoft.AspNetCore.Mvc.Versioning & Co in its API). - -As a consequence, the /smoke route for your smoke test won't be coupled to any version -like /api/v1/ etc. but will be available instead from the root of your API /smoke. - -Fortunately the error that may occurs when calling /smoke in those cases may be fixed by a simple option within your API Startup type: - -options.AssumeDefaultVersionWhenUnspecified = true; - -at the services.AddApiVersioning(...) method invocation level. - - -``` -e.g.: - -```csharp - -services.AddApiVersioning( - options => - { - options.ReportApiVersions = true; - options.DefaultApiVersion = new ApiVersion(0,0); - options.AssumeDefaultVersionWhenUnspecified = true; // the line you should add in case of problem - } ); - -``` - +### 5. How can I disable the execution of all smoke tests? -### 6. How can I disable the execution of all smoke test? - -``` -Just set false to the "Smoke:IsSmokeTestExecutionEnabled" configuration key (default value is true). - -e.g.: +Set `Smoke:IsSmokeTestExecutionEnabled` to `false` in your configuration: +```json { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, "Smoke": { "GlobalTimeoutInMsec": 1500, "IsSmokeTestExecutionEnabled": false - }, - "AllowedHosts": "*" + } } - ``` -### 7. How can I run a subset of my smoke tests only? +The `/smoke` endpoint will return HTTP 503 (Service Unavailable). + +### 6. How can I run a subset of my smoke tests only? All you have to do is: -1. To declare some [Category("myCategoryName")] attributes on the SmokeTest types you want. For instance: +1. To declare some [Category("myCategoryName")] attributes on the SmokeTest types you want. For instance: ```csharp @@ -355,7 +332,7 @@ All you have to do is: ``` -2. To call the /smoke HTTP route with the category you want to run specifically as Querystring. +2. To call the /smoke HTTP route with the category you want to run specifically as Querystring. E.g.: @@ -372,7 +349,7 @@ or if you want to call all smoke tests corresponding to many categories only (as ``` -### 8. How can I Ignore one or more smoke tests? +### 7. How can I Ignore one or more smoke tests? Just add an [Ignore] attribute on the smoke tests you want to Ignore. e.g.: @@ -387,7 +364,7 @@ e.g.: ``` -### 9. How can I discard the execution of a smoke tests depending on one of our feature flags? +### 8. How can I discard the execution of a smoke tests depending on one of our feature flags? A Discarded Smoke test is a smoke test that exist but won't be run on purpose. @@ -425,16 +402,11 @@ e.g.: ``` -### 10. What is the difference between Ignored and Discarded smoke tests? - - -``` -An Ignored smoke test is a smoke test that won't run until you remove its [Ignore("...")] attribute (compile time). +### 9. What is the difference between Ignored and Discarded smoke tests? -A Discarded Smoke test is a smoke test that can be run (or not) depending on dynamic conditions (very handy -if you want some smoke tests to be enabled with a given n+1 version or any feature toggle for instance). +An **Ignored** smoke test is a smoke test that won't run until you remove its `[Ignore]` attribute (compile-time decision). -``` +A **Discarded** smoke test is a smoke test that can be run (or not) depending on dynamic conditions at runtime (very handy if you want some smoke tests to be enabled with a given n+1 version or any feature toggle for instance). --- @@ -454,7 +426,3 @@ if you want some smoke tests to be enabled with a given n+1 version or any featu ## Hope you will enjoy it! We value your input and appreciate your feedback. Thus, don't hesitate to leave them on the [**github issues of the project**](https://github.com/42skillz/SmokeMe/issues). - - - - diff --git a/Samples/Sample.31.Api/Controllers/WeatherForecastController.cs b/Samples/Sample.31.Api/Controllers/WeatherForecastController.cs deleted file mode 100644 index ee66e93..0000000 --- a/Samples/Sample.31.Api/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Sample.Api.FakeDomain; - -namespace Sample.Api.Controllers -{ - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - private readonly IProviderNumbers _numberProvider; - - public WeatherForecastController(ILogger logger, IProviderNumbers numberProvider) - { - _logger = logger; - _numberProvider = numberProvider; - } - - [HttpGet] - public IEnumerable Get() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Id = _numberProvider.GiveMeANumber(), - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/Samples/Sample.31.Api/Program.cs b/Samples/Sample.31.Api/Program.cs deleted file mode 100644 index d19da4f..0000000 --- a/Samples/Sample.31.Api/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace Sample.Api -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/Samples/Sample.31.Api/Properties/launchSettings.json b/Samples/Sample.31.Api/Properties/launchSettings.json deleted file mode 100644 index 8e48604..0000000 --- a/Samples/Sample.31.Api/Properties/launchSettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:50325", - "sslPort": 44378 - } - }, - "profiles": { - "Smoke.Api": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "smoke?categories=DB&categories=Connectivity", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Samples/Sample.31.Api/Sample.31.Api.csproj b/Samples/Sample.31.Api/Sample.31.Api.csproj deleted file mode 100644 index 8faaaab..0000000 --- a/Samples/Sample.31.Api/Sample.31.Api.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - netcoreapp3.1 - Sample.31.Api - Sample.Api - 2.7.0 - - - - .\Sample.31.Api.xml - 4 - - - - 1 - - - - - - - - - - - - - - - - - - diff --git a/Samples/Sample.31.Api/Sample.31.Api.xml b/Samples/Sample.31.Api/Sample.31.Api.xml deleted file mode 100644 index 9594d88..0000000 --- a/Samples/Sample.31.Api/Sample.31.Api.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - Sample.31.Api - - - - - Override this method if you need to configure Swagger endpoint, e.g. using UseSwagger() and UseSwaggerUI() extension methods. - - - - - - - Extension methods for instances. - - - - - Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. - - This is only required due to bugs in the . - Once they are fixed and published, this class can be removed. - - - - Applies the filter to the specified operation using the given context. - - The operation to apply the filter to. - The current operation filter context. - - - diff --git a/Samples/Sample.31.Api/Smoke.31.Api.xml b/Samples/Sample.31.Api/Smoke.31.Api.xml deleted file mode 100644 index 9594d88..0000000 --- a/Samples/Sample.31.Api/Smoke.31.Api.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - Sample.31.Api - - - - - Override this method if you need to configure Swagger endpoint, e.g. using UseSwagger() and UseSwaggerUI() extension methods. - - - - - - - Extension methods for instances. - - - - - Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. - - This is only required due to bugs in the . - Once they are fixed and published, this class can be removed. - - - - Applies the filter to the specified operation using the given context. - - The operation to apply the filter to. - The current operation filter context. - - - diff --git a/Samples/Sample.31.Api/SmokeTests/AlwaysDiscardedSmokeTest.cs b/Samples/Sample.31.Api/SmokeTests/AlwaysDiscardedSmokeTest.cs deleted file mode 100644 index 36ca828..0000000 --- a/Samples/Sample.31.Api/SmokeTests/AlwaysDiscardedSmokeTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; -using SmokeMe; - -namespace Sample.Api.SmokeTests -{ - public class AlwaysDiscardedSmokeTest : SmokeTest - { - public override string SmokeTestName => "Discarded Smoke test"; - public override string Description => "Smoke test systematically Discarded"; - - public override async Task HasToBeDiscarded() - { - return await Task.FromResult(true); - } - - public override Task Scenario() - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/Samples/Sample.31.Api/SmokeTests/FlippingSmokeTest.cs b/Samples/Sample.31.Api/SmokeTests/FlippingSmokeTest.cs deleted file mode 100644 index 08c06c3..0000000 --- a/Samples/Sample.31.Api/SmokeTests/FlippingSmokeTest.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; -using Diverse; -using Microsoft.Extensions.Configuration; -using Sample.Api.FakeDomain; -using SmokeMe; -using SmokeMe.Helpers; - -namespace Sample.Api.SmokeTests -{ - public class FlippingSmokeTest : SmokeTest - { - private readonly IProviderNumbers _numbersProvider; - private readonly IFuzz _fuzzer; - private readonly IConfiguration _configuration; - public override string SmokeTestName => "Flipping smoke test"; - public override string Description => $"For unit testing purpose. Smoke test being able to randomly timeout, succeeded or failed."; - - public FlippingSmokeTest(IProviderNumbers numbersProvider, IFuzz fuzzer, IConfiguration configuration) - { - _numbersProvider = numbersProvider; - _fuzzer = fuzzer; - _configuration = configuration; - } - - public override async Task Scenario() - { - var delayInMsec = _fuzzer.GenerateInteger(100, 1700); - - if (_fuzzer.HeadsOrTails()) - { - // force a timeout - delayInMsec = Convert.ToInt32(_configuration.GetSmokeMeGlobalTimeout().Add(TimeSpan.FromSeconds(1)).TotalMilliseconds); - } - - await Task.Delay(delayInMsec); - - _numbersProvider.GiveMeANumber(); - - if (_fuzzer.HeadsOrTails()) - { - return new SmokeTestResult(true); - } - else - { - return new SmokeTestResult(false); - } - } - } -} diff --git a/Samples/Sample.31.Api/Startup.cs b/Samples/Sample.31.Api/Startup.cs deleted file mode 100644 index 947ea96..0000000 --- a/Samples/Sample.31.Api/Startup.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using Diverse; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Versioning; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Sample.Api.FakeDomain; -using Sample.ExternalSmokeTests.Utilities; -using Swashbuckle.AspNetCore.SwaggerGen; -using Swashbuckle.AspNetCore.SwaggerUI; - -namespace Sample.Api -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddMvc() - .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) - .AddMvcOptions((Action)(o => o.EnableEndpointRouting = false)); - - services.AddControllers() - .AddNewtonsoftJson((Action)(options => options.SerializerSettings.Converters.Add((JsonConverter)new StringEnumConverter()))); - - services.AddVersioning(); - - services.AddTransient(); - - - - // -------- Specific services for the API (no need to register anything for Smoke lib usage --------- - services.AddSingleton(); - - Fuzzer.Log += obj => - { - }; - services.AddSingleton(); - - services.AddTransient(); - // -------------------------------------------------------------------------------------------------- - - services.AddSwaggerGenNewtonsoftSupport(); - - var apiContact = new OpenApiContact() - { - Name = "Super contact team", - Email = "api.me@whatever.com" - }; - - var swaggerTitle = this.GetType().Assembly.GetCustomAttribute()?.Product ?? ""; - - services.AddSwaggerGen((Action)(o => - { - var str = Path.Combine(AppContext.BaseDirectory, Assembly.GetExecutingAssembly().GetName().Name ?? string.Empty) + ".xml"; - if (!File.Exists(str)) - return; - o.IncludeXmlComments(str); - })); - - services.AddSwaggerGeneration(apiContact, swaggerTitle, this.GetType()); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - ConfigureSwagger(app, provider); - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - - /// - /// Override this method if you need to configure Swagger endpoint, e.g. using UseSwagger() and UseSwaggerUI() extension methods. - /// - /// - /// - protected virtual void ConfigureSwagger(IApplicationBuilder app, IApiVersionDescriptionProvider provider) - { - app.UseSwagger(); - app.UseSwaggerUI((Action)(options => - { - var swaggerUiOptions = options; - var entryAssembly = Assembly.GetEntryAssembly(); - var str = ((object)entryAssembly != null ? entryAssembly.GetName().Name : (string)null) + " - Swagger"; - swaggerUiOptions.DocumentTitle = str; - foreach (var versionDescription in provider.ApiVersionDescriptions) - { - options.SwaggerEndpoint("/swagger/" + versionDescription.GroupName + "/swagger.json", versionDescription.GroupName.ToUpperInvariant()); - } - })); - } - - } - - /// - /// Extension methods for instances. - /// - public static class ServiceCollectionExtension - { - public static IServiceCollection AddSwaggerGeneration( - this IServiceCollection services, - OpenApiContact apiContact, - string swaggerTitle, - Type callerType) - { - return services.AddSwaggerGen((Action)(options => - { - foreach (ApiVersionDescription versionDescription in (IEnumerable)services.BuildServiceProvider().GetRequiredService().ApiVersionDescriptions) - options.SwaggerDoc(versionDescription.GroupName, new OpenApiInfo() - { - Title = swaggerTitle + $" {(object)versionDescription.ApiVersion}", - Version = versionDescription.ApiVersion.ToString(), - Contact = apiContact - }); - options.OperationFilter(); - options.IncludeXmlComments(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), callerType.Assembly.GetName().Name + ".xml")); - })); - } - - public static IServiceCollection AddVersioning(this IServiceCollection services) - { - return services.AddVersionedApiExplorer((Action)(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - })).AddApiVersioning((Action)(options => - { - options.ReportApiVersions = true; - options.AssumeDefaultVersionWhenUnspecified = true; - options.DefaultApiVersion = new ApiVersion(new DateTime(2016, 7, 1)); - })); - } - } - - /// - /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. - /// - /// This is only required due to bugs in the . - /// Once they are fixed and published, this class can be removed. - public class SwaggerDefaultValues : IOperationFilter - { - /// - /// Applies the filter to the specified operation using the given context. - /// - /// The operation to apply the filter to. - /// The current operation filter context. - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - if (operation.Parameters == null) - { - return; - } - - foreach (var parameter1 in (IEnumerable)operation.Parameters) - { - var parameter = parameter1; - var parameterDescription = context.ApiDescription.ParameterDescriptions.First((Func)(p => p.Name == parameter.Name)); - var routeInfo = parameterDescription.RouteInfo; - parameter.Description ??= parameterDescription.ModelMetadata?.Description; - - if (routeInfo != null) - { - parameter.Required |= !routeInfo.IsOptional; - } - } - } - } - - -} diff --git a/Samples/Sample.31.Api/WeatherForecast.cs b/Samples/Sample.31.Api/WeatherForecast.cs deleted file mode 100644 index 6208cd9..0000000 --- a/Samples/Sample.31.Api/WeatherForecast.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Sample.Api -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } - - public int Id { get; set; } - } -} diff --git a/Samples/Sample.31.Api/appsettings.Development.json b/Samples/Sample.31.Api/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/Samples/Sample.31.Api/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/Samples/Sample.31.Api/appsettings.json b/Samples/Sample.31.Api/appsettings.json deleted file mode 100644 index 5d8e890..0000000 --- a/Samples/Sample.31.Api/appsettings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "Smoke": { - "GlobalTimeoutInMsec": 1500, - "IsSmokeTestExecutionEnabled": true - }, - "AllowedHosts": "*" -} diff --git a/Samples/Sample.Api/Program.cs b/Samples/Sample.Api/Program.cs new file mode 100644 index 0000000..91c5881 --- /dev/null +++ b/Samples/Sample.Api/Program.cs @@ -0,0 +1,20 @@ +using Diverse; +using Sample.ExternalSmokeTests.Utilities; +using SmokeMe.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Register SmokeMe services (smoke test discovery + configuration) +builder.Services.AddSmokeMe(); + +// Register application-specific services needed by smoke tests +builder.Services.AddTransient(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Map the /smoke endpoint +app.MapSmokeEndpoint(); + +app.Run(); diff --git a/Samples/Sample.Api/Properties/launchSettings.json b/Samples/Sample.Api/Properties/launchSettings.json new file mode 100644 index 0000000..bd37811 --- /dev/null +++ b/Samples/Sample.Api/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "Sample.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "smoke", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Samples/Sample.Api/Sample.Api.csproj b/Samples/Sample.Api/Sample.Api.csproj new file mode 100644 index 0000000..e7dc442 --- /dev/null +++ b/Samples/Sample.Api/Sample.Api.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + Sample.Api + 3.0.0 + + + + + + + + + + + + diff --git a/Samples/Sample.Api/appsettings.json b/Samples/Sample.Api/appsettings.json new file mode 100644 index 0000000..e59de7e --- /dev/null +++ b/Samples/Sample.Api/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Smoke": { + "GlobalTimeoutInMsec": 30000, + "IsSmokeTestExecutionEnabled": true + } +} diff --git a/Samples/Sample.ExternalSmokeTests/Sample.ExternalSmokeTests.csproj b/Samples/Sample.ExternalSmokeTests/Sample.ExternalSmokeTests.csproj index 5a431ca..6b42a68 100644 --- a/Samples/Sample.ExternalSmokeTests/Sample.ExternalSmokeTests.csproj +++ b/Samples/Sample.ExternalSmokeTests/Sample.ExternalSmokeTests.csproj @@ -1,7 +1,8 @@  - netcoreapp3.1 + netstandard2.0 + latest @@ -12,7 +13,11 @@ .\Sample.ExternalSmokeTests.xml - + + + + + diff --git a/Samples/Sample.ExternalSmokeTests/Utilities/RestClient.cs b/Samples/Sample.ExternalSmokeTests/Utilities/RestClient.cs index c4836c6..2e6d9a6 100644 --- a/Samples/Sample.ExternalSmokeTests/Utilities/RestClient.cs +++ b/Samples/Sample.ExternalSmokeTests/Utilities/RestClient.cs @@ -40,7 +40,11 @@ public long MaxResponseContentBufferSize public RestClient() { - _httpClient = new HttpClient(CreateSocketsHttpHandlerWithReasonableValues()); + _httpClient = new HttpClient(); + // SocketsHttpHandler (with PooledConnectionLifetime and MaxConnectionsPerServer) + // is not available in netstandard2.0. We at least set a reasonable request timeout + // to avoid infinite waits. See https://www.stevejgordon.co.uk/httpclient-connection-pooling-in-dotnet-core + _httpClient.Timeout = TimeSpan.FromSeconds(30); } public RestClient(HttpMessageHandler handler) @@ -53,16 +57,6 @@ public RestClient(HttpMessageHandler handler, bool disposeHandler) _httpClient = new HttpClient(handler, disposeHandler); } - // This is to avoid default values which are infinite or Int.MaxValue ; see https://www.stevejgordon.co.uk/httpclient-connection-pooling-in-dotnet-core - private SocketsHttpHandler CreateSocketsHttpHandlerWithReasonableValues() - { - return new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(10), - MaxConnectionsPerServer = 8 - }; - } - public void Dispose() { _httpClient.Dispose(); diff --git a/Samples/Sample.dotnet5.Api/Controllers/WeatherForecastController.cs b/Samples/Sample.dotnet5.Api/Controllers/WeatherForecastController.cs deleted file mode 100644 index 74f7114..0000000 --- a/Samples/Sample.dotnet5.Api/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.dotnet5.Api.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/Samples/Sample.dotnet5.Api/NullSmokeTest.cs b/Samples/Sample.dotnet5.Api/NullSmokeTest.cs deleted file mode 100644 index b97233b..0000000 --- a/Samples/Sample.dotnet5.Api/NullSmokeTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Threading.Tasks; -using SmokeMe; - -namespace Sample.dotnet5.Api -{ - public class NullSmokeTest : ICheckSmoke - { - public string SmokeTestName => "Null smoke test"; - public string Description => "This is a dummy smoke test doing nothing more than waiting 0.025 ms."; - public async Task Scenario() - { - await Task.Delay(TimeSpan.FromMilliseconds(0.025)); - - return new SmokeTestResult(true); - } - } -} \ No newline at end of file diff --git a/Samples/Sample.dotnet5.Api/Program.cs b/Samples/Sample.dotnet5.Api/Program.cs deleted file mode 100644 index 8778ff6..0000000 --- a/Samples/Sample.dotnet5.Api/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Sample.dotnet5.Api -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/Samples/Sample.dotnet5.Api/Properties/launchSettings.json b/Samples/Sample.dotnet5.Api/Properties/launchSettings.json deleted file mode 100644 index 399c0d7..0000000 --- a/Samples/Sample.dotnet5.Api/Properties/launchSettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:57759", - "sslPort": 44391 - } - }, - "profiles": { - "Sample.dotnet5.Api": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "smoke", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Samples/Sample.dotnet5.Api/Sample.dotnet5.Api.csproj b/Samples/Sample.dotnet5.Api/Sample.dotnet5.Api.csproj deleted file mode 100644 index c3928cb..0000000 --- a/Samples/Sample.dotnet5.Api/Sample.dotnet5.Api.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net5.0 - - - - - - - - diff --git a/Samples/Sample.dotnet5.Api/Startup.cs b/Samples/Sample.dotnet5.Api/Startup.cs deleted file mode 100644 index 917fd40..0000000 --- a/Samples/Sample.dotnet5.Api/Startup.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Sample.dotnet5.Api -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/Samples/Sample.dotnet5.Api/WeatherForecast.cs b/Samples/Sample.dotnet5.Api/WeatherForecast.cs deleted file mode 100644 index f1028f3..0000000 --- a/Samples/Sample.dotnet5.Api/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Sample.dotnet5.Api -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } - } -} diff --git a/Samples/Sample.dotnet5.Api/appsettings.Development.json b/Samples/Sample.dotnet5.Api/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/Samples/Sample.dotnet5.Api/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/Samples/Sample.dotnet5.Api/appsettings.json b/Samples/Sample.dotnet5.Api/appsettings.json deleted file mode 100644 index d9d9a9b..0000000 --- a/Samples/Sample.dotnet5.Api/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/SmokeMe/Controllers/SmokeController.cs b/SmokeMe.AspNetCore/SmokeController.cs similarity index 65% rename from SmokeMe/Controllers/SmokeController.cs rename to SmokeMe.AspNetCore/SmokeController.cs index 45592c2..855c388 100644 --- a/SmokeMe/Controllers/SmokeController.cs +++ b/SmokeMe.AspNetCore/SmokeController.cs @@ -1,25 +1,25 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using SmokeMe.Helpers; using SmokeMe.Infra; -namespace SmokeMe.Controllers +namespace SmokeMe.AspNetCore { /// /// Executes smoke test declared for this API. /// Smoke tests are a set of short functional tests checking that the minimum viable prerequisites for this API is fine. /// + [Obsolete("Use MapSmokeEndpoint() instead. This controller will be removed in v4.")] [ApiController] [Route("smoke")] public class SmokeController : ControllerBase { private readonly IFindSmokeTests _smokeTestProvider; - private readonly IConfiguration _configuration; + private readonly ISmokeTestConfiguration _configuration; /// /// Instantiates a . @@ -27,7 +27,7 @@ public class SmokeController : ControllerBase /// The configuration of the API. /// A Service provider to be used to instantiate smoke tests. /// (optional) A smoke test provider (used for unit testing purpose). - public SmokeController(IConfiguration configuration, IServiceProvider serviceProvider, IFindSmokeTests smokeTestProvider = null) + public SmokeController(ISmokeTestConfiguration configuration, IServiceProvider serviceProvider, IFindSmokeTests smokeTestProvider = null) { _configuration = configuration; @@ -36,7 +36,7 @@ public SmokeController(IConfiguration configuration, IServiceProvider servicePro throw new ArgumentNullException("serviceProvider", "Must provide a non-null serviceProvider when smokeTestProvider is not provided."); } - smokeTestProvider??= new SmokeTestAutoFinder(serviceProvider); + smokeTestProvider ??= new SmokeTestAutoFinder(serviceProvider); _smokeTestProvider = smokeTestProvider; } @@ -47,31 +47,28 @@ public SmokeController(IConfiguration configuration, IServiceProvider servicePro [HttpGet] public async Task ExecuteSmokeTests([FromQuery] params string[] categories) { - var requestedCategories = categories; // adapt name to our context - var globalTimeout = _configuration.GetSmokeMeGlobalTimeout(); + var requestedCategories = categories; + var globalTimeout = _configuration.GlobalTimeout; - if (!_configuration.IsSmokeTestExecutionEnabled()) + if (!_configuration.IsExecutionEnabled) { - return StatusCode((int) HttpStatusCode.ServiceUnavailable, new SmokeTestsDisabledReportDto(new ApiRuntimeDescription(), globalTimeout)); + return StatusCode((int)HttpStatusCode.ServiceUnavailable, new SmokeTestsDisabledReportDto(new ApiRuntimeDescription(), globalTimeout)); } - // Find all smoke tests to run var smokeTests = _smokeTestProvider.FindAllSmokeTestsToRun(requestedCategories); - if (ThereIsNoUnignoredSmokeTest(smokeTests)) { if (requestedCategories.Length > 0) { return StatusCode((int)HttpStatusCode.NotImplemented, new SmokeTestsSessionReportDto(new ApiRuntimeDescription(), globalTimeout, status: GenerateStatusMessageForNoSmokeTestsWithCategories(requestedCategories))); } - - return StatusCode((int) HttpStatusCode.NotImplemented, new SmokeTestsSessionReportDto(new ApiRuntimeDescription(), globalTimeout, status: $"No smoke test have been found in your executing assemblies. Start adding (not ignored) {nameof(SmokeTest)} types in your code base so that the SmokeMe library can detect and run them.")); + + return StatusCode((int)HttpStatusCode.NotImplemented, new SmokeTestsSessionReportDto(new ApiRuntimeDescription(), globalTimeout, status: $"No smoke test have been found in your executing assemblies. Start adding (not ignored) {nameof(SmokeTest)} types in your code base so that the SmokeMe library can detect and run them.")); } var results = await SmokeTestRunner.ExecuteAllSmokeTestsInParallel(smokeTests, globalTimeout); - // Adapt from business to DTO with extra information var resultDto = SmokeTestSessionResultAdapter.Adapt(results, new ApiRuntimeDescription(), requestedCategories, _configuration); if (resultDto.IsSuccess) @@ -81,7 +78,7 @@ public async Task ExecuteSmokeTests([FromQuery] params string[] c if (results is TimeoutSmokeTestsSessionReport) { - return StatusCode((int) HttpStatusCode.GatewayTimeout, resultDto); + return StatusCode((int)HttpStatusCode.GatewayTimeout, resultDto); } return StatusCode((int)HttpStatusCode.InternalServerError, resultDto); @@ -89,19 +86,19 @@ public async Task ExecuteSmokeTests([FromQuery] params string[] c private static bool ThereIsNoUnignoredSmokeTest(IEnumerable smokeTests) { - return !smokeTests.Any(t=> !t.SmokeTest.GetType().HasIgnoredCustomAttribute()); + return !smokeTests.Any(t => !t.SmokeTest.GetType().HasIgnoredCustomAttribute()); } private static string GenerateStatusMessageForNoSmokeTestsWithCategories(params string[] categories) { if (categories.Length == 1) { - return @$"No smoke test with [Category(""{categories[0]}"")] attribute have been found in your executing assemblies. Check that you have one or more (not ignored) ICheckSmoke types in your code base with the declared attribute [Category(""{categories[0]}"")] so that the SmokeMe library can detect and run them."; + return @$"No smoke test with [Category(""{categories[0]}"")] attribute have been found in your executing assemblies. Check that you have one or more (not ignored) {nameof(SmokeTest)} types in your code base with the declared attribute [Category(""{categories[0]}"")] so that the SmokeMe library can detect and run them."; } var expectedAttributes = categories.Select(s => @$"[Category(""{s}"")]"); - - return @$"No smoke test with {string.Join(" or ", expectedAttributes)} attributes have been found in your executing assemblies. Check that you have one or more (not ignored) ICheckSmoke types in your code base with the expected declared [Category] attributes so that the SmokeMe library can detect and run them."; + + return @$"No smoke test with {string.Join(" or ", expectedAttributes)} attributes have been found in your executing assemblies. Check that you have one or more (not ignored) {nameof(SmokeTest)} types in your code base with the expected declared [Category] attributes so that the SmokeMe library can detect and run them."; } } } diff --git a/SmokeMe.AspNetCore/SmokeMe.AspNetCore.csproj b/SmokeMe.AspNetCore/SmokeMe.AspNetCore.csproj new file mode 100644 index 0000000..e03f0c5 --- /dev/null +++ b/SmokeMe.AspNetCore/SmokeMe.AspNetCore.csproj @@ -0,0 +1,45 @@ + + + + net8.0;net9.0 + latest + 3.0.0 + Thomas PIERRAIN (use case driven) + 42 skillz + ASP.NET Core integration for SmokeMe — exposes smoke tests via a /smoke HTTP endpoint using Minimal APIs. Use AddSmokeMe() + MapSmokeEndpoint(). + Copyright © Thomas PIERRAIN 2024 + LICENSE + https://github.com/42skillz/SmokeMe + smoke-icon.jpg + https://github.com/42skillz/SmokeMe + SmokeTests tests ContinuousDelivery Continuous Delivery Smoke Tests SmokeMe AspNetCore + v3.0.0 — Breaking changes: +- New explicit registration: builder.Services.AddSmokeMe() + app.MapSmokeEndpoint() +- Uses Minimal APIs instead of MVC Controller +- Supports net8.0 and net9.0 +- Legacy SmokeController still available (marked [Obsolete], will be removed in v4) + +Your existing SmokeTest classes work as-is. Only the hosting setup changes. +Full migration guide: https://github.com/42skillz/SmokeMe/blob/main/MIGRATION-v2-to-v3.md + SmokeMe.AspNetCore + 3.0.0.0 + 3.0.0.0 + + + + + + + + + + True + + + + True + + + + + diff --git a/SmokeMe.AspNetCore/SmokeMeConfigurationAdapter.cs b/SmokeMe.AspNetCore/SmokeMeConfigurationAdapter.cs new file mode 100644 index 0000000..b045ffc --- /dev/null +++ b/SmokeMe.AspNetCore/SmokeMeConfigurationAdapter.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.Extensions.Configuration; + +namespace SmokeMe.AspNetCore +{ + /// + /// Adapts ASP.NET Core's to . + /// Reads values from the "Smoke:" configuration section. + /// + public class SmokeMeConfigurationAdapter : ISmokeTestConfiguration + { + private readonly IConfiguration _configuration; + private readonly SmokeMeOptions _defaults; + + /// + /// Instantiates a . + /// + /// The ASP.NET Core configuration. + /// Default options (may be overridden by configuration values). + public SmokeMeConfigurationAdapter(IConfiguration configuration, SmokeMeOptions defaults = null) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _defaults = defaults ?? new SmokeMeOptions(); + } + + /// + public TimeSpan GlobalTimeout + { + get + { + if (int.TryParse(_configuration[Constants.GlobaltimeoutinmsecConfigurationKey], out var globalTimeoutInMsec)) + { + return TimeSpan.FromMilliseconds(globalTimeoutInMsec); + } + + return _defaults.GlobalTimeout; + } + } + + /// + public bool IsExecutionEnabled + { + get + { + if (bool.TryParse(_configuration[Constants.IsEnabledConfigurationKey], out var isEnabled)) + { + return isEnabled; + } + + return _defaults.IsExecutionEnabled; + } + } + } +} diff --git a/SmokeMe.AspNetCore/SmokeMeEndpointRouteBuilderExtensions.cs b/SmokeMe.AspNetCore/SmokeMeEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..f1b2e2f --- /dev/null +++ b/SmokeMe.AspNetCore/SmokeMeEndpointRouteBuilderExtensions.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using SmokeMe.Helpers; +using SmokeMe.Infra; + +namespace SmokeMe.AspNetCore +{ + /// + /// Extension methods for to map the SmokeMe endpoint. + /// + public static class SmokeMeEndpointRouteBuilderExtensions + { + /// + /// Maps a GET endpoint that executes all registered smoke tests. + /// + /// The endpoint route builder. + /// The URL pattern for the smoke endpoint. Default: "/smoke". + /// The for further configuration. + public static IEndpointConventionBuilder MapSmokeEndpoint(this IEndpointRouteBuilder endpoints, string pattern = "/smoke") + { + return endpoints.MapGet(pattern, async (HttpContext context) => + { + var configuration = context.RequestServices.GetRequiredService(); + var smokeTestProvider = context.RequestServices.GetRequiredService(); + + var globalTimeout = configuration.GlobalTimeout; + + if (!configuration.IsExecutionEnabled) + { + return Results.Json(new SmokeTestsDisabledReportDto(new ApiRuntimeDescription(), globalTimeout), + statusCode: (int)HttpStatusCode.ServiceUnavailable); + } + + var requestedCategories = context.Request.Query.ContainsKey("categories") + ? context.Request.Query["categories"].ToArray() + : new string[0]; + + var smokeTests = smokeTestProvider.FindAllSmokeTestsToRun(requestedCategories); + + if (ThereIsNoUnignoredSmokeTest(smokeTests)) + { + if (requestedCategories.Length > 0) + { + return Results.Json( + new SmokeTestsSessionReportDto(new ApiRuntimeDescription(), globalTimeout, + status: GenerateStatusMessageForNoSmokeTestsWithCategories(requestedCategories)), + statusCode: (int)HttpStatusCode.NotImplemented); + } + + return Results.Json( + new SmokeTestsSessionReportDto(new ApiRuntimeDescription(), globalTimeout, + status: $"No smoke test have been found in your executing assemblies. Start adding (not ignored) {nameof(SmokeTest)} types in your code base so that the SmokeMe library can detect and run them."), + statusCode: (int)HttpStatusCode.NotImplemented); + } + + var results = await SmokeTestRunner.ExecuteAllSmokeTestsInParallel(smokeTests, globalTimeout); + + var resultDto = SmokeTestSessionResultAdapter.Adapt(results, new ApiRuntimeDescription(), requestedCategories, configuration); + + if (resultDto.IsSuccess) + { + return Results.Json(resultDto, statusCode: (int)HttpStatusCode.OK); + } + + if (results is TimeoutSmokeTestsSessionReport) + { + return Results.Json(resultDto, statusCode: (int)HttpStatusCode.GatewayTimeout); + } + + return Results.Json(resultDto, statusCode: (int)HttpStatusCode.InternalServerError); + }); + } + + private static bool ThereIsNoUnignoredSmokeTest(IEnumerable smokeTests) + { + return !smokeTests.Any(t => !t.SmokeTest.GetType().HasIgnoredCustomAttribute()); + } + + private static string GenerateStatusMessageForNoSmokeTestsWithCategories(params string[] categories) + { + if (categories.Length == 1) + { + return @$"No smoke test with [Category(""{categories[0]}"")] attribute have been found in your executing assemblies. Check that you have one or more (not ignored) {nameof(SmokeTest)} types in your code base with the declared attribute [Category(""{categories[0]}"")] so that the SmokeMe library can detect and run them."; + } + + var expectedAttributes = categories.Select(s => @$"[Category(""{s}"")]"); + + return @$"No smoke test with {string.Join(" or ", expectedAttributes)} attributes have been found in your executing assemblies. Check that you have one or more (not ignored) {nameof(SmokeTest)} types in your code base with the expected declared [Category] attributes so that the SmokeMe library can detect and run them."; + } + } +} diff --git a/SmokeMe.AspNetCore/SmokeMeServiceCollectionExtensions.cs b/SmokeMe.AspNetCore/SmokeMeServiceCollectionExtensions.cs new file mode 100644 index 0000000..ec50f77 --- /dev/null +++ b/SmokeMe.AspNetCore/SmokeMeServiceCollectionExtensions.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace SmokeMe.AspNetCore +{ + /// + /// Extension methods for to register SmokeMe services. + /// + public static class SmokeMeServiceCollectionExtensions + { + /// + /// Registers SmokeMe services (smoke test discovery, configuration) into the DI container. + /// + /// The service collection. + /// Optional action to configure . + /// The service collection for chaining. + public static IServiceCollection AddSmokeMe(this IServiceCollection services, Action configureOptions = null) + { + var options = new SmokeMeOptions(); + configureOptions?.Invoke(options); + + services.TryAddSingleton(sp => + { + var configuration = sp.GetService(); + if (configuration != null) + { + return new SmokeMeConfigurationAdapter(configuration, options); + } + + return options; + }); + + services.TryAddSingleton(sp => new SmokeTestAutoFinder(sp)); + + return services; + } + } +} diff --git a/SmokeMe.Tests/Acceptance/SmokeControllerShould.cs b/SmokeMe.Tests/Acceptance/SmokeControllerShould.cs index 18970c5..459e5ea 100644 --- a/SmokeMe.Tests/Acceptance/SmokeControllerShould.cs +++ b/SmokeMe.Tests/Acceptance/SmokeControllerShould.cs @@ -9,7 +9,7 @@ using NUnit.Framework; using Sample.ExternalSmokeTests; using Sample.ExternalSmokeTests.Utilities; -using SmokeMe.Controllers; +using SmokeMe.AspNetCore; using SmokeMe.Infra; using SmokeMe.Tests.Helpers; using SmokeMe.Tests.SmokeTests; @@ -29,7 +29,7 @@ public void SetUp() [Repeat(10)] public async Task Run_all_smoke_tests() { - var configuration = Substitute.For(); + var configuration = Stub.ASmokeTestConfiguration(); var smokeTestProvider = Stub.ASmokeTestProvider(new AlwaysPositiveSmokeTest(TimeSpan.Zero).WithoutCategory(), new SmokeTestThrowingAnAccessViolationException(TimeSpan.Zero).WithoutCategory()); var controller = new SmokeController(configuration, null, smokeTestProvider); @@ -47,7 +47,7 @@ public async Task Run_all_smoke_tests() [Test] public async Task Return_InternalServerError_500_when_smoke_tests_fails() { - var configuration = Substitute.For(); + var configuration = Stub.ASmokeTestConfiguration(); var smokeTestProvider = Stub.ASmokeTestProvider(new SmokeTestThrowingAnAccessViolationException(TimeSpan.Zero).WithoutCategory()); var controller = new SmokeController(configuration, null, smokeTestProvider); @@ -61,7 +61,7 @@ public async Task Return_InternalServerError_500_when_smoke_tests_fails() public async Task Return_GatewayTimeout_504_when_smoke_tests_timeout_but_provide_details() { var globalTimeoutInMsec = 5 * 1000; - var configuration = Stub.AConfiguration(globalTimeoutInMsec: globalTimeoutInMsec); + var configuration = Stub.ASmokeTestConfiguration(globalTimeoutInMsec: globalTimeoutInMsec); var smokeTestProvider = Stub.ASmokeTestProvider(new AlwaysPositiveSmokeTest(TimeSpan.FromSeconds(6)).WithoutCategory(), new AlwaysPositiveSmokeTest(TimeSpan.FromSeconds(2.0)).WithoutCategory()); var controller = new SmokeController(configuration, null, smokeTestProvider); @@ -93,11 +93,11 @@ public async Task Return_GatewayTimeout_504_when_smoke_tests_timeout_but_provide [Test] public void Only_accept_null_ServiceProvider_for_unit_testing_purpose_when_we_provide_a_non_null_SmokeTestProvider() { - var smokeControllerForTesting = new SmokeController(Substitute.For(), null, Substitute.For()); + var smokeControllerForTesting = new SmokeController(Stub.ASmokeTestConfiguration(), null, Substitute.For()); Check.ThatCode(() => { - var smokeControllerForRealUsage = new SmokeController(Substitute.For(), null, null); + var smokeControllerForRealUsage = new SmokeController(Stub.ASmokeTestConfiguration(), null, null); }).Throws().WithMessage("Must provide a non-null serviceProvider when smokeTestProvider is not provided. (Parameter 'serviceProvider')"); } @@ -105,7 +105,7 @@ public void Only_accept_null_ServiceProvider_for_unit_testing_purpose_when_we_pr public async Task Return_execution_durations_in_readable_and_adjusted_string_format() { var smokeTestProvider = Stub.ASmokeTestProvider(new AlwaysPositiveSmokeTest(TimeSpan.FromSeconds(1)).WithoutCategory(), new AlwaysPositiveSmokeTest(TimeSpan.FromMilliseconds(30)).WithoutCategory(), new AlwaysPositiveSmokeTest(TimeSpan.FromSeconds(1.2)).WithoutCategory()); - var smokeController = new SmokeController(Substitute.For(), null, smokeTestProvider); + var smokeController = new SmokeController(Stub.ASmokeTestConfiguration(), null, smokeTestProvider); var response = await smokeController.ExecuteSmokeTests(); @@ -121,7 +121,7 @@ public async Task Return_execution_durations_in_readable_and_adjusted_string_for public async Task Return_501_error_code_not_implemented_when_no_smoke_test_is_found() { var smokeTestProvider = Stub.ASmokeTestProvider(); - var smokeController = new SmokeController(Substitute.For(), null, smokeTestProvider); + var smokeController = new SmokeController(Stub.ASmokeTestConfiguration(), null, smokeTestProvider); var response = await smokeController.ExecuteSmokeTests(); @@ -135,8 +135,8 @@ public async Task Return_501_error_code_not_implemented_when_no_smoke_test_is_fo [Test] public async Task Return_503_Service_Unavailable_when_SmokeMe_is_disabled_in_configuration() { - var configuration = Stub.AConfiguration(false); - + var configuration = Stub.ASmokeTestConfiguration(isEnabled: false); + var smokeTestProvider = Stub.ASmokeTestProvider(new AlwaysPositiveSmokeTest(TimeSpan.Zero).WithAssociatedCategories("DB"), new SmokeTestThrowingAnAccessViolationException(TimeSpan.Zero).WithoutCategory()); var controller = new SmokeController(configuration, null, smokeTestProvider); @@ -151,11 +151,12 @@ public async Task Return_503_Service_Unavailable_when_SmokeMe_is_disabled_in_con [Test] public async Task Only_Execute_corresponding_SmokeTest_when_specifying_one_Category() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration, new FeatureToggle("featureToggledSmokeTest", false), new FeatureToggle("mustTimeOut", false)); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig, new FeatureToggle("featureToggledSmokeTest", false), new FeatureToggle("mustTimeOut", false)); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var response = await controller.ExecuteSmokeTests("DB"); @@ -172,13 +173,14 @@ public async Task Only_Execute_corresponding_SmokeTest_when_specifying_one_Categ [Test] public async Task Only_Execute_SmokeTest_with_Specified_Categories() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration); - serviceProvider.GetService(typeof(IConfiguration)).Returns(configuration); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig); + serviceProvider.GetService(typeof(IConfiguration)).Returns(rawConfig); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var response = await controller.ExecuteSmokeTests("FailingSaMere", "DB"); @@ -198,11 +200,12 @@ public async Task Only_Execute_SmokeTest_with_Specified_Categories() [Test] public async Task Return_NotImplemented_501_with_proper_didactic_message_when_specifying_undeclared_Category() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var nonExistingCategoryName = "PortnaouaqThisIsNotAnExistingCategory"; var response = await controller.ExecuteSmokeTests(nonExistingCategoryName); @@ -211,17 +214,18 @@ public async Task Return_NotImplemented_501_with_proper_didactic_message_when_sp var reportDto = response.ExtractValue(); Check.That(reportDto.Results.TotalOfTestsRan).IsEqualTo(0); - Check.That(reportDto.Status).IsEqualTo(@$"No smoke test with [Category(""{nonExistingCategoryName}"")] attribute have been found in your executing assemblies. Check that you have one or more (not ignored) ICheckSmoke types in your code base with the declared attribute [Category(""{nonExistingCategoryName}"")] so that the SmokeMe library can detect and run them."); + Check.That(reportDto.Status).IsEqualTo(@$"No smoke test with [Category(""{nonExistingCategoryName}"")] attribute have been found in your executing assemblies. Check that you have one or more (not ignored) {nameof(SmokeTest)} types in your code base with the declared attribute [Category(""{nonExistingCategoryName}"")] so that the SmokeMe library can detect and run them."); } [Test] public async Task Return_NotImplemented_501_with_proper_didactic_message_when_specifying_multiple_undeclared_Categories() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var response = await controller.ExecuteSmokeTests("Cat1", "Cat2", "Cat3"); @@ -229,17 +233,18 @@ public async Task Return_NotImplemented_501_with_proper_didactic_message_when_sp var reportDto = response.ExtractValue(); Check.That(reportDto.Results.TotalOfTestsRan).IsEqualTo(0); - Check.That(reportDto.Status).IsEqualTo(@$"No smoke test with [Category(""Cat1"")] or [Category(""Cat2"")] or [Category(""Cat3"")] attributes have been found in your executing assemblies. Check that you have one or more (not ignored) ICheckSmoke types in your code base with the expected declared [Category] attributes so that the SmokeMe library can detect and run them."); + Check.That(reportDto.Status).IsEqualTo(@$"No smoke test with [Category(""Cat1"")] or [Category(""Cat2"")] or [Category(""Cat3"")] attributes have been found in your executing assemblies. Check that you have one or more (not ignored) {nameof(SmokeTest)} types in your code base with the expected declared [Category] attributes so that the SmokeMe library can detect and run them."); } [Test] public async Task Not_run_SmokeTests_with_Ignore_Attribute() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var response = await controller.ExecuteSmokeTests("Awkward"); @@ -247,17 +252,18 @@ public async Task Not_run_SmokeTests_with_Ignore_Attribute() var reportDto = response.ExtractValue(); Check.That(reportDto.Results.TotalOfTestsRan).IsEqualTo(0); - Check.That(reportDto.Status).IsEqualTo(@$"No smoke test with [Category(""Awkward"")] attribute have been found in your executing assemblies. Check that you have one or more (not ignored) ICheckSmoke types in your code base with the declared attribute [Category(""Awkward"")] so that the SmokeMe library can detect and run them."); + Check.That(reportDto.Status).IsEqualTo(@$"No smoke test with [Category(""Awkward"")] attribute have been found in your executing assemblies. Check that you have one or more (not ignored) {nameof(SmokeTest)} types in your code base with the declared attribute [Category(""Awkward"")] so that the SmokeMe library can detect and run them."); } [Test] public async Task Publish_the_executed_SmokeTestCategories_when_specified_by_the_client() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var response = await controller.ExecuteSmokeTests("FailingSaMere", "DB"); @@ -271,13 +277,14 @@ public async Task Publish_the_executed_SmokeTestCategories_when_specified_by_the [Test] public async Task Publish_the_Categories_of_every_executed_SmokeTest_when_existing() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration); - serviceProvider.GetService(typeof(IConfiguration)).Returns(configuration); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig); + serviceProvider.GetService(typeof(IConfiguration)).Returns(rawConfig); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var response = await controller.ExecuteSmokeTests(); @@ -299,11 +306,12 @@ public async Task Publish_the_Categories_of_every_executed_SmokeTest_when_existi [Test] public async Task Publish_the_Type_FullName_of_every_SmokeTest_even_when_they_Timeout_or_are_Discared_or_Ignored() { - var configuration = Stub.AConfiguration(true, globalTimeoutInMsec: 100); - var serviceProvider = Stub.ACompleteServiceProvider(configuration, new FeatureToggle("featureToggledSmokeTest", false), new FeatureToggle("mustTimeOut", true)); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true, globalTimeoutInMsec: 100); + var rawConfig = Stub.AConfiguration(true, globalTimeoutInMsec: 100); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig, new FeatureToggle("featureToggledSmokeTest", false), new FeatureToggle("mustTimeOut", true)); var smokeTestAutoFinder = new SmokeTestAutoFinder(serviceProvider); - var controller = new SmokeController(configuration, serviceProvider, smokeTestAutoFinder); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestAutoFinder); var response = await controller.ExecuteSmokeTests(); @@ -319,7 +327,7 @@ public async Task Publish_the_Type_FullName_of_every_SmokeTest_even_when_they_Ti Check.That(reportDto.Results.TotalOfTestsRan).IsEqualTo(5); Check.That(reportDto.Results.TotalOfTestsDetected).IsEqualTo(8); - + /// Booking smoke test must have timeout Check.That(reportDto.Results.Timeouts[0].SmokeTestType).IsEqualTo(typeof(BookingSmokeTest).FullName); Check.That(reportDto.Results.Timeouts[0].Status).IsEqualTo(Status.Timeout); @@ -333,12 +341,13 @@ public async Task Publish_the_Type_FullName_of_every_SmokeTest_even_when_they_Ti [Test] public async Task Be_able_to_discard_Test_execution_when_Indicated_with_MustBeDiscarded_set_to_true() { - var configuration = Stub.AConfiguration(true); - var serviceProvider = Stub.ACompleteServiceProvider(configuration, new FeatureToggle("featureToggledSmokeTest", false), new FeatureToggle("mustTimeOut", false)); + var smokeConfig = Stub.ASmokeTestConfiguration(isEnabled: true); + var rawConfig = Stub.AConfiguration(true); + var serviceProvider = Stub.ACompleteServiceProvider(rawConfig, new FeatureToggle("featureToggledSmokeTest", false), new FeatureToggle("mustTimeOut", false)); var smokeTestProvider = Stub.ASmokeTestProvider(new AlwaysPositiveSmokeTest(TimeSpan.FromMilliseconds(50)).WithoutCategory(), new FeatureToggledAlwaysPositiveSmokeTest(serviceProvider.GetService(typeof(IToggleFeatures)) as IToggleFeatures, TimeSpan.FromMilliseconds(50)).WithoutCategory()); - var controller = new SmokeController(configuration, serviceProvider, smokeTestProvider); + var controller = new SmokeController(smokeConfig, serviceProvider, smokeTestProvider); var response = await controller.ExecuteSmokeTests(); @@ -350,7 +359,7 @@ public async Task Be_able_to_discard_Test_execution_when_Indicated_with_MustBeDi Check.That(reportDto.Results.NbOfDiscards).IsEqualTo(1); Check.That(reportDto.Results.Successes.Select(x => x.SmokeTestName)).ContainsExactly("Always positive smoke test after a delay"); Check.That(reportDto.Results.Discards.Select(x => x.SmokeTestName)).ContainsExactly("Feature toggled test"); - + Check.That(reportDto.Results.Successes.Select(x=> x.Status)).ContainsExactly(Status.Executed); Check.That(reportDto.Results.Discards.Select(x=> x.Status)).ContainsExactly(Status.Discarded); } @@ -360,4 +369,4 @@ private static void ForceTheLoadingOfTheSampleExternalSmokeTestsAssembly() new BookingSmokeTest(Substitute.For(), Substitute.For()); } } -} \ No newline at end of file +} diff --git a/Samples/Sample.31.Api/FakeDomain/NumberProvider.cs b/SmokeMe.Tests/FakeDomain/NumberProvider.cs similarity index 95% rename from Samples/Sample.31.Api/FakeDomain/NumberProvider.cs rename to SmokeMe.Tests/FakeDomain/NumberProvider.cs index 114ab8a..3326d6e 100644 --- a/Samples/Sample.31.Api/FakeDomain/NumberProvider.cs +++ b/SmokeMe.Tests/FakeDomain/NumberProvider.cs @@ -1,4 +1,4 @@ -using Diverse; +using Diverse; namespace Sample.Api.FakeDomain { diff --git a/SmokeMe.Tests/Helpers/Stub.cs b/SmokeMe.Tests/Helpers/Stub.cs index dda3b14..ad15f1c 100644 --- a/SmokeMe.Tests/Helpers/Stub.cs +++ b/SmokeMe.Tests/Helpers/Stub.cs @@ -28,6 +28,14 @@ public static IFindSmokeTests ASmokeTestProvider(params SmokeTestInstanceWithMet return smokeTestProvider; } + public static ISmokeTestConfiguration ASmokeTestConfiguration(bool? isEnabled = null, int? globalTimeoutInMsec = null) + { + var config = Substitute.For(); + config.GlobalTimeout.Returns(TimeSpan.FromMilliseconds(globalTimeoutInMsec ?? Constants.GlobalTimeoutInMsecDefaultValue)); + config.IsExecutionEnabled.Returns(isEnabled ?? true); + return config; + } + public static IConfiguration AConfiguration(bool? isEnabled = null, int? globalTimeoutInMsec = null) { var configuration = Substitute.For(); @@ -60,7 +68,7 @@ private static IServiceProvider FeatureToggles(params FeatureToggle[] featureTog { toggleFeatures.IsEnabled(featureToggle.FeatureName).Returns(featureToggle.FeatureValue); } - + serviceProvider.GetService(typeof(IToggleFeatures)).Returns(toggleFeatures); return serviceProvider; } @@ -74,4 +82,4 @@ public static IServiceProvider ACompleteServiceProvider(IConfiguration configura return aCompleteServiceProvider; } } -} \ No newline at end of file +} diff --git a/SmokeMe.Tests/SmokeMe.Tests.csproj b/SmokeMe.Tests/SmokeMe.Tests.csproj index 35e4403..69de1d7 100644 --- a/SmokeMe.Tests/SmokeMe.Tests.csproj +++ b/SmokeMe.Tests/SmokeMe.Tests.csproj @@ -1,8 +1,8 @@  - netcoreapp3.1 - + net9.0 + latest false @@ -11,19 +11,17 @@ - - - + + + - - + - + - diff --git a/SmokeMe.Tests/Unit/SmokeTestAutoFinderShould.cs b/SmokeMe.Tests/Unit/SmokeTestAutoFinderShould.cs index e49e8d3..298b784 100644 --- a/SmokeMe.Tests/Unit/SmokeTestAutoFinderShould.cs +++ b/SmokeMe.Tests/Unit/SmokeTestAutoFinderShould.cs @@ -1,7 +1,6 @@ using System.Linq; using NFluent; using NUnit.Framework; -using Sample.Api.SmokeTests; using SmokeMe.Tests.Helpers; using SmokeMe.Tests.SmokeTests; @@ -20,9 +19,8 @@ public void Instantiate_all_concrete_classes_implementing_ITestWithSmoke() var smokeTests = smokeTestAutoFinder.FindAllSmokeTestsToRun(); Check.That(smokeTests.Select(x => x.SmokeTest.GetType())) - .Contains(typeof(AlwaysPositiveSmokeTest), - typeof(SmokeTestThrowingAnAccessViolationException), - typeof(FlippingSmokeTest)); + .Contains(typeof(AlwaysPositiveSmokeTest), + typeof(SmokeTestThrowingAnAccessViolationException)); } } -} \ No newline at end of file +} diff --git a/SmokeMe.sln b/SmokeMe.sln index ad0c789..6351d36 100644 --- a/SmokeMe.sln +++ b/SmokeMe.sln @@ -1,10 +1,12 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30804.86 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmokeMe", "SmokeMe\SmokeMe.csproj", "{D4A8CD58-1FAF-4BDA-98F6-1B28A16718EE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmokeMe.AspNetCore", "SmokeMe.AspNetCore\SmokeMe.AspNetCore.csproj", "{B1C2D3E4-F5A6-4B7C-8D9E-0F1A2B3C4D5E}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmokeMe.Tests", "SmokeMe.Tests\SmokeMe.Tests.csproj", "{3D0F52DD-7261-4EEF-A5E9-37A4F38DF412}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7008E98C-9543-48AB-9CF5-8322024B5E8D}" @@ -20,7 +22,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{2C93 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.ExternalSmokeTests", "Samples\Sample.ExternalSmokeTests\Sample.ExternalSmokeTests.csproj", "{94D582B1-490D-477F-BE25-53982B942889}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.31.Api", "Samples\Sample.31.Api\Sample.31.Api.csproj", "{38964A45-116E-4354-AF3E-0EF623B01748}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Api", "Samples\Sample.Api\Sample.Api.csproj", "{A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -32,6 +34,10 @@ Global {D4A8CD58-1FAF-4BDA-98F6-1B28A16718EE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4A8CD58-1FAF-4BDA-98F6-1B28A16718EE}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4A8CD58-1FAF-4BDA-98F6-1B28A16718EE}.Release|Any CPU.Build.0 = Release|Any CPU + {B1C2D3E4-F5A6-4B7C-8D9E-0F1A2B3C4D5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1C2D3E4-F5A6-4B7C-8D9E-0F1A2B3C4D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1C2D3E4-F5A6-4B7C-8D9E-0F1A2B3C4D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1C2D3E4-F5A6-4B7C-8D9E-0F1A2B3C4D5E}.Release|Any CPU.Build.0 = Release|Any CPU {3D0F52DD-7261-4EEF-A5E9-37A4F38DF412}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3D0F52DD-7261-4EEF-A5E9-37A4F38DF412}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D0F52DD-7261-4EEF-A5E9-37A4F38DF412}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -40,17 +46,17 @@ Global {94D582B1-490D-477F-BE25-53982B942889}.Debug|Any CPU.Build.0 = Debug|Any CPU {94D582B1-490D-477F-BE25-53982B942889}.Release|Any CPU.ActiveCfg = Release|Any CPU {94D582B1-490D-477F-BE25-53982B942889}.Release|Any CPU.Build.0 = Release|Any CPU - {38964A45-116E-4354-AF3E-0EF623B01748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38964A45-116E-4354-AF3E-0EF623B01748}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38964A45-116E-4354-AF3E-0EF623B01748}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38964A45-116E-4354-AF3E-0EF623B01748}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {94D582B1-490D-477F-BE25-53982B942889} = {2C93DF71-7730-49AA-8013-BAB917EEFF15} - {38964A45-116E-4354-AF3E-0EF623B01748} = {2C93DF71-7730-49AA-8013-BAB917EEFF15} + {A1B2C3D4-E5F6-7A8B-9C0D-E1F2A3B4C5D6} = {2C93DF71-7730-49AA-8013-BAB917EEFF15} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4FC3D105-496A-4BA9-85CA-B24A886BBDF8} diff --git a/SmokeMe/Error.cs b/SmokeMe/Error.cs index 4adba36..9609846 100644 --- a/SmokeMe/Error.cs +++ b/SmokeMe/Error.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; namespace SmokeMe { @@ -26,6 +27,7 @@ public Error(string errorMessage, Exception exception) /// /// The exception that has been catched during the execution. /// + [JsonIgnore] public Exception Exception { get; private set; } } } \ No newline at end of file diff --git a/SmokeMe/Helpers/ConfigurationExtensions.cs b/SmokeMe/Helpers/ConfigurationExtensions.cs deleted file mode 100644 index 6f6642b..0000000 --- a/SmokeMe/Helpers/ConfigurationExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.Extensions.Configuration; - -namespace SmokeMe.Helpers -{ - /// - /// Extension methods for . - /// - public static class ConfigurationExtensions - { - /// - /// Gets the Global timeout value used by the SmokeMe library (default value may be overriden through configuration file). - /// - /// The instance used by the API. - /// The Global timeout value. - public static TimeSpan GetSmokeMeGlobalTimeout(this IConfiguration configuration) - { - var globalTimeout = TimeSpan.FromMilliseconds(Constants.GlobalTimeoutInMsecDefaultValue); // default value - if (int.TryParse(configuration[Constants.GlobaltimeoutinmsecConfigurationKey], out var globalTimeoutInMsec)) - { - globalTimeout = TimeSpan.FromMilliseconds(globalTimeoutInMsec); // overriden by the one in the configuration (if valid) - } - - return globalTimeout; - } - - /// - /// Gets an indication whether the smoke tests execution is enabled or not (default true value may be overriden through configuration file). - /// - /// The instance used by the API. - /// true if the smoke test execution is enabled or not, false otherwise. - public static bool IsSmokeTestExecutionEnabled(this IConfiguration configuration) - { - if (!bool.TryParse(configuration[Constants.IsEnabledConfigurationKey], out var isEnabled)) - { - isEnabled = true; - } - - return isEnabled; - } - } -} \ No newline at end of file diff --git a/SmokeMe/ICheckSmoke.cs b/SmokeMe/ICheckSmoke.cs deleted file mode 100644 index 70bd9a4..0000000 --- a/SmokeMe/ICheckSmoke.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace SmokeMe -{ - /// - /// The ICheckSmoke interface is deprecated and MUST be replaced by SmokeTest abstract class. To do so, just replace all reference to ICheckSmoke with SmokeTest and add the 'override' keyword to your existing SmokeTestName, Description properties, but also to the Scenario() method which is now an abstract method. - /// - [Obsolete("The ICheckSmoke interface is deprecated and MUST be replaced by SmokeTest abstract class. To do so, just replace all reference to ICheckSmoke with SmokeTest and add the 'override' keyword to your existing SmokeTestName, Description properties, but also to the Scenario() method which is now an abstract method")] - public interface ICheckSmoke - { - /// - /// Breaking change: the ICheckSmoke interface is deprecated and MUST be replaced by SmokeTest abstract class. To do so, just replace all reference to ICheckSmoke with SmokeTest and add the 'override' keyword to your existing SmokeTestName, Description properties, but also to the Scenario() method which is now an abstract method. - /// - void WithTheV2MajorBreakingChangeOfSmokeMeYouMustReplaceAllYourPreviousReferenceToICheckSmokeInterfaceWithTheSmokeTestAbstractClass(); - } -} \ No newline at end of file diff --git a/SmokeMe/ISmokeTestConfiguration.cs b/SmokeMe/ISmokeTestConfiguration.cs new file mode 100644 index 0000000..61501f1 --- /dev/null +++ b/SmokeMe/ISmokeTestConfiguration.cs @@ -0,0 +1,21 @@ +using System; + +namespace SmokeMe +{ + /// + /// Configuration abstraction for the SmokeMe library. + /// Decoupled from ASP.NET Core's IConfiguration to allow usage in non-ASP.NET contexts. + /// + public interface ISmokeTestConfiguration + { + /// + /// Gets the global timeout for all smoke tests execution. + /// + TimeSpan GlobalTimeout { get; } + + /// + /// Gets a value indicating whether the smoke test execution is enabled. + /// + bool IsExecutionEnabled { get; } + } +} diff --git a/SmokeMe/Infra/SmokeTestSessionResultAdapter.cs b/SmokeMe/Infra/SmokeTestSessionResultAdapter.cs index e70bd09..db2ff8a 100644 --- a/SmokeMe/Infra/SmokeTestSessionResultAdapter.cs +++ b/SmokeMe/Infra/SmokeTestSessionResultAdapter.cs @@ -1,5 +1,4 @@ using System.Linq; -using Microsoft.Extensions.Configuration; using SmokeMe.Helpers; namespace SmokeMe.Infra @@ -18,16 +17,16 @@ public static class SmokeTestSessionResultAdapter /// /// The corresponding to the external exposition model of the provided instance. public static SmokeTestsSessionReportDto Adapt(SmokeTestsSessionReport reports, ApiRuntimeDescription runtimeDescription, string[] categories, - IConfiguration configuration) + ISmokeTestConfiguration configuration) { - // Adapt the array of results + // Adapt the array of results var resultsDto = reports.Results .Select(r => new SmokeTestResultWithMetaDataDto(r.SmokeTestName, r.SmokeTestDescription, r.Outcome, r.ErrorMessage, r.Duration, r.Duration?.GetHumanReadableVersion(), r.Status, r.SmokeTestCategories, r.SmokeTestType)); // Adapt the overall wrapper (with runtime description information too) - var result = new SmokeTestsSessionReportDto(reports, runtimeDescription, resultsDto, categories, configuration.GetSmokeMeGlobalTimeout()); + var result = new SmokeTestsSessionReportDto(reports, runtimeDescription, resultsDto, categories, configuration.GlobalTimeout); return result; } } -} \ No newline at end of file +} diff --git a/SmokeMe/Smoke.xml b/SmokeMe/Smoke.xml deleted file mode 100644 index c8ee4fc..0000000 --- a/SmokeMe/Smoke.xml +++ /dev/null @@ -1,265 +0,0 @@ - - - - SmokeMe - - - - - Constants for the /smoke library - - - - - Gets the name of the configuration key for the smoke test global execution timeout. - - - - - Gets the default value for the global timeout in milliseconds if the () configuration key is not used to override it. - - - - - Executes smoke test declared for this API. - Smoke tests are a set of short functional tests checking that the minimum viable prerequisites for this API is fine. - - - - - Instantiates a . - - The configuration of the API. - A Service provider to be used to instantiate smoke tests. - (optional) A smoke test provider (used for unit testing purpose). - - - - Execute all registered Smoke Tests for this API. - - The of the Smoke tests execution. - - - - Error during a excecution. - - - - - Instantiates an . - - The error message. - An exception that has been catched during the execution. - - - - The error message. - - - - - The exception that has been catched during the execution. - - - - - Extension methods related to the usage of Reflection. - - - - - Gets a value indicating whether a given is . - - The to check. - true if the is a instance, false otherwise. - - - - Gets all the constructors of a ordered by their number of parameters desc. - - The considered . - All the constructors of a ordered by their number of parameters desc. - - - - Responsible to find smoke tests to be run within an executable. - - - - - Instantiates all the instances that have been found in the running code. - - A collection of instances. - - - - Contains scenario to be executed in order to 'smoke test' something. - (a Smoke test actually). - Note: all the services and dependencies you need for it will be automatically - injected by the lib via the ASP.NET IServiceProvider of your API - (classical constructor-based injection). - - - - - Executes the scenario of this Smoke Test. - - The of this Smoke test. - - - - Responsible to find and instantiate smoke tests to be run within an executable. - - - - - Instantiates a - - The (IoC) instance needed to instantiate instances. - - - - Finds all smoke tests scenarii that have to be executed for this API. - - The collection of all instance declared in this API to be executed. - - - - Result of a execution. - - - - - Indicates whether the outcome of this execution is positive or not. - - - - - Gets the associated to this execution. - - - - - Instantiates a . - - The error message associated to the smoke test execution. - The associated to the smoke test execution. - - - - Instantiates a . - - The outcome of this execution. - - - - Returns a string representing the object. - - A string representing the object. - - - - Runner for instances. - - - - - Executes instances that has been found for this API. - - The instances to be executed in parallel. - The maximum amount of time allowed for all instances to be executed. - The . - - - - Represents a failed (due to timeout) smoke test session. - - - - - Instantiates a . - - The global timeout expiration that led to his failure. - - - - Result of a smoke test session. - - - - - Gets all the reports of this Smoke test session. - - - - - Returns true if the Smoke test session is succeeded (i.e. all smoke test succeeded), false otherwise. - - - - - Instantiates a . - - The reports of this Smoke test session. - Whether or not the is successful or not. - - - - Gets the API instance identifier. - - - - - Gets the API version. - - - - - Gets the OS name of this API instance. - - - - - Gets the Azure region name where this API instance is running. - - - - - Gets the number of Processors this API instance has. - - - - - Result of a execution but with its . - - - - - Indicates whether the outcome of this execution is positive or not. - - - - - Gets the associated to this execution. - - - - - Gets the duration of this execution. - - - - - Instantiates a . - - The associated with this execution. - The duration of this execution. - - - - Returns a string representing the object. - - A string representing the object. - - - diff --git a/SmokeMe/SmokeMe.csproj b/SmokeMe/SmokeMe.csproj index 3947785..cc19fde 100644 --- a/SmokeMe/SmokeMe.csproj +++ b/SmokeMe/SmokeMe.csproj @@ -1,25 +1,31 @@  - netcoreapp3.1 - 2.2.0 + netstandard2.0 + latest + 3.0.0 Thomas PIERRAIN (use case driven) 42 skillz - A convention-based dotnet plugin that will automatically expose all your declared smoke tests behind a /smoke resource in your API. - Copyright © Thomas PIERRAIN 2021 + A convention-based library for declaring and running smoke tests. Framework-agnostic core — use SmokeMe.AspNetCore for ASP.NET Core integration. + Copyright © Thomas PIERRAIN 2024 LICENSE https://github.com/42skillz/SmokeMe smoke-icon.jpg https://github.com/42skillz/SmokeMe SmokeTests tests ContinuousDelivery Continuous Delivery Smoke Tests SmokeMe - Fix: -- Fix an Issue when no smoke test are implemented yet in a solution (an exception was thrown in some cases) + v3.0.0 — Breaking changes: +- Split into two packages: SmokeMe (core, netstandard2.0) and SmokeMe.AspNetCore (net8.0/net9.0) +- New ISmokeTestConfiguration interface replaces direct IConfiguration usage +- Registration is now explicit: AddSmokeMe() + MapSmokeEndpoint() +- Removed deprecated ICheckSmoke interface (use SmokeTest abstract class) +- Replaced Newtonsoft.Json with System.Text.Json +- Removed embedded Swashbuckle and API Versioning dependencies -New features: -- A new property is available in the Report DTO: GlobalTimeoutInMSec +Your existing SmokeTest classes work as-is. Only the hosting setup changes. +Full migration guide: https://github.com/42skillz/SmokeMe/blob/main/MIGRATION-v2-to-v3.md SmokeMe - 2.2.0.0 - 2.2.0.0 + 3.0.0.0 + 3.0.0.0 @@ -32,13 +38,7 @@ New features: - - - - - - - + diff --git a/SmokeMe/SmokeMe.xml b/SmokeMe/SmokeMe.xml index f41b0fa..aecd359 100644 --- a/SmokeMe/SmokeMe.xml +++ b/SmokeMe/SmokeMe.xml @@ -34,26 +34,6 @@ Gets the default value for the global timeout in milliseconds if the () configuration key is not used to override it. - - - Executes smoke test declared for this API. - Smoke tests are a set of short functional tests checking that the minimum viable prerequisites for this API is fine. - - - - - Instantiates a . - - The configuration of the API. - A Service provider to be used to instantiate smoke tests. - (optional) A smoke test provider (used for unit testing purpose). - - - - Execute all registered Smoke Tests for this API. - - The of the Smoke tests execution. - Error during a excecution. @@ -76,25 +56,6 @@ The exception that has been catched during the execution. - - - Extension methods for . - - - - - Gets the Global timeout value used by the SmokeMe library (default value may be overriden through configuration file). - - The instance used by the API. - The Global timeout value. - - - - Gets an indication whether the smoke tests execution is enabled or not (default true value may be overriden through configuration file). - - The instance used by the API. - true if the smoke test execution is enabled or not, false otherwise. - Extension methods related to the usage of Reflection. @@ -138,16 +99,6 @@ The type we want to check. true if the Type has an [Ignore()] attribute; false otherwise. - - - The ICheckSmoke interface is deprecated and MUST be replaced by SmokeTest abstract class. To do so, just replace all reference to ICheckSmoke with SmokeTest and add the 'override' keyword to your existing SmokeTestName, Description properties, but also to the Scenario() method which is now an abstract method. - - - - - Breaking change: the ICheckSmoke interface is deprecated and MUST be replaced by SmokeTest abstract class. To do so, just replace all reference to ICheckSmoke with SmokeTest and add the 'override' keyword to your existing SmokeTestName, Description properties, but also to the Scenario() method which is now an abstract method. - - Responsible to find smoke tests to be run within an executable. @@ -258,7 +209,7 @@ Adapter from SmokeMe internal model to SmokeMe external DTOs. - + Adapts a instance to a one. @@ -346,6 +297,39 @@ The associated to that smoke test execution. + + + Configuration abstraction for the SmokeMe library. + Decoupled from ASP.NET Core's IConfiguration to allow usage in non-ASP.NET contexts. + + + + + Gets the global timeout for all smoke tests execution. + + + + + Gets a value indicating whether the smoke test execution is enabled. + + + + + Options for configuring the SmokeMe library. + + + + + Gets or sets the global timeout for all smoke tests execution. + Default: 30 seconds. + + + + + Gets or sets a value indicating whether the smoke test execution is enabled. + Default: true. + + Smoke test/scenario/code to be executed in order to check that a minimum diff --git a/SmokeMe/SmokeMeOptions.cs b/SmokeMe/SmokeMeOptions.cs new file mode 100644 index 0000000..ce41c10 --- /dev/null +++ b/SmokeMe/SmokeMeOptions.cs @@ -0,0 +1,22 @@ +using System; + +namespace SmokeMe +{ + /// + /// Options for configuring the SmokeMe library. + /// + public class SmokeMeOptions : ISmokeTestConfiguration + { + /// + /// Gets or sets the global timeout for all smoke tests execution. + /// Default: 30 seconds. + /// + public TimeSpan GlobalTimeout { get; set; } = TimeSpan.FromMilliseconds(Constants.GlobalTimeoutInMsecDefaultValue); + + /// + /// Gets or sets a value indicating whether the smoke test execution is enabled. + /// Default: true. + /// + public bool IsExecutionEnabled { get; set; } = true; + } +} diff --git a/SmokeMe/SmokeTestResultWithMetaData.cs b/SmokeMe/SmokeTestResultWithMetaData.cs index 976fdc6..1e15da3 100644 --- a/SmokeMe/SmokeTestResultWithMetaData.cs +++ b/SmokeMe/SmokeTestResultWithMetaData.cs @@ -1,5 +1,4 @@ using System; -using Microsoft.AspNetCore.Routing.Matching; namespace SmokeMe { diff --git a/SmokeMe/SmokeTestRunner.cs b/SmokeMe/SmokeTestRunner.cs index c0f2a8d..8f0bdc6 100644 --- a/SmokeMe/SmokeTestRunner.cs +++ b/SmokeMe/SmokeTestRunner.cs @@ -105,7 +105,7 @@ private static async Task> GetSmokeTest private static bool IsNotAFalsePositive(Task allSmokeTasks) { - return !allSmokeTasks.IsCompletedSuccessfully; + return !(allSmokeTasks.IsCompleted && allSmokeTasks.Status == TaskStatus.RanToCompletion); } private static async Task StopWatchSafeSmokeTestExecution(SmokeTestInstanceWithMetaData smokeTestWithMetaData) diff --git a/SmokeMe/Status.cs b/SmokeMe/Status.cs index 48b3054..f6f7ef1 100644 --- a/SmokeMe/Status.cs +++ b/SmokeMe/Status.cs @@ -1,5 +1,8 @@ +using System.Text.Json.Serialization; + namespace SmokeMe { + [JsonConverter(typeof(JsonStringEnumConverter))] public enum Status { Executed, @@ -7,4 +10,4 @@ public enum Status Discarded, Ignored } -} \ No newline at end of file +}