From 94ab454c0b5f0e094db564ba72e8e6ab40b86a05 Mon Sep 17 00:00:00 2001 From: Mathieu Gamache Date: Wed, 20 Dec 2023 18:15:43 -0500 Subject: [PATCH] Feature/idp 823 explicit contract first flow (#21) * [IDP-823] Add parameter to control Contract First vs Code first and opt-in code generation for validation * Add limitation of running system test locally and workaround --------- Co-authored-by: Mathieu Gamache Co-authored-by: Anthony Simmon --- README.md | 33 +++++++ Run-SystemTest.ps1 | 10 +- src/WebApiDebugger/WebApiDebugger.csproj | 5 + src/Workleap.OpenApi.MSBuild.sln | 9 +- .../CodeFirstProcess.cs | 53 +++++++++++ .../ContractFirstProcess.cs | 93 +++++++++++++++++++ .../SpectralManager.cs | 2 +- .../ValidateOpenApiTask.cs | 93 +++++++++++-------- .../tools/Workleap.OpenApi.MSBuild.targets | 8 ++ .../Controllers/WeatherForecastController.cs | 0 .../WeatherManagementController.cs | 0 .../Program.cs | 0 .../Properties/launchSettings.json | 26 ++++++ .../WeatherForecast.cs | 0 ...ebApi.MsBuild.SystemTest.CodeFirst.csproj} | 9 +- .../appsettings.Development.json | 0 .../appsettings.json | 0 .../openapi-v1-management.yaml | 0 .../openapi-v1.yaml | 0 .../Controllers/WeatherForecastController.cs | 36 +++++++ .../WeatherManagementController.cs | 30 ++++++ .../Program.cs | 28 ++++++ .../Properties/launchSettings.json | 0 .../WeatherForecast.cs | 12 +++ ...pi.MsBuild.SystemTest.ContractFirst.csproj | 22 +++++ .../appsettings.Development.json | 8 ++ .../appsettings.json | 9 ++ .../openapi-v1-management.yaml | 47 ++++++++++ .../openapi-v1.yaml | 64 +++++++++++++ 29 files changed, 550 insertions(+), 47 deletions(-) create mode 100644 src/Workleap.OpenApi.MSBuild/CodeFirstProcess.cs create mode 100644 src/Workleap.OpenApi.MSBuild/ContractFirstProcess.cs rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/Controllers/WeatherForecastController.cs (100%) rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/Controllers/WeatherManagementController.cs (100%) rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/Program.cs (100%) create mode 100644 src/tests/WebApi.MsBuild.SystemTest.CodeFirst/Properties/launchSettings.json rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/WeatherForecast.cs (100%) rename src/tests/{WebApi.MsBuild.SystemTest/WebApi.MsBuild.SystemTest.csproj => WebApi.MsBuild.SystemTest.CodeFirst/WebApi.MsBuild.SystemTest.CodeFirst.csproj} (62%) rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/appsettings.Development.json (100%) rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/appsettings.json (100%) rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/openapi-v1-management.yaml (100%) rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.CodeFirst}/openapi-v1.yaml (100%) create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherForecastController.cs create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherManagementController.cs create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Program.cs rename src/tests/{WebApi.MsBuild.SystemTest => WebApi.MsBuild.SystemTest.ContractFirst}/Properties/launchSettings.json (100%) create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WeatherForecast.cs create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WebApi.MsBuild.SystemTest.ContractFirst.csproj create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.Development.json create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.json create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1-management.yaml create mode 100644 src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1.yaml diff --git a/README.md b/README.md index 142b3b5..5d7ddfc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,36 @@ # Workleap.OpenApi.MSBuild Validates at build time that the OpenAPI specification files extracted from the ASP.NET Core Web API being built conform to Workleap API guidelines. + +Depending if the user chose the Contract-First or Code-First development mode this MSBuild task will: + +- Install tools: [OasDiff](https://github.com/Tufin/oasdiff), [Spectral](https://github.com/stoplightio/spectral), [SwashbuckleCLI](https://github.com/domaindrivendev/Swashbuckle.AspNetCore?tab=readme-ov-file#swashbuckleaspnetcorecli) +- Generate the OpenAPI specification file from the associated Web API +- Validate [Workleap Spectral rules](https://github.com/gsoft-inc/wl-api-guidelines/blob/main/.spectral.yaml) +- Compare the given OpenAPI specification file with the generated one + +## How it works + +[Official Documentation](https://learn.microsoft.com/en-us/visualstudio/msbuild/tutorial-custom-task-code-generation?view=vs-2022#include-msbuild-properties-and-targets-in-a-package) + +For the TLDR version: + +- The entry point is `ValidateOpenApiTask.ExecuteAsync()` and will be executed after the referencing project is built. This is defined in `./src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets` as a `UsingTask.TaskName` +- The default value are defined in the property group on the target `ValidateOpenApi` in this file `./src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets` + +## How to test locally + +### How debug the project + +Since it's a MSBuild task named `ValidateOpenApi` you can run it this command: `msbuild /t:ValidateOpenApi`. `./src/WebApiDebugger` already have a `launchSettings.json` named `ValidateOpenApi` that you can run in debug. + +### With the system test + +This command `./Run-SystemTest.ps1` will: + +1. Pack the MSBuild library +2. Build the WebApi.MsBuild.SystemTest and inject the previously packed library + +Be careful since it will update the project dependencies to use a local version: do not commit this. + +Also if you run it multiple time on the same branch you need to clear the local cache `%UserProfile%\.nuget\packages\workleap.openapi.msbuild` since the name won't change. \ No newline at end of file diff --git a/Run-SystemTest.ps1 b/Run-SystemTest.ps1 index 3ef0b3b..456a005 100644 --- a/Run-SystemTest.ps1 +++ b/Run-SystemTest.ps1 @@ -14,14 +14,20 @@ Process { $workingDir = Join-Path $PSScriptRoot "src" $outputDir = Join-Path $PSScriptRoot ".output" - $sysTestDir = Join-Path $PSScriptRoot "src/tests/WebApi.MsBuild.SystemTest" + $contractFirstSysTestDir = Join-Path $PSScriptRoot "src/tests/WebApi.MsBuild.SystemTest.ContractFirst" + $codeFirstSysTestDir = Join-Path $PSScriptRoot "src/tests/WebApi.MsBuild.SystemTest.CodeFirst" try { Push-Location $workingDir Exec { & dotnet pack -c Release -o "$outputDir" } - Push-Location $sysTestDir + Push-Location $contractFirstSysTestDir + + Exec { & dotnet add package Workleap.OpenApi.MSBuild --prerelease --source $outputDir } + Exec { & dotnet build -c Release } + + Push-Location $codeFirstSysTestDir Exec { & dotnet add package Workleap.OpenApi.MSBuild --prerelease --source $outputDir } Exec { & dotnet build -c Release } diff --git a/src/WebApiDebugger/WebApiDebugger.csproj b/src/WebApiDebugger/WebApiDebugger.csproj index ae4abf1..2a134db 100644 --- a/src/WebApiDebugger/WebApiDebugger.csproj +++ b/src/WebApiDebugger/WebApiDebugger.csproj @@ -15,6 +15,11 @@ true + + ContractFirst + true + + diff --git a/src/Workleap.OpenApi.MSBuild.sln b/src/Workleap.OpenApi.MSBuild.sln index 69bc798..17d5350 100644 --- a/src/Workleap.OpenApi.MSBuild.sln +++ b/src/Workleap.OpenApi.MSBuild.sln @@ -19,7 +19,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiDebugger", "WebApiDeb EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4DDE83BF-D190-4CC9-AD36-E9250DABB27D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.MsBuild.SystemTest", "tests\WebApi.MsBuild.SystemTest\WebApi.MsBuild.SystemTest.csproj", "{B8A81C76-574B-40FD-B34B-FA893D6C1423}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.MsBuild.SystemTest.ContractFirst", "tests\WebApi.MsBuild.SystemTest.ContractFirst\WebApi.MsBuild.SystemTest.ContractFirst.csproj", "{B8A81C76-574B-40FD-B34B-FA893D6C1423}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.MsBuild.SystemTest.CodeFirst", "tests\WebApi.MsBuild.SystemTest.CodeFirst\WebApi.MsBuild.SystemTest.CodeFirst.csproj", "{575E14D8-52E8-4B5B-A93C-D3E86E30BD3C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,9 +48,14 @@ Global {B8A81C76-574B-40FD-B34B-FA893D6C1423}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8A81C76-574B-40FD-B34B-FA893D6C1423}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8A81C76-574B-40FD-B34B-FA893D6C1423}.Release|Any CPU.Build.0 = Release|Any CPU + {575E14D8-52E8-4B5B-A93C-D3E86E30BD3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {575E14D8-52E8-4B5B-A93C-D3E86E30BD3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {575E14D8-52E8-4B5B-A93C-D3E86E30BD3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {575E14D8-52E8-4B5B-A93C-D3E86E30BD3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {A4C90BE2-889F-46BB-8B4D-2219B1084695} = {07F66FC2-73B9-44C7-843C-36C13905AE9B} {B8A81C76-574B-40FD-B34B-FA893D6C1423} = {4DDE83BF-D190-4CC9-AD36-E9250DABB27D} + {575E14D8-52E8-4B5B-A93C-D3E86E30BD3C} = {4DDE83BF-D190-4CC9-AD36-E9250DABB27D} EndGlobalSection EndGlobal diff --git a/src/Workleap.OpenApi.MSBuild/CodeFirstProcess.cs b/src/Workleap.OpenApi.MSBuild/CodeFirstProcess.cs new file mode 100644 index 0000000..3c74d6d --- /dev/null +++ b/src/Workleap.OpenApi.MSBuild/CodeFirstProcess.cs @@ -0,0 +1,53 @@ +namespace Workleap.OpenApi.MSBuild; + +/// +/// For a Code First approach it will: +/// 1. (if not disabled) Generate the OpenAPI specification files from the code +/// 2. Validate the OpenAPI specification files base on spectral rules +/// +internal class CodeFirstProcess +{ + private const string DisableSpecGenEnvVarName = "WL_DISABLE_SPECGEN"; + + private readonly SpectralManager _spectralManager; + private readonly SwaggerManager _swaggerManager; + + internal CodeFirstProcess(SpectralManager spectralManager, SwaggerManager swaggerManager) + { + this._spectralManager = spectralManager; + this._swaggerManager = swaggerManager; + } + + internal async Task Execute( + string[] openApiSpecificationFiles, + string[] openApiSwaggerDocumentNames, + string openApiSpectralRulesetUrl, + CancellationToken cancellationToken) + { + var isGenerationEnabled = string.Equals(Environment.GetEnvironmentVariable(DisableSpecGenEnvVarName), "true", StringComparison.OrdinalIgnoreCase); + + await this.InstallDependencies(isGenerationEnabled, cancellationToken); + + if (isGenerationEnabled) + { + var generateOpenApiDocsPath = (await this._swaggerManager.RunSwaggerAsync(openApiSwaggerDocumentNames, cancellationToken)).ToList(); + } + + await this._spectralManager.RunSpectralAsync(openApiSpecificationFiles, openApiSpectralRulesetUrl, cancellationToken); + } + + private async Task InstallDependencies( + bool isGenerationEnable, + CancellationToken cancellationToken) + { + var installationTasks = new List(); + installationTasks.Add(this._spectralManager.InstallSpectralAsync(cancellationToken)); + + if (!isGenerationEnable) + { + installationTasks.Add(this._swaggerManager.InstallSwaggerCliAsync(cancellationToken)); + } + + await Task.WhenAll(installationTasks); + } +} \ No newline at end of file diff --git a/src/Workleap.OpenApi.MSBuild/ContractFirstProcess.cs b/src/Workleap.OpenApi.MSBuild/ContractFirstProcess.cs new file mode 100644 index 0000000..1c287fe --- /dev/null +++ b/src/Workleap.OpenApi.MSBuild/ContractFirstProcess.cs @@ -0,0 +1,93 @@ +namespace Workleap.OpenApi.MSBuild; + +/// +/// For a Contract First approach it will: +/// 1. Validate the OpenAPI specification files base on spectral rules +/// 2. If is enabled, will generate the OpenAPI specification files from the code and validate if it match the provided specifications. +/// +internal class ContractFirstProcess +{ + private readonly ILoggerWrapper _loggerWrapper; + private readonly SpectralManager _spectralManager; + private readonly SwaggerManager _swaggerManager; + private readonly OasdiffManager _oasdiffManager; + + internal ContractFirstProcess(ILoggerWrapper loggerWrapper, SpectralManager spectralManager, SwaggerManager swaggerManager, OasdiffManager oasdiffManager) + { + this._loggerWrapper = loggerWrapper; + this._spectralManager = spectralManager; + this._swaggerManager = swaggerManager; + this._oasdiffManager = oasdiffManager; + } + + internal enum CompareCodeAgainstSpecFile + { + Disabled, + Enabled, + } + + internal async Task Execute( + string[] openApiSpecificationFiles, + string openApiToolsDirectoryPath, + string[] openApiSwaggerDocumentNames, + string openApiSpectralRulesetUrl, + CompareCodeAgainstSpecFile compareCodeAgainstSpecFile, + CancellationToken cancellationToken) + { + if (!this.CheckIfBaseSpecExists(openApiSpecificationFiles, openApiToolsDirectoryPath)) + { + return false; + } + + await this.InstallDependencies(compareCodeAgainstSpecFile, cancellationToken); + + if (compareCodeAgainstSpecFile == CompareCodeAgainstSpecFile.Enabled) + { + var generateOpenApiDocsPath = (await this._swaggerManager.RunSwaggerAsync(openApiSwaggerDocumentNames, cancellationToken)).ToList(); + await this._oasdiffManager.RunOasdiffAsync(openApiSpecificationFiles, generateOpenApiDocsPath, cancellationToken); + } + + await this._spectralManager.RunSpectralAsync(openApiSpecificationFiles, openApiSpectralRulesetUrl, cancellationToken); + + return true; + } + + private bool CheckIfBaseSpecExists( + string[] openApiSpecificationFiles, + string openApiToolsDirectoryPath) + { + foreach (var file in openApiSpecificationFiles) + { + if (File.Exists(file)) + { + continue; + } + + this._loggerWrapper.LogWarning( + "The file '{0}' does not exist. If you are running this for the first time, we have generated specification here '{1}' which can be used as base specification. " + + "Please copy specification file(s) to your project directory and rebuild.", + file, + openApiToolsDirectoryPath); + + return false; + } + + return true; + } + + private async Task InstallDependencies( + CompareCodeAgainstSpecFile compareCodeAgainstSpecFile, + CancellationToken cancellationToken) + { + var installationTasks = new List(); + installationTasks.Add(this._spectralManager.InstallSpectralAsync(cancellationToken)); + + if (compareCodeAgainstSpecFile == CompareCodeAgainstSpecFile.Enabled) + { + installationTasks.Add(this._swaggerManager.InstallSwaggerCliAsync(cancellationToken)); + installationTasks.Add(this._oasdiffManager.InstallOasdiffAsync(cancellationToken)); + } + + await Task.WhenAll(installationTasks); + } +} \ No newline at end of file diff --git a/src/Workleap.OpenApi.MSBuild/SpectralManager.cs b/src/Workleap.OpenApi.MSBuild/SpectralManager.cs index 7d023e5..bb6daba 100644 --- a/src/Workleap.OpenApi.MSBuild/SpectralManager.cs +++ b/src/Workleap.OpenApi.MSBuild/SpectralManager.cs @@ -55,7 +55,7 @@ public async Task RunSpectralAsync(IEnumerable swaggerDocumentPaths, str this._loggerWrapper.LogMessage("\n ******** Spectral: Validating {0} against ruleset ********", MessageImportance.High, documentPath); File.Delete(htmlReportPath); await this.GenerateSpectralReport(spectralExecutePath, documentPath, rulesetUrl, htmlReportPath, cancellationToken); - this._loggerWrapper.LogMessage("\n *********************************************************", MessageImportance.High, documentPath); + this._loggerWrapper.LogMessage("\n ****************************************************************", MessageImportance.High); } } diff --git a/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs b/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs index 9a4e204..cd77966 100644 --- a/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs +++ b/src/Workleap.OpenApi.MSBuild/ValidateOpenApiTask.cs @@ -4,28 +4,43 @@ namespace Workleap.OpenApi.MSBuild; public sealed class ValidateOpenApiTask : CancelableAsyncTask { + private const string CodeFirst = "CodeFirst"; + private const string ContractFirst = "ContractFirst"; + + /// + /// 2 supported modes: + /// - CodeFirst: Generate the OpenAPI specification files from the code + /// - ContractFirst: Will use the OpenAPI specification files provided + /// + [Required] + public string OpenApiDevelopmentMode { get; set; } = string.Empty; + + /// When Development mode is Contract first, will validate if the specification match the code. + [Required] + public bool OpenApiCompareCodeAgainstSpecFile { get; set; } = false; + /// The path of the ASP.NET Core project startup assembly directory. [Required] public string StartupAssemblyPath { get; set; } = string.Empty; /// The path of the ASP.NET Core project being built. - [Required] + [Microsoft.Build.Framework.Required] public string OpenApiWebApiAssemblyPath { get; set; } = string.Empty; /// The base directory path where the OpenAPI tools will be downloaded. - [Required] + [Microsoft.Build.Framework.Required] public string OpenApiToolsDirectoryPath { get; set; } = string.Empty; /// The URL of the OpenAPI Spectral ruleset to validate against. - [Required] + [Microsoft.Build.Framework.Required] public string OpenApiSpectralRulesetUrl { get; set; } = string.Empty; /// The names of the Swagger documents to generate OpenAPI specifications for. - [Required] + [Microsoft.Build.Framework.Required] public string[] OpenApiSwaggerDocumentNames { get; set; } = Array.Empty(); /// The paths of the OpenAPI specification files to validate against. - [Required] + [Microsoft.Build.Framework.Required] public string[] OpenApiSpecificationFiles { get; set; } = Array.Empty(); protected override async Task ExecuteAsync(CancellationToken cancellationToken) @@ -40,6 +55,9 @@ protected override async Task ExecuteAsync(CancellationToken cancellationT var spectralManager = new SpectralManager(loggerWrapper, processWrapper, this.OpenApiToolsDirectoryPath, reportsPath, httpClientWrapper); var oasdiffManager = new OasdiffManager(loggerWrapper, processWrapper, this.OpenApiToolsDirectoryPath, httpClientWrapper); + var codeFirstProcess = new CodeFirstProcess(spectralManager, swaggerManager); + var contractFirstProcess = new ContractFirstProcess(loggerWrapper, spectralManager, swaggerManager, oasdiffManager); + this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiWebApiAssemblyPath), this.OpenApiWebApiAssemblyPath); this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiToolsDirectoryPath), this.OpenApiToolsDirectoryPath); this.Log.LogMessage(MessageImportance.Low, "{0} = '{1}'", nameof(this.OpenApiSpectralRulesetUrl), this.OpenApiSpectralRulesetUrl); @@ -58,23 +76,36 @@ protected override async Task ExecuteAsync(CancellationToken cancellationT await this.GeneratePublicNugetSource(); Directory.CreateDirectory(reportsPath); - var installSwaggerCliTask = swaggerManager.InstallSwaggerCliAsync(cancellationToken); - var installSpectralTask = spectralManager.InstallSpectralAsync(cancellationToken); - var installOasdiffTask = oasdiffManager.InstallOasdiffAsync(cancellationToken); - - await installSwaggerCliTask; - await installSpectralTask; - await installOasdiffTask; - - var generateOpenApiDocsPath = (await swaggerManager.RunSwaggerAsync(this.OpenApiSwaggerDocumentNames, cancellationToken)).ToList(); - - if (!this.CheckIfBaseSpecExists()) + switch (this.OpenApiDevelopmentMode) { - return false; + case CodeFirst: + await codeFirstProcess.Execute( + this.OpenApiSpecificationFiles, + this.OpenApiSwaggerDocumentNames, + this.OpenApiSpectralRulesetUrl, + cancellationToken); + break; + + case ContractFirst: + var isSuccess = await contractFirstProcess.Execute( + this.OpenApiSpecificationFiles, + this.OpenApiToolsDirectoryPath, + this.OpenApiSwaggerDocumentNames, + this.OpenApiSpectralRulesetUrl, + this.OpenApiCompareCodeAgainstSpecFile ? ContractFirstProcess.CompareCodeAgainstSpecFile.Enabled : ContractFirstProcess.CompareCodeAgainstSpecFile.Disabled, + cancellationToken); + + if (!isSuccess) + { + return false; + } + + break; + + default: + this.Log.LogError("Invalid value for {0}. Allowed values are '{1}' or '{2}'", nameof(ValidateOpenApiTask.OpenApiDevelopmentMode), ContractFirst, CodeFirst); + return false; } - - await spectralManager.RunSpectralAsync(this.OpenApiSpecificationFiles, this.OpenApiSpectralRulesetUrl, cancellationToken); - await oasdiffManager.RunOasdiffAsync(this.OpenApiSpecificationFiles, generateOpenApiDocsPath, cancellationToken); } catch (OpenApiTaskFailedException e) { @@ -84,27 +115,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationT return true; } - private bool CheckIfBaseSpecExists() - { - foreach (var file in this.OpenApiSpecificationFiles) - { - if (File.Exists(file)) - { - continue; - } - - this.Log.LogWarning( - "The file '{0}' does not exist. If you are running this for the first time, we have generated specification here '{1}' which can be used as base specification. " + - "Please copy specification file(s) to your project directory and rebuild.", - file, - this.OpenApiToolsDirectoryPath); - - return false; - } - - return true; - } - + // Why do we need to generate a public nuget source? private async Task GeneratePublicNugetSource() { Directory.CreateDirectory(this.OpenApiToolsDirectoryPath); diff --git a/src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets b/src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets index 60181e5..2622c19 100644 --- a/src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets +++ b/src/Workleap.OpenApi.MSBuild/msbuild/tools/Workleap.OpenApi.MSBuild.targets @@ -15,6 +15,12 @@ + + ContractFirst + + + false + $(MSBuildProjectDirectory)\$(OutputPath) @@ -48,6 +54,8 @@ enable false false - v1;v1-management + WebApi.MsBuild.SystemTest + + + + CodeFirst + v1;v1-management - + diff --git a/src/tests/WebApi.MsBuild.SystemTest/appsettings.Development.json b/src/tests/WebApi.MsBuild.SystemTest.CodeFirst/appsettings.Development.json similarity index 100% rename from src/tests/WebApi.MsBuild.SystemTest/appsettings.Development.json rename to src/tests/WebApi.MsBuild.SystemTest.CodeFirst/appsettings.Development.json diff --git a/src/tests/WebApi.MsBuild.SystemTest/appsettings.json b/src/tests/WebApi.MsBuild.SystemTest.CodeFirst/appsettings.json similarity index 100% rename from src/tests/WebApi.MsBuild.SystemTest/appsettings.json rename to src/tests/WebApi.MsBuild.SystemTest.CodeFirst/appsettings.json diff --git a/src/tests/WebApi.MsBuild.SystemTest/openapi-v1-management.yaml b/src/tests/WebApi.MsBuild.SystemTest.CodeFirst/openapi-v1-management.yaml similarity index 100% rename from src/tests/WebApi.MsBuild.SystemTest/openapi-v1-management.yaml rename to src/tests/WebApi.MsBuild.SystemTest.CodeFirst/openapi-v1-management.yaml diff --git a/src/tests/WebApi.MsBuild.SystemTest/openapi-v1.yaml b/src/tests/WebApi.MsBuild.SystemTest.CodeFirst/openapi-v1.yaml similarity index 100% rename from src/tests/WebApi.MsBuild.SystemTest/openapi-v1.yaml rename to src/tests/WebApi.MsBuild.SystemTest.CodeFirst/openapi-v1.yaml diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherForecastController.cs b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..3971198 --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherForecastController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WebApi.MsBuild.SystemTest.Controllers; + +[ApiController] +[Route("[controller]")] +[Produces("application/json")] +[ApiExplorerSettings(GroupName = "v1")] +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) + { + this._logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)], + }) + .ToArray(); + } +} \ No newline at end of file diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherManagementController.cs b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherManagementController.cs new file mode 100644 index 0000000..8b4d9c0 --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Controllers/WeatherManagementController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WebApi.MsBuild.SystemTest.Controllers; + +[ApiController] +[Route("[controller]")] +[Produces("application/json")] +[ApiExplorerSettings(GroupName = "v1-management")] +public class WeatherManagementController : ControllerBase +{ + private static readonly string[] Sources = + { + "Accuweather", "AerisWeather", "Foreca", "Open Weathermap", "National Oceanic and Atmospheric Administration", + }; + + private readonly ILogger _logger; + + public WeatherManagementController(ILogger logger) + { + this._logger = logger; + } + + [HttpGet(Name = "GetWeatherSources")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public IEnumerable Get() + { + return Sources; + } +} \ No newline at end of file diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Program.cs b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Program.cs new file mode 100644 index 0000000..0dd2a6c --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Program.cs @@ -0,0 +1,28 @@ +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "V1 API General", Version = "v1" }); + c.SwaggerDoc("v1-management", new OpenApiInfo { Title = "V1 API management", Version = "v1-management" }); +}); + +var app = builder.Build(); + +app.UseSwagger(); +app.UseSwaggerUI(options => +{ + options.SwaggerEndpoint("v1/swagger.json", "v1"); + options.SwaggerEndpoint("v1-management/swagger.json", "v1-management"); +}); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/src/tests/WebApi.MsBuild.SystemTest/Properties/launchSettings.json b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Properties/launchSettings.json similarity index 100% rename from src/tests/WebApi.MsBuild.SystemTest/Properties/launchSettings.json rename to src/tests/WebApi.MsBuild.SystemTest.ContractFirst/Properties/launchSettings.json diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WeatherForecast.cs b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WeatherForecast.cs new file mode 100644 index 0000000..ae21257 --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace WebApi.MsBuild.SystemTest; + +public class WeatherForecast +{ + public DateTime Date { get; set; } + + public int TemperatureC { get; init; } + + public int TemperatureF => 32 + (int)(this.TemperatureC / 0.5556); + + public string? Summary { get; set; } +} \ No newline at end of file diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WebApi.MsBuild.SystemTest.ContractFirst.csproj b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WebApi.MsBuild.SystemTest.ContractFirst.csproj new file mode 100644 index 0000000..09afe24 --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/WebApi.MsBuild.SystemTest.ContractFirst.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + false + false + WebApi.MsBuild.SystemTest + + + + ContractFirst + true + v1;v1-management + + + + + + + diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.Development.json b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.json b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1-management.yaml b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1-management.yaml new file mode 100644 index 0000000..eab7525 --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1-management.yaml @@ -0,0 +1,47 @@ +openapi: 3.0.1 +info: + title: V1 API management + version: v1-management +paths: + /WeatherManagement: + get: + tags: + - WeatherManagement + operationId: GetWeatherSources + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + type: string + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' +components: + schemas: + ProblemDetails: + type: object + properties: + type: + type: string + nullable: true + title: + type: string + nullable: true + status: + type: integer + format: int32 + nullable: true + detail: + type: string + nullable: true + instance: + type: string + nullable: true + additionalProperties: {} \ No newline at end of file diff --git a/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1.yaml b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1.yaml new file mode 100644 index 0000000..a8fec9c --- /dev/null +++ b/src/tests/WebApi.MsBuild.SystemTest.ContractFirst/openapi-v1.yaml @@ -0,0 +1,64 @@ +openapi: 3.0.1 +info: + title: V1 API General + version: v1 +paths: + /WeatherForecast: + get: + tags: + - WeatherForecast + operationId: GetWeatherForecast + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WeatherForecast' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' +components: + schemas: + ProblemDetails: + type: object + properties: + type: + type: string + nullable: true + title: + type: string + nullable: true + status: + type: integer + format: int32 + nullable: true + detail: + type: string + nullable: true + instance: + type: string + nullable: true + additionalProperties: {} + WeatherForecast: + type: object + properties: + date: + type: string + format: date-time + temperatureC: + type: integer + format: int32 + temperatureF: + type: integer + format: int32 + readOnly: true + summary: + type: string + nullable: true + additionalProperties: false \ No newline at end of file