From 41cc79600a44e24277b9f5a9effdfd8c7019ff54 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 1 Apr 2020 15:48:39 +0200 Subject: [PATCH 1/2] Swagger Generator: Merge multiple documents It's becoming very hard to edit the swagger file as it grows (especially with multiple PRs altering it). This PR allows the swagger file to be generated from multiple jsons instead which are merged in the controller. --- BTCPayServer/Controllers/HomeController.cs | 12 +- .../swagger/v1/swagger.template.api-keys.json | 165 ++++++++ .../v1/swagger.template.authorization.json | 72 ++++ .../wwwroot/swagger/v1/swagger.template.json | 363 +----------------- .../swagger/v1/swagger.template.users.json | 145 +++++++ 5 files changed, 391 insertions(+), 366 deletions(-) create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.authorization.json create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json diff --git a/BTCPayServer/Controllers/HomeController.cs b/BTCPayServer/Controllers/HomeController.cs index 1b48cd2b8..34a839168 100644 --- a/BTCPayServer/Controllers/HomeController.cs +++ b/BTCPayServer/Controllers/HomeController.cs @@ -116,10 +116,14 @@ namespace BTCPayServer.Controllers [Route("swagger/v1/swagger.json")] public async Task Swagger() { - var fi = _fileProvider.GetFileInfo("swagger/v1/swagger.template.json"); - using var stream = fi.CreateReadStream(); - using var reader = new StreamReader(fi.CreateReadStream()); - var json = JObject.Parse(await reader.ReadToEndAsync()); + JObject json = new JObject(); + var directoryContents = _fileProvider.GetDirectoryContents("swagger/v1"); + foreach (IFileInfo fi in directoryContents) + { + await using var stream = fi.CreateReadStream(); + using var reader = new StreamReader(fi.CreateReadStream()); + json.Merge(JObject.Parse(await reader.ReadToEndAsync())); + } var servers = new JArray(); servers.Add(new JObject(new JProperty("url", HttpContext.Request.GetAbsoluteRoot()))); json["servers"] = servers; diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json new file mode 100644 index 000000000..38981d5f9 --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.api-keys.json @@ -0,0 +1,165 @@ +{ + "paths": { + "/api/v1/api-keys/{apikey}": { + "delete": { + "tags": [ + "API Keys" + ], + "summary": "Revoke an API Key", + "description": "Revoke the current API key so that it cannot be used anymore", + "parameters": [ + { + "name": "apikey", + "in": "path", + "required": true, + "description": "The API Key to revoke", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "The key has been deleted" + }, + "404": { + "description": "The key is not found for this user" + } + }, + "security": [ + { + "API Key": [ "unrestricted" ], + "Basic": [] + } + ] + } + }, + "/api/v1/api-keys/current": { + "get": { + "tags": [ + "API Keys" + ], + "summary": "Get the current API Key information", + "description": "View information about the current API key", + "responses": { + "200": { + "description": "The key has been deleted" + } + }, + "security": [ + { + "API Key": [] + } + ] + }, + "delete": { + "tags": [ + "API Keys" + ], + "summary": "Revoke the current API Key", + "description": "Revoke the current API key so that it cannot be used anymore", + "responses": { + "200": { + "description": "The key was revoked and is no longer usable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "security": [ + { + "API Key": [] + } + ] + } + }, + "/api/v1/api-keys": { + "post": { + "tags": [ + "API Keys" + ], + "summary": "Create a new API Key", + "description": "Create a new API Key", + "responses": { + "200": { + "description": "Information about the new api key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyData" + } + } + } + } + }, + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "description": "The label of the new API Key", + "nullable": true + }, + "permissions": { + "type": "array", + "description": "The permissions granted to this API Key (See API Key Authentication)", + "nullable": true, + "items": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "API Key": [ "unrestricted" ], + "Basic": [] + } + ] + } + } + }, + "components": { + "schemas": { + "ApiKeyData": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiKey": { + "type": "string", + "description": "The API Key to use for API Key Authentication", + "nullable": false + }, + "label": { + "type": "string", + "description": "The label given by the user to this API Key", + "nullable": false + }, + "permissions": { + "type": "array", + "description": "The permissions associated to this API Key", + "nullable": false, + "items": { + "type": "string" + } + } + } + } + } + }, + "tags": [ + { + "name": "API Keys" + } + ] +} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.authorization.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.authorization.json new file mode 100644 index 000000000..02a24b192 --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.authorization.json @@ -0,0 +1,72 @@ +{ + "paths": { + "/api-keys/authorize": { + "get": { + "tags": [ + "Authorization" + ], + "summary": "Authorize User", + "description": "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions", + "parameters": [ + { + "name": "permissions", + "description": "The permissions to request. (See API Key authentication)", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + }, + { + "name": "applicationName", + "description": "The name of your application", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 2 + }, + { + "name": "strict", + "description": "If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting.", + "in": "query", + "schema": { + "type": "boolean", + "default": true, + "nullable": true + }, + "x-position": 3 + }, + { + "name": "selectiveStores", + "description": "If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control.", + "in": "query", + "schema": { + "type": "boolean", + "default": false, + "nullable": true + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "A HTML form that a user can use to confirm permissions to grant", + "content": { + "text/html": { + } + } + } + }, + "security": [] + } + } + } +} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 95a9f4496..1f8e5d3af 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -11,338 +11,8 @@ }, "servers": [ ], - "paths": { - "/api-keys/authorize": { - "get": { - "tags": [ - "Authorization" - ], - "summary": "Authorize User", - "description": "Redirect the browser to this endpoint to request the user to generate an api-key with specific permissions", - "parameters": [ - { - "name": "permissions", - "description": "The permissions to request. (See API Key authentication)", - "in": "query", - "style": "form", - "explode": true, - "schema": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - } - }, - "x-position": 1 - }, - { - "name": "applicationName", - "description": "The name of your application", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - }, - { - "name": "strict", - "description": "If permissions are specified, and strict is set to false, it will allow the user to reject some of permissions the application is requesting.", - "in": "query", - "schema": { - "type": "boolean", - "default": true, - "nullable": true - }, - "x-position": 3 - }, - { - "name": "selectiveStores", - "description": "If the application is requesting the CanModifyStoreSettings permission and selectiveStores is set to true, this allows the user to only grant permissions to selected stores under the user's control.", - "in": "query", - "schema": { - "type": "boolean", - "default": false, - "nullable": true - }, - "x-position": 4 - } - ], - "responses": { - "200": { - "description": "A HTML form that a user can use to confirm permissions to grant", - "content": { - "text/html": { - } - } - } - }, - "security": [] - } - }, - "/api/v1/users/me": { - "get": { - "tags": [ - "Users" - ], - "summary": "Get current user information", - "description": "View information about the current user", - "operationId": "Users_GetCurrentUser", - "responses": { - "200": { - "description": "Information about the current user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApplicationUserData" - } - } - } - } - }, - "security": [ - { - "API Key": [ - "btcpay.user.canviewprofile" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/users": { - "post": { - "tags": [ - "Users" - ], - "summary": "Create user", - "description": "Create a new user.\n\nThis operation can be called without authentication in any of this cases:\n* There is not any administrator yet on the server,\n* The subscriptions are not disabled in the server's policies.\n\nIf the first administrator is created by this call, subscriptions are automatically disabled.", - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string", - "description": "The email of the new user", - "nullable": false - }, - "password": { - "type": "string", - "description": "The password of the new user" - }, - "isAdministrator": { - "type": "boolean", - "description": "Make this user administrator (only if you have the `unrestricted` permission of a server administrator)", - "nullable": true, - "default": false - } - } - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "201": { - "description": "Information about the new user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApplicationUserData" - } - } - } - }, - "400": { - "description": "A list of errors that occurred when creating the user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } - }, - "401": { - "description": "If you need to authenticate for this endpoint (ie. the server settings policies lock subscriptions and that an admin already exists)" - }, - "403": { - "description": "If you are authenticated but forbidden to create a new user (ie. you don't have the `unrestricted` permission on a server administrator or if you are not administrator and registrations are disabled in the server's policies)" - }, - "429": { - "description": "DDoS protection if you are creating more than 2 accounts every minutes (non-admin only)" - } - }, - "security": [ - { - "API Key": [ - "btcpay.server.cancreateuser" - ], - "Basic": [] - } - ] - } - }, - "/api/v1/api-keys/{apikey}": { - "delete": { - "tags": [ - "API Keys" - ], - "summary": "Revoke an API Key", - "description": "Revoke the current API key so that it cannot be used anymore", - "parameters": [ - { - "name": "apikey", - "in": "path", - "required": true, - "description": "The API Key to revoke", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "The key has been deleted" - }, - "404": { - "description": "The key is not found for this user" - } - }, - "security": [ - { - "API Key": [ "unrestricted" ], - "Basic": [] - } - ] - } - }, - "/api/v1/api-keys/current": { - "get": { - "tags": [ - "API Keys" - ], - "summary": "Get the current API Key information", - "description": "View information about the current API key", - "responses": { - "200": { - "description": "The key has been deleted" - } - }, - "security": [ - { - "API Key": [] - } - ] - }, - "delete": { - "tags": [ - "API Keys" - ], - "summary": "Revoke the current API Key", - "description": "Revoke the current API key so that it cannot be used anymore", - "responses": { - "200": { - "description": "The key was revoked and is no longer usable", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyData" - } - } - } - } - }, - "security": [ - { - "API Key": [] - } - ] - } - }, - "/api/v1/api-keys": { - "post": { - "tags": [ - "API Keys" - ], - "summary": "Create a new API Key", - "description": "Create a new API Key", - "responses": { - "200": { - "description": "Information about the new api key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiKeyData" - } - } - } - } - }, - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "string", - "description": "The label of the new API Key", - "nullable": true - }, - "permissions": { - "type": "array", - "description": "The permissions granted to this API Key (See API Key Authentication)", - "nullable": true, - "items": { - "type": "string" - } - } - } - } - } - } - }, - "security": [ - { - "API Key": [ "unrestricted" ], - "Basic": [] - } - ] - } - } - }, "components": { "schemas": { - "ApplicationUserData": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "The id of the new user", - "nullable": false - }, - "email": { - "type": "string", - "description": "The email of the new user", - "nullable": false - }, - "emailConfirmed": { - "type": "boolean", - "description": "True if the email has been confirmed by the user" - }, - "requiresEmailConfirmation": { - "type": "boolean", - "description": "True if the email requires email confirmation to log in" - } - } - }, "ValidationProblemDetails": { "allOf": [ { @@ -398,30 +68,6 @@ } } }, - "ApiKeyData": { - "type": "object", - "additionalProperties": false, - "properties": { - "apiKey": { - "type": "string", - "description": "The API Key to use for API Key Authentication", - "nullable": false - }, - "label": { - "type": "string", - "description": "The label given by the user to this API Key", - "nullable": false - }, - "permissions": { - "type": "array", - "description": "The permissions associated to this API Key", - "nullable": false, - "items": { - "type": "string" - } - } - } - } }, "securitySchemes": { "API Key": { @@ -446,12 +92,5 @@ "Basic": [] } ], - "tags": [ - { - "name": "Users" - }, - { - "name": "API Keys" - } - ] + "tags": [] } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json new file mode 100644 index 000000000..6fbe758f6 --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.users.json @@ -0,0 +1,145 @@ +{ + "paths": { + "/api/v1/users/me": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get current user information", + "description": "View information about the current user", + "operationId": "Users_GetCurrentUser", + "responses": { + "200": { + "description": "Information about the current user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationUserData" + } + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.user.canviewprofile" + ], + "Basic": [] + } + ] + } + }, + "/api/v1/users": { + "post": { + "tags": [ + "Users" + ], + "summary": "Create user", + "description": "Create a new user.\n\nThis operation can be called without authentication in any of this cases:\n* There is not any administrator yet on the server,\n* The subscriptions are not disabled in the server's policies.\n\nIf the first administrator is created by this call, subscriptions are automatically disabled.", + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "string", + "description": "The email of the new user", + "nullable": false + }, + "password": { + "type": "string", + "description": "The password of the new user" + }, + "isAdministrator": { + "type": "boolean", + "description": "Make this user administrator (only if you have the `unrestricted` permission of a server administrator)", + "nullable": true, + "default": false + } + } + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "201": { + "description": "Information about the new user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationUserData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when creating the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "401": { + "description": "If you need to authenticate for this endpoint (ie. the server settings policies lock subscriptions and that an admin already exists)" + }, + "403": { + "description": "If you are authenticated but forbidden to create a new user (ie. you don't have the `unrestricted` permission on a server administrator or if you are not administrator and registrations are disabled in the server's policies)" + }, + "429": { + "description": "DDoS protection if you are creating more than 2 accounts every minutes (non-admin only)" + } + }, + "security": [ + { + "API Key": [ + "btcpay.server.cancreateuser" + ], + "Basic": [] + } + ] + } + } + }, + "components": { + "schemas": { + "ApplicationUserData": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "The id of the new user", + "nullable": false + }, + "email": { + "type": "string", + "description": "The email of the new user", + "nullable": false + }, + "emailConfirmed": { + "type": "boolean", + "description": "True if the email has been confirmed by the user" + }, + "requiresEmailConfirmation": { + "type": "boolean", + "description": "True if the email requires email confirmation to log in" + } + } + } + } + }, + "tags": [ + { + "name": "Users" + } + ] +} From 76b919d8874167eb69558f4a08475ae2a2f2d687 Mon Sep 17 00:00:00 2001 From: Kukks Date: Sun, 5 Apr 2020 09:43:49 +0200 Subject: [PATCH 2/2] make swagger test use live endpoint --- BTCPayServer.Tests/UnitTest1.cs | 36 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 0b8031fd2..5a6d8f28d 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -93,24 +93,32 @@ namespace BTCPayServer.Tests } [Fact] - [Trait("Fast", "Fast")] + [Trait("Integration", "Integration")] public async Task CheckSwaggerIsConformToSchema() { - JObject swagger = JObject.Parse(File.ReadAllText(Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "swagger", "v1", "swagger.template.json"))); - using HttpClient client = new HttpClient(); - var resp = await client.GetAsync("https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json"); - var schema = JSchema.Parse(await resp.Content.ReadAsStringAsync()); - IList errors; - bool valid = swagger.IsValid(schema, out errors); - //the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas. - if (!valid && errors.Count == 1 && errors.Any(error => - error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf)) + using (var tester = ServerTester.Create()) { - errors = new List(); - valid = true; + await tester.StartAsync(); + var sresp = await tester.PayTester.HttpClient.GetAsync("swagger/v1/swagger.json"); + + JObject swagger = JObject.Parse(await sresp.Content.ReadAsStringAsync()); + using HttpClient client = new HttpClient(); + var resp = await client.GetAsync( + "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json"); + var schema = JSchema.Parse(await resp.Content.ReadAsStringAsync()); + IList errors; + bool valid = swagger.IsValid(schema, out errors); + //the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas. + if (!valid && errors.Count == 1 && errors.Any(error => + error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf)) + { + errors = new List(); + valid = true; + } + + Assert.Empty(errors); + Assert.True(valid); } - Assert.Empty(errors); - Assert.True(valid); } private static async Task CheckLinks(Regex regex, HttpClient httpClient, string file)