From b37c7905cd0154042aa39a8b94ba570a445ea694 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Mon, 12 Oct 2020 22:47:31 -0700 Subject: [PATCH 1/8] stub controller to help with future testing --- test-app/app/assets/javascripts/stubs.js | 2 ++ test-app/app/assets/stylesheets/stubs.css | 4 ++++ test-app/app/controllers/stubs_controller.rb | 9 +++++++++ test-app/app/helpers/stubs_helper.rb | 2 ++ test-app/config/routes.rb | 2 ++ 5 files changed, 19 insertions(+) create mode 100644 test-app/app/assets/javascripts/stubs.js create mode 100644 test-app/app/assets/stylesheets/stubs.css create mode 100644 test-app/app/controllers/stubs_controller.rb create mode 100644 test-app/app/helpers/stubs_helper.rb diff --git a/test-app/app/assets/javascripts/stubs.js b/test-app/app/assets/javascripts/stubs.js new file mode 100644 index 0000000..dee720f --- /dev/null +++ b/test-app/app/assets/javascripts/stubs.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/test-app/app/assets/stylesheets/stubs.css b/test-app/app/assets/stylesheets/stubs.css new file mode 100644 index 0000000..afad32d --- /dev/null +++ b/test-app/app/assets/stylesheets/stubs.css @@ -0,0 +1,4 @@ +/* + Place all the styles related to the matching controller here. + They will automatically be included in application.css. +*/ diff --git a/test-app/app/controllers/stubs_controller.rb b/test-app/app/controllers/stubs_controller.rb new file mode 100644 index 0000000..7a4acd9 --- /dev/null +++ b/test-app/app/controllers/stubs_controller.rb @@ -0,0 +1,9 @@ +class StubsController < ApplicationController + def index + render plain: 'OK' + end + + def show + render plain: 'OK' + end +end diff --git a/test-app/app/helpers/stubs_helper.rb b/test-app/app/helpers/stubs_helper.rb new file mode 100644 index 0000000..7ae9034 --- /dev/null +++ b/test-app/app/helpers/stubs_helper.rb @@ -0,0 +1,2 @@ +module StubsHelper +end diff --git a/test-app/config/routes.rb b/test-app/config/routes.rb index 038f728..3107e95 100644 --- a/test-app/config/routes.rb +++ b/test-app/config/routes.rb @@ -9,6 +9,8 @@ TestApp::Application.routes.draw do post 'auth-tests/api-key', to: 'auth_tests#api_key' post 'auth-tests/basic-and-api-key', to: 'auth_tests#basic_and_api_key' + resources :stubs + mount Rswag::Api::Engine => 'api-docs' mount Rswag::Ui::Engine => 'api-docs' end From 7e1a79220cf6c33adc6e295e0e0d8005c9aa7996 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Mon, 12 Oct 2020 22:49:47 -0700 Subject: [PATCH 2/8] start standing up exhaustive output unit tests for OpenAPI v3 --- test-app/spec/integration/openapi3_spec.rb | 273 +++++++++++++++++++++ test-app/spec/swagger_helper.rb | 63 +++++ test-app/swagger/v3/openapi.json | 139 +++++++++++ 3 files changed, 475 insertions(+) create mode 100644 test-app/spec/integration/openapi3_spec.rb create mode 100644 test-app/swagger/v3/openapi.json diff --git a/test-app/spec/integration/openapi3_spec.rb b/test-app/spec/integration/openapi3_spec.rb new file mode 100644 index 0000000..8d0d082 --- /dev/null +++ b/test-app/spec/integration/openapi3_spec.rb @@ -0,0 +1,273 @@ +require 'swagger_helper' +require 'rswag/specs/swagger_formatter' + +# This spec file validates OpenAPI output generated by spec metadata. +# Specifically here, we look at OpenApi 3 as documented at +# https://swagger.io/docs/specification/about/ + +RSpec.describe 'Generated OpenApi', type: :request, swagger_doc: 'v3/openapi.json' do + before do |example| + output = double('output').as_null_object + swagger_root = File.expand_path('tmp/swagger', __dir__) + config = double('config', swagger_root: swagger_root, get_swagger_doc: swagger_doc ) + formatter = Rswag::Specs::SwaggerFormatter.new(output, config) + + example_group = OpenStruct.new(group: OpenStruct.new(metadata: example.metadata)) + formatter.example_group_finished(example_group) + end + + # Framework definition, to be overridden for contexts + let(:swagger_doc) do + { # That which would be defined in swagger_helper.rb + openapi: api_openapi, + basePath: api_base_path, + schemes: api_schemes, + host: api_host, + produces: api_produces, + components: api_components + } + end + let(:api_openapi) { '3.0.1' } + let(:api_base_path) { '/foo' } + let(:api_schemes) { ['https'] } + let(:api_host) { 'api.example.com' } + let(:api_produces) { ['application/json'] } + let(:api_components) { {} } + + describe 'Basic Structure' + describe 'API Server and Base Path' + + describe 'Media Types' do + path '/stubs' do + get 'a summary' do + tags 'Media Types' + + response '200', 'OK' do + run_test! + + it 'declares output as application/json' do + pending "Not yet implemented?" + tree = swagger_doc.dig(:paths, "/stubs", :get, :responses, '200', :content) + expect(tree).to have_key('application/json') + end + end + end + end + end + + describe 'Paths and Operations' + + describe 'Parameter Serialization' do + describe 'Path Parameters' do + path '/stubs/{a_param}' do + get 'a summary' do + tags 'Parameter Serialization: Query String' + produces 'application/json' + + parameter( + name: 'a_param', + in: :path, + ) + let(:a_param) { "42" } + + response '200', 'OK' do + run_test! + + it 'declares parameter in path' do + tree = swagger_doc.dig(:paths, "/stubs/{a_param}", :get, :parameters) + expect(tree.first[:name]).to eq('a_param') + expect(tree.first[:in]).to eq(:path) + end + + it 'declares path parameters as required' do + tree = swagger_doc.dig(:paths, "/stubs/{a_param}", :get, :parameters) + expect(tree.first[:required]).to eq(true) + end + end + end + end + end + + describe 'Query Parameters' do + path '/stubs' do + get 'a summary' do + tags 'Parameter Serialization: Query String' + produces 'application/json' + + parameter( + name: 'a_param', + in: :query, + ) + let(:a_param) { "a foo" } + + response '200', 'OK' do + run_test! + + it 'declares parameter in query string' do + tree = swagger_doc.dig(:paths, "/stubs", :get, :parameters) + expect(tree.first[:name]).to eq('a_param') + expect(tree.first[:in]).to eq(:query) + end + end + + # TODO: Serialization (form/spaceDelimited/pipeDelimited/deepObject) + end + end + end + + # TODO: Header + # TODO: Cookie + # TODO: Default values + # TODO: Enum + # TODO: Constant + # TODO: Empty/Nullable + # TODO: Examples + # TODO: Deprecated + # TODO: Common Parameters + end + + describe 'Request Body' + describe 'Responses' + describe 'Data Models (Schemas)' + describe 'Examples' + describe 'Authentication' + describe 'Links' + describe 'Callbacks' + describe 'Components Section' + describe 'Using $ref' + describe 'Grouping Operations with Tags' + + + # path '/blogs' do + # post 'Creates a blog' do + # tags 'Blogs' + # description 'Creates a new blog from provided data' + # operationId 'createBlog' + # consumes 'application/json' + # produces 'application/json' + # parameter name: :blog, in: :body, schema: { '$ref' => '#/definitions/blog' } + + # let(:blog) { { title: 'foo', content: 'bar' } } + + # response '201', 'blog created' do + # # schema '$ref' => '#/definitions/blog' + # run_test! + # end + + # response '422', 'invalid request' do + # schema '$ref' => '#/definitions/errors_object' + + # let(:blog) { { title: 'foo' } } + # run_test! do |response| + # expect(response.body).to include("can't be blank") + # end + + # it 'outputs parameters' do + # pp swagger_doc + # params = swagger_doc.dig(:paths, "/blogs", :post, :parameters) + # expect(params[0][:name]).to eq(:blog) + # end + # end + # end + + # get 'Searches blogs' do + # tags 'Blogs' + # description 'Searches blogs by keywords' + # operationId 'searchBlogs' + # produces 'application/json' + # parameter name: :keywords, in: :query, type: 'string' + + # let(:keywords) { 'foo bar' } + + # response '200', 'success' do + # schema type: 'array', items: { '$ref' => '#/definitions/blog' } + # end + + # response '406', 'unsupported accept header' do + # let(:'Accept') { 'application/foo' } + # run_test! + # end + # end + # end + + # path '/blogs/flexible' do + # post 'Creates a blog flexible body' do + # tags 'Blogs' + # description 'Creates a flexible blog from provided data' + # operationId 'createFlexibleBlog' + # consumes 'application/json' + # produces 'application/json' + + # parameter name: :flexible_blog, in: :body, schema: { + # oneOf: [ + # { '$ref' => '#/definitions/blog' }, + # { '$ref' => '#/definitions/flexible_blog' } + # ] + # } + + # let(:flexible_blog) { { blog: { headline: 'my headline', text: 'my text' } } } + + # response '201', 'flexible blog created' do + # schema oneOf: [{ '$ref' => '#/definitions/blog' }, { '$ref' => '#/definitions/flexible_blog' }] + # run_test! + # end + # end + # end + + # path '/blogs/{id}' do + # parameter name: :id, in: :path, type: :string + + # let(:id) { blog.id } + # let(:blog) { Blog.create(title: 'foo', content: 'bar', thumbnail: 'thumbnail.png') } + + # get 'Retrieves a blog' do + # tags 'Blogs' + # description 'Retrieves a specific blog by id' + # operationId 'getBlog' + # produces 'application/json' + + # response '200', 'blog found' do + # header 'ETag', type: :string + # header 'Last-Modified', type: :string + # header 'Cache-Control', type: :string + + # schema '$ref' => '#/definitions/blog' + + # examples 'application/json' => { + # id: 1, + # title: 'Hello world!', + # content: 'Hello world and hello universe. Thank you all very much!!!', + # thumbnail: 'thumbnail.png' + # } + + # let(:id) { blog.id } + # run_test! + # end + + # response '404', 'blog not found' do + # let(:id) { 'invalid' } + # run_test! + # end + # end + # end + + # path '/blogs/{id}/upload' do + # parameter name: :id, in: :path, type: :string + + # let(:id) { blog.id } + # let(:blog) { Blog.create(title: 'foo', content: 'bar') } + + # put 'Uploads a blog thumbnail' do + # tags 'Blogs' + # description 'Upload a thumbnail for specific blog by id' + # operationId 'uploadThumbnailBlog' + # consumes 'multipart/form-data' + # parameter name: :file, :in => :formData, :type => :file, required: true + + # response '200', 'blog updated' do + # let(:file) { Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures/thumbnail.png")) } + # run_test! + # end + # end + # end +end diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/swagger_helper.rb index 3f2e151..3b8dfae 100644 --- a/test-app/spec/swagger_helper.rb +++ b/test-app/spec/swagger_helper.rb @@ -77,6 +77,69 @@ RSpec.configure do |config| in: :query } } + }, + 'v3/openapi.json' => { + openapi: '3.0.0', + info: { + title: 'API V1', + version: 'v1' + }, + paths: {}, + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: 'www.example.com' + } + } + } + ], + definitions: { + errors_object: { + type: 'object', + properties: { + errors: { '$ref' => '#/definitions/errors_map' } + } + }, + errors_map: { + type: 'object', + additionalProperties: { + type: 'array', + items: { type: 'string' } + } + }, + blog: { + type: 'object', + properties: { + id: { type: 'integer' }, + title: { type: 'string' }, + content: { type: 'string', 'x-nullable': true }, + thumbnail: { type: 'string', 'x-nullable': true} + }, + required: [ 'id', 'title' ] + }, + flexible_blog: { + type: 'object', + properties: { + id: { type: 'integer' }, + headline: { type: 'string' }, + text: { type: 'string', nullable: true }, + thumbnail: { type: 'string', nullable: true } + }, + required: ['id', 'headline'] + } + }, + securityDefinitions: { + basic_auth: { + type: :basic + }, + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } + } } } end diff --git a/test-app/swagger/v3/openapi.json b/test-app/swagger/v3/openapi.json new file mode 100644 index 0000000..97506c4 --- /dev/null +++ b/test-app/swagger/v3/openapi.json @@ -0,0 +1,139 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "API V1", + "version": "v1" + }, + "paths": { + "/stubs": { + "get": { + "summary": "a summary", + "tags": [ + "Parameter Serialization: Query String" + ], + "responses": { + "200": { + "description": "OK", + "content": { + } + } + }, + "parameters": [ + { + "name": "a_param", + "in": "query" + } + ] + } + }, + "/stubs/{a_param}": { + "get": { + "summary": "a summary", + "tags": [ + "Parameter Serialization: Query String" + ], + "parameters": [ + { + "name": "a_param", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + } + } + } + } + } + }, + "servers": [ + { + "url": "https://{defaultHost}", + "variables": { + "defaultHost": { + "default": "www.example.com" + } + } + } + ], + "definitions": { + "errors_object": { + "type": "object", + "properties": { + "errors": { + "$ref": "#/definitions/errors_map" + } + } + }, + "errors_map": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "blog": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string", + "x-nullable": true + }, + "thumbnail": { + "type": "string", + "x-nullable": true + } + }, + "required": [ + "id", + "title" + ] + }, + "flexible_blog": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "headline": { + "type": "string" + }, + "text": { + "type": "string", + "nullable": true + }, + "thumbnail": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "headline" + ] + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "basic" + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "query" + } + } + } +} \ No newline at end of file From f6630cc7a612f72839aa8d0257e3263992eb657e Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Mon, 12 Oct 2020 22:50:48 -0700 Subject: [PATCH 3/8] fix a bug related to upgrade_request_type! translation --- rswag-specs/lib/rswag/specs/request_factory.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb index 2e8f3f1..30c423e 100644 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ b/rswag-specs/lib/rswag/specs/request_factory.rb @@ -118,7 +118,7 @@ module Rswag def build_query_string_part(param, value) name = param[:name] - return "#{name}=#{value}" unless param[:type].to_sym == :array + return "#{name}=#{value}" unless param.dig(:schema, :type)&.to_sym == :array case param[:collectionFormat] when :ssv From 68e64dba2c563f5c7a1357c0cf7a0aa87860dfd1 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Mon, 12 Oct 2020 22:53:18 -0700 Subject: [PATCH 4/8] OpenAPI currently at v 3.0.3 --- test-app/spec/integration/openapi3_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-app/spec/integration/openapi3_spec.rb b/test-app/spec/integration/openapi3_spec.rb index 8d0d082..7515ef1 100644 --- a/test-app/spec/integration/openapi3_spec.rb +++ b/test-app/spec/integration/openapi3_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'Generated OpenApi', type: :request, swagger_doc: 'v3/openapi.jso components: api_components } end - let(:api_openapi) { '3.0.1' } + let(:api_openapi) { '3.0.3' } let(:api_base_path) { '/foo' } let(:api_schemes) { ['https'] } let(:api_host) { 'api.example.com' } From 91a27852f2f4786b4db5bbcf5d89f67158439be5 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Tue, 13 Oct 2020 09:17:15 -0700 Subject: [PATCH 5/8] more flex in this check --- rswag-specs/lib/rswag/specs/request_factory.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb index 30c423e..523b50f 100644 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ b/rswag-specs/lib/rswag/specs/request_factory.rb @@ -118,7 +118,8 @@ module Rswag def build_query_string_part(param, value) name = param[:name] - return "#{name}=#{value}" unless param.dig(:schema, :type)&.to_sym == :array + type = param[:type] || param.dig(:schema, :type) + return "#{name}=#{value}" unless type&.to_sym == :array case param[:collectionFormat] when :ssv From d090516f481dc5ee4037b51294adedbaac861621 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Tue, 13 Oct 2020 16:24:51 -0700 Subject: [PATCH 6/8] properly list servers for openapi v3 --- test-app/spec/integration/openapi3_spec.rb | 69 +++++++++++++++++++--- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/test-app/spec/integration/openapi3_spec.rb b/test-app/spec/integration/openapi3_spec.rb index 7515ef1..051c464 100644 --- a/test-app/spec/integration/openapi3_spec.rb +++ b/test-app/spec/integration/openapi3_spec.rb @@ -20,22 +20,77 @@ RSpec.describe 'Generated OpenApi', type: :request, swagger_doc: 'v3/openapi.jso let(:swagger_doc) do { # That which would be defined in swagger_helper.rb openapi: api_openapi, - basePath: api_base_path, - schemes: api_schemes, - host: api_host, + info: {}, + servers: api_servers, produces: api_produces, components: api_components } end let(:api_openapi) { '3.0.3' } - let(:api_base_path) { '/foo' } - let(:api_schemes) { ['https'] } - let(:api_host) { 'api.example.com' } + let(:api_servers) {[{ url: "https://api.example.com/foo" }]} let(:api_produces) { ['application/json'] } let(:api_components) { {} } describe 'Basic Structure' - describe 'API Server and Base Path' + + describe 'API Server and Base Path' do + path '/stubs' do + get 'a summary' do + tags 'Server and Path' + + response '200', 'OK' do + run_test! + + it 'lists server' do + tree = swagger_doc.dig(:servers) + expect(tree).to eq([ + { url: "https://api.example.com/foo" } + ]) + end + + context "multiple" do + let(:api_servers) {[ + { url: "https://api.example.com/foo" }, + { url: "http://api.example.com/foo" }, + ]} + + it 'lists servers' do + tree = swagger_doc.dig(:servers) + expect(tree).to eq([ + { url: "https://api.example.com/foo" }, + { url: "http://api.example.com/foo" } + ]) + end + end + + context "with variables" do + let(:api_servers) {[{ + url: "https://{defaultHost}/foo", + variables: { + defaultHost: { + default: "api.example.com" + } + } + }]} + + it 'lists server and variables' do + tree = swagger_doc.dig(:servers) + expect(tree).to eq([{ + url: "https://{defaultHost}/foo", + variables: { + defaultHost: { + default: "api.example.com" + } + } + }]) + end + end + + # TODO: Enum variables, defaults, override at path/operation + end + end + end + end describe 'Media Types' do path '/stubs' do From 3e10b09f23b5ee63b765b2487b474952a984e495 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Tue, 13 Oct 2020 16:27:32 -0700 Subject: [PATCH 7/8] swap deprecated SecurityDefinitions for SecuritySchemes --- test-app/spec/swagger_helper.rb | 36 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/swagger_helper.rb index 3b8dfae..ea72c4d 100644 --- a/test-app/spec/swagger_helper.rb +++ b/test-app/spec/swagger_helper.rb @@ -67,14 +67,16 @@ RSpec.configure do |config| required: ['id', 'headline'] } }, - securityDefinitions: { - basic_auth: { - type: :basic - }, - api_key: { - type: :apiKey, - name: 'api_key', - in: :query + components: { + securitySchemes: { + basic_auth: { + type: :basic + }, + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } } } }, @@ -130,14 +132,16 @@ RSpec.configure do |config| required: ['id', 'headline'] } }, - securityDefinitions: { - basic_auth: { - type: :basic - }, - api_key: { - type: :apiKey, - name: 'api_key', - in: :query + components: { + securitySchemes: { + basic_auth: { + type: :basic + }, + api_key: { + type: :apiKey, + name: 'api_key', + in: :query + } } } } From ab457743a820d0544fd464f86a0920ba13a224e0 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Tue, 13 Oct 2020 16:28:40 -0700 Subject: [PATCH 8/8] also move away from deprecated type: basic --- test-app/spec/swagger_helper.rb | 6 ++++-- test-app/swagger/v1/swagger.json | 3 ++- test-app/swagger/v3/openapi.json | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test-app/spec/swagger_helper.rb b/test-app/spec/swagger_helper.rb index ea72c4d..cadb592 100644 --- a/test-app/spec/swagger_helper.rb +++ b/test-app/spec/swagger_helper.rb @@ -70,7 +70,8 @@ RSpec.configure do |config| components: { securitySchemes: { basic_auth: { - type: :basic + type: :http, + scheme: :basic }, api_key: { type: :apiKey, @@ -135,7 +136,8 @@ RSpec.configure do |config| components: { securitySchemes: { basic_auth: { - type: :basic + type: :http, + scheme: :basic }, api_key: { type: :apiKey, diff --git a/test-app/swagger/v1/swagger.json b/test-app/swagger/v1/swagger.json index 957206c..e18f579 100644 --- a/test-app/swagger/v1/swagger.json +++ b/test-app/swagger/v1/swagger.json @@ -377,7 +377,8 @@ "components": { "securitySchemes": { "basic_auth": { - "type": "basic" + "type": "http", + "scheme": "basic" }, "api_key": { "type": "apiKey", diff --git a/test-app/swagger/v3/openapi.json b/test-app/swagger/v3/openapi.json index 97506c4..90b9f18 100644 --- a/test-app/swagger/v3/openapi.json +++ b/test-app/swagger/v3/openapi.json @@ -127,7 +127,8 @@ "components": { "securitySchemes": { "basic_auth": { - "type": "basic" + "type": "http", + "scheme": "basic" }, "api_key": { "type": "apiKey",