{"id":21565809,"url":"https://github.com/hylandsoftware/hot-potato","last_synced_at":"2025-04-10T13:13:58.467Z","repository":{"id":114298101,"uuid":"381786093","full_name":"HylandSoftware/Hot-Potato","owner":"HylandSoftware","description":"ASP.NET Core proxy that will validate an API's conformance to an OpenAPI spec.","archived":false,"fork":false,"pushed_at":"2023-05-03T20:19:45.000Z","size":15988,"stargazers_count":3,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-24T11:56:55.263Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit-0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/HylandSoftware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-30T17:41:20.000Z","updated_at":"2024-08-16T17:20:02.000Z","dependencies_parsed_at":null,"dependency_job_id":"c2ba0016-ddef-4212-95fc-7ccf9ffb1a1a","html_url":"https://github.com/HylandSoftware/Hot-Potato","commit_stats":null,"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HylandSoftware%2FHot-Potato","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HylandSoftware%2FHot-Potato/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HylandSoftware%2FHot-Potato/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HylandSoftware%2FHot-Potato/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/HylandSoftware","download_url":"https://codeload.github.com/HylandSoftware/Hot-Potato/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248225653,"owners_count":21068078,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-24T10:21:48.583Z","updated_at":"2025-04-10T13:13:57.966Z","avatar_url":"https://github.com/HylandSoftware.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hot Potato Proxy\n\n![Build Status](https://github.com/HylandSoftware/Hot-Potato/workflows/hot-potato-ci/badge.svg)\n\nThe Hot Potato is an ASP.NET Core reverse proxy that will validate an API's conformance to an OpenAPI spec.\n\n## Setup\n\nTo use the complete tool you will need to download the `HotPotato.AspNetCore.Host` NuGet package from https://www.nuget.org/packages/HotPotato.AspNetCore.Host/. Since Hot Potato is a dotnet global tool you can easily download it from Powershell or Command Prompt.\n\n### Install\nTo install Hot Potato use the following command:\n```sh\ndotnet tool install -g hotpotato.aspnetcore.host\n```\nThere are other options that can be utilized when downloading a dotnet tool. A complete list of options can be found here: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install\n\nIf the install is successful you will see a message like this:  \n```sh\nYou can invoke the tool using the following command: HotPotato\nTool 'hotpotato.aspnetcore.host' (version '2.0.0') was successfully installed.\n```\n### Start Hot Potato\n\nYou can now start the tool by using the command `HotPotato`. Add the arguments for your testing situation and you can utilize `HotPotato` from the command line.\n```sh\nHotPotato --RemoteEndpoint http://example.com/my/endpoint --SpecLocation http://example.com/my/specification.yaml\n```\n\n### SSL Validation Issues\n\nWe have also also provided an environment variable named `HttpClientSettings__IgnoreClientHttpsCertificateValidationErrors` that can be set to `true` in the case of of persistent SSL certificate validation issues. \n\n### Specs with Token Validation\n\nIn the case of something like accessing a raw file in a private repo on Github, a token is needed to access a specification. For cases like this, we have included a `SpecToken` environment variable that can be used as a secret. If you're running Hot Potato locally via Visual Studio, an easy way of setting the secret can be found here: [Manage User Secrets](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0\u0026tabs=windows#json-structure-flattening-in-visual-studio).\n\n\u003ca name=\"results\"\u003e\u003c/a\u003e\n## Results\n\nIn order to retrieve results from the proxy, we have exposed a `/results` endpoint. This endpoint will return a JSON-formatted object that shows all of the requests that have come through and whether or not they're conformant. The response will also include a `X-Status` header which will be either `Pass`, `Fail`, or `Inconclusive`.\n\n__Pass Result__\n```json\n[\n    {\n        \"Path\":\"/endpoint\",\n        \"Method\":\"GET\",\n        \"StatusCode\":200,\n        \"State\":\"Pass\"\n    }\n]\n```\n\n__Fail Result__\n```json\n[\n    {        \n        \"Path\":\"/endpoint\",\n        \"Method\":\"GET\",\n        \"StatusCode\":404,\n        \"State\":\"Fail\",\n        \"Reasons\":[\"InvalidBody\"],\n        \"ValidationErrors\":\n        [\n            {\n                \"Message\":\"Error\",\n                \"Kind\":\"Unknown\",\n                \"Property\":\"Property\",\n                \"LineNumber\":5,\n                \"LinePosition\":10\n            }\n        ]\n    }\n]\n```\n\n### Accessing results via ResultCollector\n\nWhen writing tests using a fixture to set up a mock server (such as TestServer), you can expose a public List\u003cResult\u003e member obtained from the IResultCollector service in the fixture to validate each Result instead of having to pull from the /results endpoint.\n\nIn the test fixture constructor:\n```csharp\nResults = hotPotatoServer.Host.Services.GetService\u003cIResultCollector\u003e().Results;\n```\n\nIn test:\n```csharp\nResult result = results.ElementAt(0);\n\n//We overrode the ToString() on the Result objects to output the json string of a result in a failed assert\nAssert.True(result.State == State.Pass, result.ToString());\nAssert.Equal(methodString, result.Method, ignoreCase: true);\nAssert.Equal(pathUri.AbsolutePath, result.Path);\nAssert.Equal(expectedStatusCode, result.StatusCode);\n\nresults.Clear();\n```\n\nMore information about writing tests using TestServer can be found below in the [Middleware](#middleware) section.\n\n### Custom Result Headers\n\nHot Potato also allows users to add custom headers to results objects. To do so, users can can add the prefix \"X-HP-\" to a header key in a request, and it will appear at the top of the result in a \"custom\" array. The custom array only appears if custom headers are provided.\n\n```json\n[\n\t{\n        \"custom\": {\n            \"X-HP-Name\": [\n                \"LandingPage\"\n            ]\n        },\n        \"state\": \"Pass\",\n        \"path\": \"/\",\n        \"method\": \"get\",\n        \"statusCode\": 200\n    }\n]\n```\n\n## Structure\n\nThe proxy is broken down into a number of components to allow flexibility for developers.\n\n### HotPotato.AspNetCore.Host\n\nThis is an ASP.NET Core host configured to use the Hot Potato Middleware. It is stood up as a separate server that listens by default on port `3232`. There is an `appsettings.json` to allow the developer to set the remote endpoint to forward requests to and the location of the OpenAPI specification to validate conformance. These values can also be passed into the command line via the following command:\n\n`hotpotato --RemoteEndpoint http://example.com/my/endpoint --SpecLocation http://example.com/my/specification.yaml`\n\n### HotPotato.Core\n\n`HotPotato.Core` contains all of the models, HTTP logic, the proxy forwarder, and the interface for the proxy processor. This library is a dependency of `HotPotato.AspNetCore.Middleware` but will likely never need to be directly consumed.\n\n### HotPotato.OpenApi\n\n`HotPotato.OpenApi` contains all of the OpenAPI functionality including the processor implementation that validates an HTTP request/response pair against a spec, a result collection, logic to find paths, and so on. Currently, we are using the packages `NSwag`, `NJsonSchema`, and `NJsonSchema.Yaml` to consume a spec and to validate the contained schemas.\n\n\u003ca name=\"middleware\"\u003e\u003c/a\u003e\n### HotPotato.AspNetCore.Middleware\n\nThis is an ASP.NET Core middleware that can be used in situations where test suites are directly starting up the server startup or using [`TestServer`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver).\n\nThe main factor in setting up a test suite to use the isolated middleware is that because Hot Potato's default behavior is to create its own client through DI setup, an instance of the API Under Test's client must be created first to be able to be injected into DI.\n\n#### Using the Middleware with TestServer\n\nIn our example [test fixture](https://github.com/HylandSoftware/Hot-Potato/blob/master/test/HotPotato.TestServer.Test/TestFixture.cs), we use two instances of TestServer: one to create the client representing the API Under Test, and one that consumes this client to create a new client housing the Hot Potato proxy.\n\nFirst, we define a TestFixture class to use a generic Startup reference type:\n ```csharp\npublic class TestFixture\u003cTStartup\u003e : IDisposable where TStartup : class\n ```\n\nNext, TestServer doesn't actually listen on an address, but it needs a placeholder BaseAddress to be used by the HttpRequest constructors. In our example, we use localhost:5000 for the TestServer housing the API and localhost:3232 for the TestServer housing the Hot Potato proxy to be consistent with the rest of our testing. These can be set to anything as long as they are valid URIs.\n\n```\nprivate const string ApiServerAddress = \"http://localhost:5000\";\nprivate const string HotPotatoAddress = \"http://localhost:3232\";\n```\n\nThen we create a TestServer instance using the Startup type to create a client:\n```csharp\nvar apiBuilder = new WebHostBuilder()\n    .UseStartup\u003cTStartup\u003e();\n\napiServer = new TestServer(apiBuilder);\napiServer.BaseAddress = new Uri(TestServerAddress);\n\nHotPotatoClient apiClient = new HotPotatoClient(apiServer.CreateClient());\n```\n\nNow that we have a client created for the API Under Test, we can build our web host and inject it into the Test Server.\nThis is done in our fixture setup, but can also be placed in a custom Startup class:\n\n```csharp\nvar hotPotatoBuilder = new WebHostBuilder()\n    //Setting this here instead of in appsettings.json so it always matches the BaseAddress on TestServer\n    .UseSetting(\"RemoteEndpoint\", TestServerAddress)\n    .ConfigureAppConfiguration((hostingContext, config) =\u003e\n    {\n        config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)\n            .AddJsonFile(\"appsettings.json\", optional: true);\n    })\n    .ConfigureServices(services =\u003e\n    {\n        services.ConfigureMiddlewareServices(apiClient);\n    })\n    .Configure(builder =\u003e\n    {\n        builder.UseMiddleware\u003cHotPotatoMiddleware\u003e();\n    });\n\n    hotPotatoServer = new TestServer(hotPotatoBuilder);\n    hotPotatoServer.BaseAddress = new Uri(HotPotatoAddress);\n```\n\nAn important part of this builder is the line:\n```csharp\nservices.ConfigureMiddlewareServices(apiClient);\n```\n`ConfigureMiddlewareServices` is an extension method found in the AspNetCore.Middleware project that adds the client to DI as well as all the services necessary to use the Middleware.\n\nThese services are `IProxy`, `ISpecificationProivder`, `IResultCollector`, and `IProcessor`.\n\nWe set the fixture's public members of Results to the List\u003cResult\u003e member of the IResultCollector and Client to the client created with the Hot Potato TestServer:\n```csharp\nResults = hotPotatoServer.Host.Services.GetService\u003cIResultCollector\u003e().Results;\nClient = new HotPotatoClient(hotPotatoServer.CreateClient());\n```\n\nThen we use them like so in a test to send requests and verify the validation results:\n```csharp\nawait client.SendAsync(req);\n\nResult result = results.ElementAt(0);\n\nAssert.Equal(State.Pass, result.State);\n```\n\nMake sure to call `results.Clear()` in a `Dispose()` method in XUnit or a `[Teardown]` method in NUnit. Another option is to call `results.Clear` in the `finally` block of a try-finally statement containing the test fixture. \n\nThe full example test can be found at [RawPotatoTest.cs](https://github.com/HylandSoftware/Hot-Potato/blob/master/test/HotPotato.TestServer.Test/RawPotatoTest.cs).\n\n#### Using Middleware/TestServer with Startups in separate test projects\n\nAs mentioned above, the fixture setup can be done in a Startup class, and to avoid re-writing the same code in multiple test projects, the Startup class from one test project can be referenced by another. When choosing this route, the test Startup class will need to be registered as a MVC Application Part with the line `services.AddMvc().AddApplicationPart(typeof(Startup).Assembly);`.\n\n```csharp\npublic override void ConfigureServices(IServiceCollection services)\n{\n    startup.ConfigureServices(services);\n\n    services.AddMvc().AddApplicationPart(typeof(Startup).Assembly);\n\n    //custom code here\n}\n```\n\n## Testing with Postman\n\nEnd-to-End tests using Hot Potato can be run with Postman both locally and through a pipeline also using Newman.\n\nTo use Postman locally, you must have instances of both the Hot Potato server and your API server running.\n\nFor our test project, we provided our own sample Hot Potato API, which can be found [here](https://github.com/HylandSoftware/Hot-Potato/tree/master/test/HotPotato.Api).\n\nOnce your System Under Test is ready, you may start writing Postman requests with the base address of localhost:3232.\nTo check the results of these requests, you can query the results endpoint as shown in the [Results](#results) section above.\n\nYou may also create these requests as part of a collection, which will allow for the creation of test sets, and the ability for them to be exported and run by a pipeline.\n\nIf you are not familiar with creating collections and writing tests in Postman, more information can be found in the links below:\n\n[Collections](https://learning.getpostman.com/docs/postman/collections/creating-collections/)\n\n[Tests](https://learning.getpostman.com/docs/postman/scripts/test_scripts/)\n\nTests will usually check for critical information such as if the correct status code and body are being returned correctly in the response.\n\nExamples can be found in our HappyPath collection [here](https://github.com/HylandSoftware/Hot-Potato/blob/master/test/HappyPathTests.postman_collection.json).\n\n**Check that the response contains the correct status code and expected body**\n```javascript\npm.test(\\\"LandingPage returns 200 OK\\\", function () {\n\tpm.response.to.have.status(200)\n})\n\npm.test(\\\"LandingPage returns expected body \\\", function () {\n\tpm.response.to.have.body(\\\"https://github.com/HylandSoftware/Hot-Potato\\\")\n})\n```\n\nAt the end of each collection, a GET request can be sent to the /results endpoint so that the list of results can be tested.\nWe split our test suite into three different collections of HappyPath, Non-Conformant, and NotInSpec tests, so that we could easily check if each collection only contained either all 'Pass' or all 'Fail' results.\n\n**Check that the HappyPath results do not contain fail results**\n```javascript\npm.test(\\\"Results should not contain Fail\\\", function () {\n\tpm.expect(pm.response.text()).to.not.include(\\\"Fail\\\")\n})\n```\n\nMake sure to send a DELETE request at the end of your collection so that the results from collection do not carry over to the next.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhylandsoftware%2Fhot-potato","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhylandsoftware%2Fhot-potato","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhylandsoftware%2Fhot-potato/lists"}