diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93077e3a..fc31f1e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ jobs: matrix: include: # Recent Rubies and Rails + - ruby-version: '3.3' - ruby-version: '3.2' - ruby-version: '3.1' - ruby-version: '3.0' diff --git a/features/open_api.feature b/features/open_api.feature index a24b7094..cece521e 100644 --- a/features/open_api.feature +++ b/features/open_api.feature @@ -1,3 +1,4 @@ +@ruby27_required Feature: Generate Open API Specification from test examples Background: @@ -290,531 +291,507 @@ Feature: Generate Open API Specification from test examples And the exit status should be 0 Scenario: Index file should look like we expect - Then the file "doc/api/open_api.json" should contain exactly: + Then the file "doc/api/open_api.json" should contain JSON exactly like: """ - { - "swagger": "2.0", - "info": { - "title": "OpenAPI App", - "description": "This is a sample of OpenAPI specification.", - "termsOfService": "http://open-api.io/terms/", - "contact": { - "name": "API Support", - "url": "http://www.open-api.io/support", - "email": "support@open-api.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0.1" - }, - "host": "localhost:3000", - "schemes": [ - "http" - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "paths": { - "/not_hided": { - }, - "/instructions": { - "get": { - "tags": [ - "Instructions" - ], - "summary": "This should be used to get all instructions.", - "description": "This description came from config.yml 1", - "consumes": [ - - ], - "produces": [ - "text/html" - ], - "parameters": [ - - ], - "responses": { - "200": { - "description": "List all instructions", - "schema": { - "type": "object", - "properties": { - } - }, - "headers": { - "Content-Type": { - "type": "string", - "x-example-value": "text/html;charset=utf-8" - }, - "Content-Length": { - "type": "string", - "x-example-value": "57" - } - }, - "examples": { - "text/html": { - "data": { - "id": "1", - "type": "instructions", - "attributes": { - } - } - } +{ + "swagger": "2.0", + "info": { + "title": "OpenAPI App", + "description": "This is a sample of OpenAPI specification.", + "termsOfService": "http://open-api.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.open-api.io/support", + "email": "support@open-api.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.1" + }, + "host": "localhost:3000", + "schemes": [ + "http" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/not_hided": {}, + "/instructions": { + "get": { + "tags": [ + "Instructions" + ], + "summary": "This should be used to get all instructions.", + "description": "This description came from config.yml 1", + "consumes": [], + "produces": [ + "text/html" + ], + "parameters": [], + "responses": { + "200": { + "description": "List all instructions", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "text/html;charset=utf-8" + }, + "Content-Length": { + "type": "string", + "x-example-value": "57" + } + }, + "examples": { + "text/html": { + "data": { + "id": "1", + "type": "instructions", + "attributes": {} } } + } + } + }, + "deprecated": false, + "security": [] + } + }, + "/orders": { + "get": { + "tags": [ + "Orders" + ], + "summary": "This URL allows users to interact with all orders.", + "description": "Long description.", + "consumes": [], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "one_level_array", + "in": "query", + "description": "One level array", + "required": false, + "type": "array", + "items": { + "type": "string", + "enum": [ + "string1", + "string2" + ] }, - "deprecated": false, - "security": [ - + "default": [ + "string1" + ] + }, + { + "name": "two_level_array", + "in": "query", + "description": "Two level array", + "required": false, + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "one_level_arr", + "in": "query", + "description": "One level arr", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "value1", + "value2" + ] + }, + { + "name": "two_level_arr", + "in": "query", + "description": "Two level arr", + "required": false, + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + } + }, + "example": [ + [ + 5.1, + 3.0 + ], + [ + 1.0, + 4.5 + ] ] } - }, - "/orders": { - "get": { - "tags": [ - "Orders" - ], - "summary": "This URL allows users to interact with all orders.", - "description": "Long description.", - "consumes": [ - - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "one_level_array", - "in": "query", - "description": " one level array", - "required": false, - "type": "array", - "items": { - "type": "string", - "enum": [ - "string1", - "string2" - ] - }, - "default": [ - "string1" - ] - }, - { - "name": "two_level_array", - "in": "query", - "description": " two level array", - "required": false, - "type": "array", - "items": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "one_level_arr", - "in": "query", - "description": " one level arr", - "required": false, - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "value1", - "value2" - ] + ], + "responses": { + "200": { + "description": "Getting a list of orders", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/vnd.api+json" }, - { - "name": "two_level_arr", - "in": "query", - "description": " two level arr", - "required": false, - "type": "array", - "items": { - "type": "array", - "items": { - "type": "number" - } - }, - "example": [ - [ - 5.1, - 3.0 - ], - [ - 1.0, - 4.5 - ] - ] + "Content-Length": { + "type": "string", + "x-example-value": "137" } - ], - "responses": { - "200": { - "description": "Getting a list of orders", - "schema": { - "type": "object", - "properties": { - } - }, - "headers": { - "Content-Type": { - "type": "string", - "x-example-value": "application/vnd.api+json" + }, + "examples": { + "application/vnd.api+json": { + "page": 1, + "orders": [ + { + "name": "Order 1", + "amount": 9.99, + "description": null }, - "Content-Length": { - "type": "string", - "x-example-value": "137" + { + "name": "Order 2", + "amount": 100.0, + "description": "A great order" } - }, - "examples": { - "application/vnd.api+json": { - "page": 1, - "orders": [ - { - "name": "Order 1", - "amount": 9.99, - "description": null - }, - { - "name": "Order 2", - "amount": 100.0, - "description": "A great order" - } - ] - } - } + ] } - }, - "deprecated": false, - "security": [ - - ] - }, - "post": { - "tags": [ - "Orders" - ], - "summary": "This is used to create orders.", - "description": "This description came from config.yml 2", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "in": "body", - "description": "", - "required": false, - "schema": { + } + } + }, + "deprecated": false, + "security": [] + }, + "post": { + "tags": [ + "Orders" + ], + "summary": "This is used to create orders.", + "description": "This description came from config.yml 2", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "body", + "in": "body", + "description": "", + "required": false, + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { - "data": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "Order 1", - "default": "name", - "description": "Data name" - }, - "description": { - "type": "string", - "example": "A description", - "description": "Data description" - }, - "amount": { - "type": "number", - "example": 100.0, - "description": "Data amount", - "minimum": 0, - "maximum": 100 - }, - "values": { - "type": "array", - "example": [ - 5.0, - 1.0 - ], - "description": "Data values", - "items": { - "type": "number", - "enum": [ - 1, - 2, - 3, - 5 - ] - } - } + "name": { + "type": "string", + "example": "Order 1", + "default": "name", + "description": "Data name" + }, + "description": { + "type": "string", + "example": "A description", + "description": "Data description" + }, + "amount": { + "type": "number", + "example": 100.0, + "description": "Data amount", + "minimum": 0, + "maximum": 100 + }, + "values": { + "type": "array", + "example": [ + 5.0, + 1.0 + ], + "description": "Data values", + "items": { + "type": "number", + "enum": [ + 1, + 2, + 3, + 5 + ] } } } } } - ], - "responses": { - "201": { - "description": "Creating an order", - "schema": { - "type": "object", - "properties": { - } - }, - "headers": { - "Content-Type": { - "type": "string", - "x-example-value": "application/json" - }, - "Content-Length": { - "type": "string", - "x-example-value": "73" - } - }, - "examples": { - "application/json": { - "order": { - "name": "Order 1", - "amount": 100.0, - "description": "A great order" - } - } - } + } + } + ], + "responses": { + "201": { + "description": "Creating an order", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "73" } }, - "deprecated": false, - "security": [ - - ] + "examples": { + "application/json": { + "order": { + "name": "Order 1", + "amount": 100.0, + "description": "A great order" + } + } + } } }, - "/orders/{id}": { - "get": { - "tags": [ - "Orders" - ], - "summary": "This is used to return orders.", - "description": "Returns a specific order.", - "consumes": [ - - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "deprecated": false, + "security": [] + } + }, + "/orders/{id}": { + "get": { + "tags": [ + "Orders" + ], + "summary": "This is used to return orders.", + "description": "Returns a specific order.", + "consumes": [], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Getting a specific order", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "73" } - ], - "responses": { - "200": { - "description": "Getting a specific order", - "schema": { - "type": "object", - "properties": { - } - }, - "headers": { - "Content-Type": { - "type": "string", - "x-example-value": "application/json" - }, - "Content-Length": { - "type": "string", - "x-example-value": "73" - } - }, - "examples": { - "application/json": { - "order": { - "name": "Order 1", - "amount": 100.0, - "description": "A great order" - } - } + }, + "examples": { + "application/json": { + "order": { + "name": "Order 1", + "amount": 100.0, + "description": "A great order" } } - }, - "deprecated": false, - "security": [ - - ] + } + } + }, + "deprecated": false, + "security": [] + }, + "put": { + "tags": [ + "Orders" + ], + "summary": "This is used to update orders.", + "description": "", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" }, - "put": { - "tags": [ - "Orders" - ], - "summary": "This is used to update orders.", + { + "name": "body", + "in": "body", "description": "", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "type": "integer" - }, - { - "name": "body", - "in": "body", - "description": "", - "required": false, - "schema": { + "required": false, + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { - "data": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "order", - "description": "The order name" - }, - "amount": { - "type": "integer", - "example": 1, - "description": "Data amount" - }, - "description": { - "type": "string", - "example": "fast order", - "description": "The order description" - } - }, - "required": [ - "name", - "description" - ] + "name": { + "type": "string", + "example": "order", + "description": "The order name" + }, + "amount": { + "type": "integer", + "example": 1, + "description": "Data amount" + }, + "description": { + "type": "string", + "example": "fast order", + "description": "The order description" } - } - } - } - ], - "responses": { - "200": { - "description": "Update an order", - "schema": { - "type": "object", - "properties": { - } - }, - "headers": { - "Content-Type": { - "type": "string", - "x-example-value": "application/json" }, - "Content-Length": { - "type": "string", - "x-example-value": "63" - } - }, - "examples": { - } - }, - "400": { - "description": "Invalid request", - "schema": { - "type": "object", - "properties": { - } - }, - "headers": { - "Content-Type": { - "type": "string", - "x-example-value": "application/json" - }, - "Content-Length": { - "type": "string", - "x-example-value": "0" - } - }, - "examples": { + "required": [ + "name", + "description" + ] } } + } + } + ], + "responses": { + "200": { + "description": "Update an order", + "schema": { + "type": "object", + "properties": {} }, - "deprecated": false, - "security": [ - - ] - }, - "delete": { - "tags": [ - "Orders" - ], - "summary": "This is used to delete orders.", - "description": "", - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "text/html" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "63" } - ], - "responses": { - "200": { - "description": "Deleting an order", - "schema": { - "type": "object", - "properties": { - } - }, - "headers": { - "Content-Type": { - "type": "string", - "x-example-value": "text/html;charset=utf-8" - }, - "Content-Length": { - "type": "string", - "x-example-value": "0" - } - }, - "examples": { + }, + "examples": { + "application/json": { + "data": { + "name": "order", + "amount": 1, + "description": "fast order" } } + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "properties": {} }, - "deprecated": false, - "security": [ - - ] + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "application/json" + }, + "Content-Length": { + "type": "string", + "x-example-value": "0" + } + }, + "examples": {} } - } + }, + "deprecated": false, + "security": [] }, - "tags": [ - { - "name": "Orders", - "description": "Order's tag description" + "delete": { + "tags": [ + "Orders" + ], + "summary": "This is used to delete orders.", + "description": "", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "text/html" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Deleting an order", + "schema": { + "type": "object", + "properties": {} + }, + "headers": { + "Content-Type": { + "type": "string", + "x-example-value": "text/html;charset=utf-8" + }, + "Content-Length": { + "type": "string", + "x-example-value": "0" + } + }, + "examples": {} + } }, - { - "name": "Instructions", - "description": "Instructions help the users use the app." - } - ] + "deprecated": false, + "security": [] + } + } + }, + "tags": [ + { + "name": "Orders", + "description": "Order's tag description" + }, + { + "name": "Instructions", + "description": "Instructions help the users use the app." } + ] +} """ Scenario: Example 'Deleting an order' file should not be created diff --git a/features/step_definitions/json_steps.rb b/features/step_definitions/json_steps.rb index c04867ac..1b5733be 100644 --- a/features/step_definitions/json_steps.rb +++ b/features/step_definitions/json_steps.rb @@ -1,3 +1,5 @@ Then /^the file "(.*?)" should contain JSON exactly like:$/ do |file, exact_content| - expect(JSON.parse(read(file).join)).to eq(JSON.parse(exact_content)) + actual = JSON.dump(JSON.parse(read(file).join)) + expected = JSON.dump(JSON.parse(exact_content)) + expect(actual).to eq(expected) end diff --git a/features/support/env.rb b/features/support/env.rb index cfb550d6..86f6b8f4 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -9,3 +9,9 @@ config.match = :prefer_exact config.ignore_hidden_elements = false end + +Before('@ruby27_required') do |scenario| + if RUBY_VERSION < '2.7' + raise Cucumber::Pending, "Skipped on Ruby #{RUBY_VERSION} (requires >= 2.7)" + end +end diff --git a/lib/rspec_api_documentation.rb b/lib/rspec_api_documentation.rb index a15e6018..5f07de9e 100644 --- a/lib/rspec_api_documentation.rb +++ b/lib/rspec_api_documentation.rb @@ -1,3 +1,4 @@ +require 'logger' # Ensure Logger is loaded for ActiveSupport 6.1+ compatibility with Ruby <= 2.6 require 'active_support' require 'active_support/inflector' require 'active_support/core_ext/array/extract_options' diff --git a/lib/rspec_api_documentation/api_formatter.rb b/lib/rspec_api_documentation/api_formatter.rb index 7a9f97f3..117df936 100644 --- a/lib/rspec_api_documentation/api_formatter.rb +++ b/lib/rspec_api_documentation/api_formatter.rb @@ -2,7 +2,7 @@ module RspecApiDocumentation class ApiFormatter < RSpec::Core::Formatters::BaseTextFormatter - RSpec::Core::Formatters.register self, :example_passed, :example_failed, :stop + RSpec::Core::Formatters.register self, :example_passed, :example_failed, :stop, :example_group_started def initialize(output) super @@ -19,7 +19,7 @@ def start(notification) def example_group_started(notification) super - output.puts " #{@example_group.description}" + output.puts " #{notification.group.description}" end def example_passed(example_notification) diff --git a/lib/rspec_api_documentation/client_base.rb b/lib/rspec_api_documentation/client_base.rb index db0560a3..34ccbdb1 100644 --- a/lib/rspec_api_documentation/client_base.rb +++ b/lib/rspec_api_documentation/client_base.rb @@ -45,7 +45,8 @@ def process(method, path, params = {}, headers ={}) def read_request_body input = last_request.env["rack.input"] - input.rewind + return "" unless input + input.rewind if input.respond_to?(:rewind) input.read end @@ -89,6 +90,12 @@ def record_response_body(response_content_type, response_body) return nil if response_body.empty? formatter = RspecApiDocumentation.configuration.response_body_formatter + # Only force UTF-8 for text-based content types + if response_body.respond_to?(:encoding) && response_body.encoding == Encoding::ASCII_8BIT + if response_content_type && (response_content_type.include?('json') || response_content_type.include?('text')) + response_body = response_body.force_encoding(Encoding::UTF_8) + end + end formatter.call(response_content_type, response_body) end diff --git a/lib/rspec_api_documentation/headers.rb b/lib/rspec_api_documentation/headers.rb index d3041cde..465fe5cf 100644 --- a/lib/rspec_api_documentation/headers.rb +++ b/lib/rspec_api_documentation/headers.rb @@ -6,7 +6,7 @@ def env_to_headers(env) headers = {} env.each do |key, value| # HTTP_ACCEPT_CHARSET => Accept-Charset - if key =~ /^(HTTP_|CONTENT_TYPE)/ + if key =~ /^(HTTP_|CONTENT_TYPE)/ && key != "HTTP_VERSION" header = key.gsub(/^HTTP_/, '').split('_').map{|s| s.titleize}.join("-") headers[header] = value end diff --git a/lib/rspec_api_documentation/oauth2_mac_client.rb b/lib/rspec_api_documentation/oauth2_mac_client.rb index 596171a9..51aaf9e1 100644 --- a/lib/rspec_api_documentation/oauth2_mac_client.rb +++ b/lib/rspec_api_documentation/oauth2_mac_client.rb @@ -25,7 +25,13 @@ def request_headers end def response_headers - last_response.headers + if last_response.respond_to?(:headers) + last_response.headers + elsif last_response.respond_to?(:env) && last_response.env.respond_to?(:response_headers) + last_response.env.response_headers + else + {} + end end def query_string @@ -33,7 +39,11 @@ def query_string end def status - last_response.status + if last_response.respond_to?(:status) + last_response.status + else + last_response.env.status if last_response.respond_to?(:env) + end end def response_body @@ -45,7 +55,13 @@ def request_content_type end def response_content_type - last_response.content_type + if last_response.respond_to?(:content_type) + last_response.content_type + elsif last_response.respond_to?(:headers) + last_response.headers['Content-Type'] || last_response.headers['content-type'] + else + nil + end end protected @@ -71,7 +87,13 @@ def access_token @access_token ||= begin app = ProxyApp.new(self, context.app) stub_request(:any, %r{http://example\.com}).to_rack(app) - Rack::OAuth2::Client.new(options.merge(:host => "example.com", :scheme => "http")).access_token! + + # Create a Bearer access token as MAC is no longer supported + access_token = Rack::OAuth2::AccessToken::Bearer.new( + :access_token => options[:identifier] || "1" + ) + + access_token end end end diff --git a/lib/rspec_api_documentation/open_api/node.rb b/lib/rspec_api_documentation/open_api/node.rb index 2f102c88..47669ab0 100644 --- a/lib/rspec_api_documentation/open_api/node.rb +++ b/lib/rspec_api_documentation/open_api/node.rb @@ -102,9 +102,10 @@ def as_json end end + def settings; @settings ||= {} end + private - def settings; @settings ||= {} end def instance_settings; @instance_settings ||= [] end def self.class_settings; @class_settings ||= [] end end diff --git a/lib/rspec_api_documentation/views/api_blueprint_index.rb b/lib/rspec_api_documentation/views/api_blueprint_index.rb index ef42c1fa..0289d349 100644 --- a/lib/rspec_api_documentation/views/api_blueprint_index.rb +++ b/lib/rspec_api_documentation/views/api_blueprint_index.rb @@ -103,7 +103,8 @@ def fields(property_name, examples) # equals the name, I assume it is blank. def description_blank?(property) !property[:description] || - property[:description].to_s.strip == property[:name].to_s.strip + property[:description].to_s.strip == property[:name].to_s.strip || + property[:description].to_s.strip == property[:name].to_s.humanize end end end diff --git a/lib/rspec_api_documentation/writers/formatter.rb b/lib/rspec_api_documentation/writers/formatter.rb index 11c70dd8..a7d35da0 100644 --- a/lib/rspec_api_documentation/writers/formatter.rb +++ b/lib/rspec_api_documentation/writers/formatter.rb @@ -3,9 +3,9 @@ module Writers module Formatter def self.to_json(object) - JSON.pretty_generate(object.as_json) + JSON.pretty_generate(object.as_json) + "\n" end end end -end \ No newline at end of file +end diff --git a/lib/rspec_api_documentation/writers/open_api_writer.rb b/lib/rspec_api_documentation/writers/open_api_writer.rb index ed5d0420..bb8a871f 100644 --- a/lib/rspec_api_documentation/writers/open_api_writer.rb +++ b/lib/rspec_api_documentation/writers/open_api_writer.rb @@ -119,7 +119,7 @@ def process_responses(responses, example) if /\A(?[^;]+)/ =~ request[:response_content_type] response.safe_assign_setting(:examples, OpenApi::Example.new) response_body = JSON.parse(request[:response_body]) rescue nil - response.examples.add_setting response_content_type, :value => response_body + response.examples.add_setting response_content_type, :value => response_body if response_body end responses.add_setting "#{request[:response_status]}", :value => response end diff --git a/rspec_api_documentation.gemspec b/rspec_api_documentation.gemspec index 978b3bf0..b96fcdf7 100644 --- a/rspec_api_documentation.gemspec +++ b/rspec_api_documentation.gemspec @@ -15,28 +15,52 @@ Gem::Specification.new do |s| s.required_rubygems_version = ">= 1.3.6" s.add_runtime_dependency "rspec", "~> 3.0" + s.add_development_dependency "rspec", "~> 3.0" s.add_runtime_dependency "activesupport", ">= 3.0.0" s.add_runtime_dependency "mustache", "~> 1.0", ">= 0.99.4" - s.add_development_dependency "bundler", ">= 1.16" - s.add_development_dependency "fakefs", "~> 0.6.0" - s.add_development_dependency "sinatra", "~> 2.0.8" - s.add_development_dependency "aruba", "~> 0.14.14" - s.add_development_dependency "capybara", "~> 3.39.2" - s.add_development_dependency "rake", "~> 13.2.1" - s.add_development_dependency "rack-test", "~> 0.6.3" - s.add_development_dependency "rack-oauth2", "~> 1.12.0" - s.add_development_dependency "webmock", "~> 3.23.0" - s.add_development_dependency "rspec-its", "~> 1.3.0" - s.add_development_dependency "faraday", "~> 1.0.0" - s.add_development_dependency "nokogiri", "~> 1.8.4" - s.add_development_dependency "yard", "~> 0.9.15" - s.add_development_dependency "inch", "~> 0.8.0" - s.add_development_dependency "minitest", "~> 5.8.4" - s.add_development_dependency "contracts", "~> 0.17" - s.add_development_dependency "gherkin", "~> 9.0.0" - s.add_development_dependency "multi_json", "~> 1.15.0" - s.add_development_dependency "rspec", "~> 3.0" + if RUBY_VERSION < '2.7' + s.add_development_dependency "bundler", ">= 1.16" + s.add_development_dependency "fakefs", "~> 0.6.0" + s.add_development_dependency "sinatra", "~> 1.4.7" + s.add_development_dependency "aruba", "~> 0.13.0" + s.add_development_dependency "capybara", "~> 2.6.2" + s.add_development_dependency "rake", "~> 10.5.0" + s.add_development_dependency "rack-test", "~> 0.6.3" + s.add_development_dependency "rack-oauth2", "~> 1.2.2" + s.add_development_dependency "webmock", "~> 3.8.3" + s.add_development_dependency "rspec-its", "~> 1.2.0" + s.add_development_dependency "faraday", "~> 1.0.0" + s.add_development_dependency "nokogiri", "~> 1.8.4" + s.add_development_dependency "yard", "~> 0.9.15" + s.add_development_dependency "inch", "~> 0.8.0" + s.add_development_dependency "minitest", "~> 5.8.4" + s.add_development_dependency "contracts", "~> 0.13.0" + s.add_development_dependency "gherkin", "~> 3.2.0" + s.add_development_dependency "multi_json", "~> 1.11.2" + else + s.add_development_dependency "bundler", ">= 1.16" + s.add_development_dependency "fakefs" + s.add_development_dependency "sinatra", "~> 2.0" + s.add_development_dependency "aruba" + s.add_development_dependency "capybara" + s.add_development_dependency "rake" + s.add_development_dependency "rack", "~> 2.2" + s.add_development_dependency "rack-test" + s.add_development_dependency "rack-oauth2" + s.add_development_dependency "webmock" + s.add_development_dependency "rspec-its" + s.add_development_dependency "faraday" + s.add_development_dependency "nokogiri" + s.add_development_dependency "yard" + s.add_development_dependency "inch" + s.add_development_dependency "minitest" + s.add_development_dependency "contracts" + s.add_development_dependency "gherkin" + s.add_development_dependency "multi_json" + s.add_development_dependency "webrick" + s.add_development_dependency "rackup" + end s.files = Dir.glob("lib/**/*") + Dir.glob("templates/**/*") s.require_path = "lib" diff --git a/spec/api_formatter_spec.rb b/spec/api_formatter_spec.rb index 266cad8d..d0fc442a 100644 --- a/spec/api_formatter_spec.rb +++ b/spec/api_formatter_spec.rb @@ -2,7 +2,18 @@ describe RspecApiDocumentation::ApiFormatter do let(:metadata) { {} } - let(:group) { RSpec::Core::ExampleGroup.describe("Orders", metadata) } + let(:group) { + # Create an anonymous class that inherits from ExampleGroup but doesn't auto-register + Class.new(RSpec::Core::ExampleGroup) do + def self.description + "Orders" + end + + def self.metadata + {} + end + end + } let(:output) { StringIO.new } let(:formatter) { RspecApiDocumentation::ApiFormatter.new(output) } diff --git a/spec/example_spec.rb b/spec/example_spec.rb index 1aa94610..3d78b885 100644 --- a/spec/example_spec.rb +++ b/spec/example_spec.rb @@ -64,7 +64,7 @@ end context "when the example is pending" do - let(:rspec_example) { rspec_example_group.pending(description, metadata) {} } + let(:rspec_example) { rspec_example_group.pending(description, metadata) { raise "Pending example" } } it { should be_falsey } end diff --git a/spec/http_test_client_spec.rb b/spec/http_test_client_spec.rb index fd77dc0f..a95e54dc 100644 --- a/spec/http_test_client_spec.rb +++ b/spec/http_test_client_spec.rb @@ -2,14 +2,21 @@ require 'rack/test' require 'capybara' require 'capybara/server' -require 'sinatra/base' require 'webmock/rspec' require 'support/stub_app' describe RspecApiDocumentation::HttpTestClient do before(:all) do + if RUBY_VERSION < '2.7' + skip("Skipped on Ruby #{RUBY_VERSION} (requires >= 2.7)") + end WebMock.allow_net_connect! - server = Capybara::Server.new(StubApp.new, 8888) + # Capybara.server= was introduced in later versions + # For older versions, we use the Capybara::Server directly with webrick + if Capybara.respond_to?(:server=) + Capybara.server = :webrick + end + server = Capybara::Server.new(StubApp.new, port: 8888) server.boot end diff --git a/spec/rack_test_client_spec.rb b/spec/rack_test_client_spec.rb index e3a9b53c..f1b57b84 100644 --- a/spec/rack_test_client_spec.rb +++ b/spec/rack_test_client_spec.rb @@ -1,10 +1,9 @@ require 'spec_helper' require 'rack/test' -require 'sinatra/base' require 'support/stub_app' describe RspecApiDocumentation::RackTestClient do - let(:context) { |example| double(:app => StubApp, :example => example) } + let(:context) { |example| double(:app => StubApp.new, :example => example) } let(:test_client) { RspecApiDocumentation::RackTestClient.new(context, {}) } subject { test_client } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 918dd620..95fd852d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,4 +4,16 @@ require 'pry' RSpec.configure do |config| + config.before(:all) do + if self.class.metadata[:api_doc_dsl] || self.respond_to?(:app) + begin + require 'support/stub_app' + RspecApiDocumentation.configure do |config| + config.app = StubApp.new unless config.app + end + rescue LoadError + # StubApp not available, skip + end + end + end end diff --git a/spec/support/stub_app.rb b/spec/support/stub_app.rb index 35226be2..8f2a334f 100644 --- a/spec/support/stub_app.rb +++ b/spec/support/stub_app.rb @@ -1,31 +1,29 @@ -class StubApp < Sinatra::Base - get "/" do - content_type :json +class StubApp + def call(env) + req = Rack::Request.new(env) - { :hello => "world" }.to_json - end + case "#{req.request_method} #{req.path_info}" + when "GET /" + [200, {'Content-Type' => 'application/json'}, [{ :hello => "world" }.to_json]] + when "POST /greet" + body = req.body.read + req.body.rewind if req.body.respond_to?(:rewind) - post "/greet" do - content_type :json + begin + data = JSON.parse(body) if body && !body.empty? + rescue JSON::ParserError + data = nil + end - request.body.rewind - begin - data = JSON.parse request.body.read - rescue JSON::ParserError - request.body.rewind - data = request.body.read + target = data.is_a?(Hash) ? data["target"] : "nurse" + [200, {'Content-Type' => 'application/json', 'Content-Length' => '17'}, [{ :hello => target }.to_json]] + when "GET /xml" + [200, {'Content-Type' => 'application/xml'}, ["World"]] + when "GET /binary" + [200, {'Content-Type' => 'application/octet-stream'}, ["\x01\x02\x03".force_encoding(Encoding::ASCII_8BIT)]] + else + [404, {'Content-Type' => 'text/plain'}, ["Not Found"]] end - { :hello => data["target"] }.to_json - end - - get "/xml" do - content_type :xml - - "World" - end - - get '/binary' do - content_type 'application/octet-stream' - "\x01\x02\x03".force_encoding(Encoding::ASCII_8BIT) end end + diff --git a/spec/views/api_blueprint_index_spec.rb b/spec/views/api_blueprint_index_spec.rb index e923abf2..1d526597 100644 --- a/spec/views/api_blueprint_index_spec.rb +++ b/spec/views/api_blueprint_index_spec.rb @@ -142,7 +142,7 @@ properties_description: "required, string" }, { name: "option", - description: 'Option', + description: nil, properties_description: 'optional' }] expect(post_route_with_optionals[:has_attributes?]).to eq false @@ -158,7 +158,7 @@ expect(posts_route[:attributes]).to eq [{ required: false, name: "description", - description: 'Description', + description: nil, properties_description: "optional" }] end