diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..57df306 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,17 @@ +## Describe the bug +A clear and concise description of what the bug is. + +## Steps to Test or Reproduce +Please provide an example repo or the steps to reproduce the behavior. + +## Expected behavior +A clear and concise description of what you expected to happen. + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Additional context +Add any other context about the problem here. + +## Rswag Version +The version of rswag are you using. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bde1cd0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,11 @@ +## Is your feature request related to a problem? Please describe. +A clear and concise description of what the problem is. + +## Describe the solution you'd like +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered +A clear and concise description of any alternative solutions or features you've considered. + +## Additional context +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..73c44a1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,15 @@ +## Problem +A clear and concise description of what the problem is. + +## Solution +A clear and concise description of what the solution is. + +### Related Issues +Links to any related issues. + +### Checklist +- [ ] Added tests +- [ ] Changelog updated + +### Steps to Test or Reproduce +Outline the steps to test or reproduce the PR here. \ No newline at end of file diff --git a/.ruby-version b/.ruby-version index 2bf1c1c..ec1cf33 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.6.3 diff --git a/.travis.yml b/.travis.yml index bdf321a..f987c52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,35 @@ language: ruby +dist: bionic +services: + - xvfb + rvm: - - 2.2.5 + - 2.6.3 env: + - RAILS_VERSION=6.0.0 - RAILS_VERSION=5.2.0 - - RAILS_VERSION=4.2.0 - - RAILS_VERSION=3.2.22 + +addons: + apt: + packages: + - libqtwebkit-dev + - libqtwebkit4 cache: directories: - - /home/travis/.rvm/gems/ruby-2.2.5 + - /home/travis/.rvm/gems/ruby-2.6.3 install: ./ci/build.sh -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - - sleep 3 - script: ./ci/test.sh jobs: include: - stage: publish components script: 'cd rswag-api' + if: tag IS present deploy: gemspec: rswag-api.gemspec provider: rubygems @@ -35,6 +40,7 @@ jobs: - stage: publish components script: 'cd rswag-specs' + if: tag IS present deploy: gemspec: rswag-specs.gemspec provider: rubygems @@ -45,6 +51,7 @@ jobs: - stage: publish components script: 'cd rswag-ui' + if: tag IS present deploy: gemspec: rswag-ui.gemspec provider: rubygems @@ -56,6 +63,7 @@ jobs: - stage: publish rswag script: 'cd rswag' + if: tag IS present deploy: gemspec: rswag.gemspec provider: rubygems diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aed1d80 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# rswag +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added +### Changed +- Update swagger-ui version to 3.23.11 [#239](https://github.com/rswag/rswag/pull/239) +### Deprecated +### Removed +### Fixed +### Security + +## [2.2.0] - 2019-11-01 +### Added +- New swagger_format config option for setting YAML output [#251](https://github.com/rswag/rswag/pull/251) +### Changed +- rswag-api will serve yaml files as yaml [#251](https://github.com/rswag/rswag/pull/251) + +## [2.1.1] - 2019-10-18 +### Fixed +- Fix incorrect require reference for swagger_generator [#248](https://github.com/rswag/rswag/issues/248) + +## [2.1.0] - 2019-10-17 +### Added +- New Spec Generator [#75](https://github.com/rswag/rswag/pull/75) +- Support for Options and Trace verbs; You must use a framework that supports this, for Options Rails 6.1+ Rails 6 does not support Trace. [#237](https://github.com/rswag/rswag/pull/75) +### Changed +- Update swagger-ui to 3.18.2 [#240](https://github.com/rswag/rswag/pull/240) + +## [2.0.6] - 2019-10-03 +### Added +- Support for Rails 6 [#228](https://github.com/rswag/rswag/pull/228) +- Support for Windows paths [#176](https://github.com/rswag/rswag/pull/176) +### Changed +- Show response body when error code is not expected [#117](https://github.com/rswag/rswag/pull/177) + +## [2.0.5] - 2018-07-10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7db3926 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing + +🎉 Thanks for taking the time to contribute! 🎉 + +We put forward the philosophy put forward by the [react community](https://reactcommunity.org/) about ownership, responsibility and avoiding burnout. + +We also strive to achieve [semantic versioning](https://semver.org/) for this repo. + +## Fork, then clone the repo: +``` +git clone git@github.com:rswag/rswag.git +cd rswag +``` + +## Build +Set up your machine: +``` +./ci/build.sh +``` +Or manually +``` +bundle +cd test-app +bundle exec rake db:setup +cd - + +cd rswag-ui +npm install +cd - +``` + +## Test +Make sure the tests pass: +``` +./ci/test.sh +``` +or manually +``` +cd test-app +bundle exec rspec +``` + +Make your change. Add tests for your change. Make the tests pass: + +``` +bundle exec rspec +``` + +Push to your fork and [submit a Pull Request][pr]. + +[pr]: https://github.com/rswag/rswag/compare/ + +## Updating Swagger UI + +Find the latest versions of swagger-ui here: +https://github.com/swagger-api/swagger-ui/releases + +Update the swagger-ui-dist version in the rswag-ui dependencies +``` +./rswag-ui/package.json +``` + +Navigate to the rswag-ui folder and run npm install to update the package-lock.json + + +## Release +(for maintainers) + +Update the changelog.md, putting the new version number in and moving the Unreleased marker. + +Merge the changes into master you wish to release. + +Add and push a new git tag, annotated tags preferred: +``` +git tag -s 2.0.6 -m 'v2.0.6' +``` + +Travis will detect the tag and release all gems with that tag version number. diff --git a/Gemfile b/Gemfile index bbd3d73..b9f1fbe 100644 --- a/Gemfile +++ b/Gemfile @@ -9,11 +9,16 @@ gem 'rails', "#{rails_version}" case rails_version.split('.').first when '3' gem 'strong_parameters' -when '4', '5' +when '4', '5', '6' gem 'responders' end -gem 'sqlite3' +case rails_version.split('.').first +when '3', '4', '5' + gem 'sqlite3', '~> 1.3.6' +when '6' + gem 'sqlite3', '~> 1.4.1' +end gem 'rswag-api', path: './rswag-api' gem 'rswag-ui', path: './rswag-ui' @@ -23,7 +28,8 @@ group :test do gem 'rspec-rails' gem 'generator_spec' gem 'capybara' - gem 'capybara-webkit' + gem 'geckodriver-helper' + gem 'selenium-webdriver' gem 'rswag-specs', path: './rswag-specs' end diff --git a/README.md b/README.md index be84c11..bc6b121 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ rswag ========= -[![Build Status](https://travis-ci.org/domaindrivendev/rswag.svg?branch=master)](https://travis-ci.org/domaindrivendev/rswag) +[![Build Status](https://travis-ci.org/rswag/rswag.svg?branch=master)](https://travis-ci.org/rswag/rswag) +[![Maintainability](https://api.codeclimate.com/v1/badges/1175b984edc4610f82ab/maintainability)](https://codeclimate.com/github/rswag/rswag/maintainability) [Swagger](http://swagger.io) tooling for Rails API's. Generate beautiful API documentation, including a UI to explore and test operations, directly from your rspec integration tests. -Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed JSON. This toolchain makes it seamless to go from integration specs, which youre probably doing in some form already, to living documentation for your API consumers. +Rswag extends rspec-rails "request specs" with a Swagger-based DSL for describing and testing API operations. You describe your API operations with a succinct, intuitive syntax, and it automaticaly runs the tests. Once you have green tests, run a rake task to auto-generate corresponding Swagger files and expose them as YAML or JSON endpoints. Rswag also provides an embedded version of the awesome [swagger-ui](https://github.com/swagger-api/swagger-ui) that's powered by the exposed file. This toolchain makes it seamless to go from integration specs, which youre probably doing in some form already, to living documentation for your API consumers. And that's not all ... @@ -14,9 +15,9 @@ Once you have an API that can describe itself in Swagger, you've opened the trea |Rswag Version|Swagger (OpenAPI) Spec.|swagger-ui| |----------|----------|----------| -|[master](https://github.com/domaindrivendev/rswag/tree/master)|2.0|3.17.3| -|[2.0.5](https://github.com/domaindrivendev/rswag/tree/2.0.4)|2.0|3.17.3| -|[1.6.0](https://github.com/domaindrivendev/rswag/tree/1.6.0)|2.0|2.2.5| +|[master](https://github.com/rswag/rswag/tree/master)|2.0|3.18.2| +|[2.2.0](https://github.com/rswag/rswag/tree/2.2.0)|2.0|3.18.2| +|[1.6.0](https://github.com/rswag/rswag/tree/1.6.0)|2.0|2.2.5| ## Getting Started ## @@ -26,14 +27,15 @@ Once you have an API that can describe itself in Swagger, you've opened the trea gem 'rswag' ``` - or if you like to avoid loading rspec in other bundler groups. + or if you like to avoid loading rspec in other bundler groups load the rswag-specs component separately. + Note: Adding it to the :development group is not strictly necessary, but without it, generators and rake tasks must be preceded by RAILS_ENV=test. ```ruby # Gemfile gem 'rswag-api' gem 'rswag-ui' - group :test do + group :development, :test do gem 'rspec-rails' gem 'rswag-specs' end @@ -48,7 +50,8 @@ Once you have an API that can describe itself in Swagger, you've opened the trea Or run the install generators for each package separately if you installed Rswag as separate gems, as indicated above: ```ruby - rails g rswag:api:install rswag:ui:install + rails g rswag:api:install + rails g rswag:ui:install RAILS_ENV=test rails g rswag:specs:install ``` @@ -120,12 +123,17 @@ Once you have an API that can describe itself in Swagger, you've opened the trea end ``` + There is also a generator which can help get you started `rails generate rspec:swagger API::MyController` + + 4. Generate the Swagger JSON file(s) ```ruby rake rswag:specs:swaggerize ``` + This common command is also aliased as `rake rswag`. + 5. Spin up your app and check out the awesome, auto-generated docs at _/api-docs_! ## The rspec DSL ## @@ -185,7 +193,7 @@ describe 'Blogs API' do end end ``` -*Note:* the OAI v3 may be released soon(ish?) and include a nullable property. This may have an effect on the need/use of custom extension to the draft. Do not use this property if you don't understand the implications. +*Note:* OAI v3 has a nullable property. Rswag will work to support this soon. This may have an effect on the need/use of custom extension to the draft. Do not use this property if you don't understand the implications. ### Global Metadata ### @@ -202,16 +210,18 @@ RSpec.configure do |config| swagger: '2.0', info: { title: 'API V1', - version: 'v1' + version: 'v1', + description: 'This is the first version of my API' }, basePath: '/api/v1' }, - 'v2/swagger.json' => { - swagger: '2.0', + 'v2/swagger.yaml' => { + openapi: '3.0.0', info: { title: 'API V2', - version: 'v2' + version: 'v2', + description: 'This is the second version of my API' }, basePath: '/api/v2' } @@ -219,11 +229,12 @@ RSpec.configure do |config| end ``` -__NOTE__: By default, the paths, operations and responses defined in your spec files will be associated with the first Swagger document in _swagger_helper.rb_. If you're using multiple documents, you'll need to tag the individual specs with their target document name: +#### Supporting multiple versions of API #### +By default, the paths, operations and responses defined in your spec files will be associated with the first Swagger document in _swagger_helper.rb_. If your API has multiple versions, you should be using separate documents to describe each of them. In order to assign a file with a given version of API, you'll need to add the ```swagger_doc``` tag to each spec specifying its target document name: ```ruby # spec/integration/v2/blogs_spec.rb -describe 'Blogs API', swagger_doc: 'v2/swagger.json' do +describe 'Blogs API', swagger_doc: 'v2/swagger.yaml' do path '/blogs' do ... @@ -233,6 +244,25 @@ describe 'Blogs API', swagger_doc: 'v2/swagger.json' do end ``` +#### Formatting the description literals: #### +Swagger supports the Markdown syntax to format strings. This can be especially handy if you were to provide a long description of a given API version or endpoint. Use [this guide](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) for reference. + +__NOTE:__ There is one difference between the official Markdown syntax and Swagger interpretation, namely tables. To create a table like this: + +| Column1 | Collumn2 | +| ------- | -------- | +| cell1 | cell2 | + +you should use the folowing syntax, making sure there are no whitespaces at the start of any of the lines: + +``` + +| Column1 | Collumn2 | +| ------- | -------- | +| cell1 | cell2 | + +``` + ### Specifying/Testing API Security ### Swagger allows for the specification of different security schemes and their applicability to operations in an API. To leverage this in rswag, you define the schemes globally in _swagger_helper.rb_ and then use the "security" attribute at the operation level to specify which schemes, if any, are applicable to that operation. Swagger supports :basic, :apiKey and :oauth2 scheme types. See [the spec](http://swagger.io/specification/#security-definitions-object-109) for more info. @@ -309,6 +339,15 @@ end __NOTE__: If you do change this, you'll also need to update the rswag-api.rb initializer (assuming you're using rswag-api). More on this later. +### Input Location for Rspec Tests ### + +By default, rswag will search for integration tests in _spec/requests_, _spec/api_ and _spec/integration_. If you want to use tests from other locations, provide the PATTERN argument to rake: + +```ruby +# search for tests in spec/swagger +rake rswag:specs:swaggerize PATTERN="spec/swagger/**/*_spec.rb" +``` + ### Referenced Parameters and Schema Definitions ### Swagger allows you to describe JSON structures inline with your operation descriptions OR as referenced globals. For example, you might have a standard response structure for all failed operations. Rather than repeating the schema in every operation spec, you can define it globally and provide a reference to it in each spec: @@ -415,12 +454,41 @@ end ``` You need to disable --dry-run option for Rspec > 3 -Add to application.rb: +Add to config/environments/test.rb: ```ruby RSpec.configure do |config| config.swagger_dry_run = false end ``` + +### Running tests without documenting ### + +If you want to use Rswag for testing without adding it to you swagger docs, you can provide the document tag: +```ruby +describe 'Blogs API' do + path '/blogs/{blog_id}' do + get 'Retrieves a blog' do + # documentation is now disabled for this response only + response 200, 'blog found', document: false do + ... +``` + +You can also reenable documentation for specific responses only: +```ruby +# documentation is now disabled +describe 'Blogs API', document: false do + path '/blogs/{blog_id}' do + get 'Retrieves a blog' do + # documentation is reenabled for this response only + response 200, 'blog found', document: true do + ... + end + + response 401, 'special case' do + ... + end +``` + ### Route Prefix for Swagger JSON Endpoints ### The functionality to expose Swagger files, such as those generated by rswag-specs, as JSON endpoints is implemented as a Rails Engine. As with any Engine, you can change it's mount prefix in _routes.rb_: @@ -464,7 +532,7 @@ Rswag::Api.configure do |c| end ``` -Note how the filter is passed the rack env for the current request. This provides a lot of flexibilty. For example, you can assign the "host" property (as shown) or you could inspect session information or an Authoriation header and remove operations based on user permissions. +Note how the filter is passed the rack env for the current request. This provides a lot of flexibilty. For example, you can assign the "host" property (as shown) or you could inspect session information or an Authorization header and remove operations based on user permissions. ### Custom Headers for Swagger Files ### @@ -490,6 +558,17 @@ Rswag::Ui.configure do |c| end ``` +### Enable Simple Basic Auth for swagger-ui + +You can also update the _rswag-ui.rb_ initializer, installed with rswag-ui to specify a username and password should you want to keep your documentation private. + +```ruby +Rswag::Ui.configure do |c| + c.basic_auth_enabled = true + c.basic_auth_credentials 'username', 'password' +end +``` + ### Route Prefix for the swagger-ui ### Similar to rswag-api, you can customize the swagger-ui path by changing it's mount prefix in _routes.rb_: diff --git a/rswag-api/lib/rswag/api/middleware.rb b/rswag-api/lib/rswag/api/middleware.rb index 658988d..77a3b01 100644 --- a/rswag-api/lib/rswag/api/middleware.rb +++ b/rswag-api/lib/rswag/api/middleware.rb @@ -1,4 +1,6 @@ require 'json' +require 'yaml' +require 'rack/mime' module Rswag module Api @@ -14,14 +16,16 @@ module Rswag filename = "#{@config.resolve_swagger_root(env)}/#{path}" if env['REQUEST_METHOD'] == 'GET' && File.file?(filename) - swagger = load_json(filename) + swagger = parse_file(filename) @config.swagger_filter.call(swagger, env) unless @config.swagger_filter.nil? - headers = { 'Content-Type' => 'application/json' }.merge(@config.swagger_headers || {}) + mime = Rack::Mime.mime_type(::File.extname(path), 'text/plain') + headers = { 'Content-Type' => mime }.merge(@config.swagger_headers || {}) + body = unload_swagger(filename, swagger) return [ '200', headers, - [ JSON.dump(swagger) ] + [ body ] ] end @@ -30,9 +34,29 @@ module Rswag private + def parse_file(filename) + if /\.ya?ml$/ === filename + load_yaml(filename) + else + load_json(filename) + end + end + + def load_yaml(filename) + YAML.safe_load(File.read(filename)) + end + def load_json(filename) JSON.parse(File.read(filename)) end + + def unload_swagger(filename, swagger) + if /\.ya?ml$/ === filename + YAML.dump(swagger) + else + JSON.dump(swagger) + end + end end end end diff --git a/rswag-api/rswag-api.gemspec b/rswag-api/rswag-api.gemspec index 299172d..97e7a6a 100644 --- a/rswag-api/rswag-api.gemspec +++ b/rswag-api/rswag-api.gemspec @@ -13,5 +13,5 @@ Gem::Specification.new do |s| s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile"] - s.add_dependency 'railties', '>= 3.1', '< 6.0' + s.add_dependency 'railties', '>= 3.1', '< 7.0' end diff --git a/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.yml b/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.yml new file mode 100644 index 0000000..0757e2a --- /dev/null +++ b/rswag-api/spec/rswag/api/fixtures/swagger/v1/swagger.yml @@ -0,0 +1,5 @@ +swagger: '2.0' +info: + title: API V1 + version: v1 +paths: {} diff --git a/rswag-api/spec/rswag/api/middleware_spec.rb b/rswag-api/spec/rswag/api/middleware_spec.rb index 6a24e2c..049b3fd 100644 --- a/rswag-api/spec/rswag/api/middleware_spec.rb +++ b/rswag-api/spec/rswag/api/middleware_spec.rb @@ -112,6 +112,21 @@ module Rswag expect(response[2].join).to include('"host":"tempuri.org"') end end + + context 'when a path maps to a yaml swagger file' do + let(:env) { env_defaults.merge('PATH_INFO' => 'v1/swagger.yml') } + + it 'returns a 200 status' do + expect(response.length).to eql(3) + expect(response.first).to eql('200') + end + + it 'returns contents of the swagger file' do + expect(response.length).to eql(3) + expect(response[1]).to include( 'Content-Type' => 'text/yaml') + expect(response[2].join).to include('title: API V1') + end + end end end end diff --git a/rswag-specs/lib/generators/rspec/USAGE b/rswag-specs/lib/generators/rspec/USAGE new file mode 100644 index 0000000..bc354c4 --- /dev/null +++ b/rswag-specs/lib/generators/rspec/USAGE @@ -0,0 +1,9 @@ +Description: + This creates an RSpec request spec to define Swagger documentation for a + controller. It will create a test for each of the controller's methods. + +Example: + rails generate rspec:swagger V3::AccountsController + + This will create: + spec/requests/v3/accounts_spec.rb diff --git a/rswag-specs/lib/generators/rspec/swagger_generator.rb b/rswag-specs/lib/generators/rspec/swagger_generator.rb new file mode 100644 index 0000000..ddb862c --- /dev/null +++ b/rswag-specs/lib/generators/rspec/swagger_generator.rb @@ -0,0 +1,22 @@ +require 'rswag/route_parser' +require 'rails/generators' + +module Rspec + class SwaggerGenerator < ::Rails::Generators::NamedBase + source_root File.expand_path('../templates', __FILE__) + + def setup + @routes = Rswag::RouteParser.new(controller_path).routes + end + + def create_spec_file + template 'spec.rb', File.join('spec', 'requests', "#{controller_path}_spec.rb") + end + + private + + def controller_path + file_path.chomp('_controller') + end + end +end diff --git a/rswag-specs/lib/generators/rspec/templates/spec.rb b/rswag-specs/lib/generators/rspec/templates/spec.rb new file mode 100644 index 0000000..346e348 --- /dev/null +++ b/rswag-specs/lib/generators/rspec/templates/spec.rb @@ -0,0 +1,30 @@ +require 'swagger_helper' + +RSpec.describe '<%= controller_path %>', type: :request do +<% @routes.each do | template, path_item | %> + path '<%= template %>' do +<% unless path_item[:params].empty? -%> + # You'll want to customize the parameter types... +<% path_item[:params].each do |param| -%> + parameter name: '<%= param %>', in: :path, type: :string, description: '<%= param %>' +<% end -%> +<% end -%> +<% path_item[:actions].each do | action, details | %> + <%= action %>('<%= details[:summary] %>') do + response(200, 'successful') do +<% unless path_item[:params].empty? -%> +<% path_item[:params].each do |param| -%> + let(:<%= param %>) { '123' } +<% end -%> +<% end -%> + + after do |example| + example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) } + end + run_test! + end + end +<% end -%> + end +<% end -%> +end diff --git a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb index 3d27729..327b2c8 100644 --- a/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb +++ b/rswag-specs/lib/generators/rswag/specs/install/templates/swagger_helper.rb @@ -4,7 +4,7 @@ RSpec.configure do |config| # Specify a root folder where Swagger JSON files are generated # NOTE: If you're using the rswag-api to serve API descriptions, you'll need # to ensure that it's configured to serve Swagger from the same folder - config.swagger_root = Rails.root.to_s + '/swagger' + config.swagger_root = Rails.root.join('swagger').to_s # Define one or more Swagger documents and provide global metadata for each one # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will @@ -13,8 +13,8 @@ RSpec.configure do |config| # document below. You can override this behavior by adding a swagger_doc tag to the # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' config.swagger_docs = { - 'v1/swagger.json' => { - swagger: '2.0', + 'v1/swagger.yaml' => { + openapi: '3.0.1', info: { title: 'API V1', version: 'v1' @@ -22,4 +22,10 @@ RSpec.configure do |config| paths: {} } } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The swagger_docs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.swagger_format = :yaml end diff --git a/rswag-specs/lib/rswag/route_parser.rb b/rswag-specs/lib/rswag/route_parser.rb new file mode 100644 index 0000000..523b36b --- /dev/null +++ b/rswag-specs/lib/rswag/route_parser.rb @@ -0,0 +1,58 @@ +module Rswag + class RouteParser + attr_reader :controller + + def initialize(controller) + @controller = controller + end + + def routes + ::Rails.application.routes.routes.select do |route| + route.defaults[:controller] == controller + end.reduce({}) do |tree, route| + path = path_from(route) + verb = verb_from(route) + tree[path] ||= { params: params_from(route), actions: {} } + tree[path][:actions][verb] = { summary: summary_from(route) } + tree + end + end + + private + + def path_from(route) + route.path.spec.to_s + .chomp('(.:format)') # Ignore any format suffix + .gsub(/:([^\/.?]+)/, '{\1}') # Convert :id to {id} + end + + def verb_from(route) + verb = route.verb + if verb.kind_of? String + verb.downcase + else + verb.source.gsub(/[$^]/, '').downcase + end + end + + def summary_from(route) + verb = route.requirements[:action] + noun = route.requirements[:controller].split('/').last.singularize + + # Apply a few customizations to make things more readable + case verb + when 'index' + verb = 'list' + noun = noun.pluralize + when 'destroy' + verb = 'delete' + end + + "#{verb} #{noun}" + end + + def params_from(route) + route.segments - ['format'] + end + end +end diff --git a/rswag-specs/lib/rswag/specs.rb b/rswag-specs/lib/rswag/specs.rb index de29ce9..a3f0c16 100644 --- a/rswag-specs/lib/rswag/specs.rb +++ b/rswag-specs/lib/rswag/specs.rb @@ -12,6 +12,7 @@ module Rswag c.add_setting :swagger_root c.add_setting :swagger_docs c.add_setting :swagger_dry_run + c.add_setting :swagger_format c.extend ExampleGroupHelpers, type: :request c.include ExampleHelpers, type: :request end diff --git a/rswag-specs/lib/rswag/specs/configuration.rb b/rswag-specs/lib/rswag/specs/configuration.rb index 4adf33c..4c6ee68 100644 --- a/rswag-specs/lib/rswag/specs/configuration.rb +++ b/rswag-specs/lib/rswag/specs/configuration.rb @@ -31,6 +31,14 @@ module Rswag end end + def swagger_format + @swagger_format ||= begin + @rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty? + raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format) + @rspec_config.swagger_format + end + end + def get_swagger_doc(name) return swagger_docs.values.first if name.nil? raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name] diff --git a/rswag-specs/lib/rswag/specs/example_group_helpers.rb b/rswag-specs/lib/rswag/specs/example_group_helpers.rb index c293c63..4939abb 100644 --- a/rswag-specs/lib/rswag/specs/example_group_helpers.rb +++ b/rswag-specs/lib/rswag/specs/example_group_helpers.rb @@ -7,7 +7,7 @@ module Rswag describe(template, metadata, &block) end - [ :get, :post, :patch, :put, :delete, :head ].each do |verb| + [ :get, :post, :patch, :put, :delete, :head, :options, :trace ].each do |verb| define_method(verb) do |summary, &block| api_metadata = { operation: { verb: verb, summary: summary } } describe(verb, api_metadata, &block) diff --git a/rswag-specs/lib/rswag/specs/extended_schema.rb b/rswag-specs/lib/rswag/specs/extended_schema.rb index 62eb4ee..29888f8 100644 --- a/rswag-specs/lib/rswag/specs/extended_schema.rb +++ b/rswag-specs/lib/rswag/specs/extended_schema.rb @@ -3,7 +3,7 @@ require 'json-schema' module Rswag module Specs class ExtendedSchema < JSON::Schema::Draft4 - + def initialize super @attributes['type'] = ExtendedTypeAttribute @@ -13,7 +13,7 @@ module Rswag end class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute - + def self.validate(current_schema, data, fragments, processor, validator, options={}) return if data.nil? && current_schema.schema['x-nullable'] == true super diff --git a/rswag-specs/lib/rswag/specs/railtie.rb b/rswag-specs/lib/rswag/specs/railtie.rb index 8deec2b..617403e 100644 --- a/rswag-specs/lib/rswag/specs/railtie.rb +++ b/rswag-specs/lib/rswag/specs/railtie.rb @@ -5,6 +5,10 @@ module Rswag rake_tasks do load File.expand_path('../../../tasks/rswag-specs_tasks.rake', __FILE__) end + + generators do + require 'generators/rspec/swagger_generator.rb' + end end end end diff --git a/rswag-specs/lib/rswag/specs/request_factory.rb b/rswag-specs/lib/rswag/specs/request_factory.rb index 7106015..14b1edc 100644 --- a/rswag-specs/lib/rswag/specs/request_factory.rb +++ b/rswag-specs/lib/rswag/specs/request_factory.rb @@ -54,7 +54,7 @@ module Rswag definitions[key] end - def add_verb(request, metadata) + def add_verb(request, metadata) request[:verb] = metadata[:operation][:verb] end @@ -104,7 +104,7 @@ module Rswag end # Content-Type header - consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] + consumes = metadata[:operation][:consumes] || swagger_doc[:consumes] if consumes content_type = example.respond_to?(:'Content-Type') ? example.send(:'Content-Type') : consumes.first tuples << [ 'Content-Type', content_type ] diff --git a/rswag-specs/lib/rswag/specs/response_validator.rb b/rswag-specs/lib/rswag/specs/response_validator.rb index 5b366ce..c3e363f 100644 --- a/rswag-specs/lib/rswag/specs/response_validator.rb +++ b/rswag-specs/lib/rswag/specs/response_validator.rb @@ -14,17 +14,19 @@ module Rswag def validate!(metadata, response) swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) - validate_code!(metadata, response.code) + validate_code!(metadata, response) validate_headers!(metadata, response.headers) validate_body!(metadata, swagger_doc, response.body) end private - def validate_code!(metadata, code) + def validate_code!(metadata, response) expected = metadata[:response][:code].to_s - if code != expected - raise UnexpectedResponse, "Expected response code '#{code}' to match '#{expected}'" + if response.code != expected + raise UnexpectedResponse, + "Expected response code '#{response.code}' to match '#{expected}'\n" \ + "Response body: #{response.body}" end end diff --git a/rswag-specs/lib/rswag/specs/swagger_formatter.rb b/rswag-specs/lib/rswag/specs/swagger_formatter.rb index 794a9d9..568f7e2 100644 --- a/rswag-specs/lib/rswag/specs/swagger_formatter.rb +++ b/rswag-specs/lib/rswag/specs/swagger_formatter.rb @@ -1,9 +1,10 @@ require 'active_support/core_ext/hash/deep_merge' +require 'rspec/core/formatters/base_text_formatter' require 'swagger_helper' module Rswag module Specs - class SwaggerFormatter + class SwaggerFormatter < ::RSpec::Core::Formatters::BaseTextFormatter # NOTE: rspec 2.x support if RSPEC_VERSION > 2 @@ -25,7 +26,11 @@ module Rswag metadata = notification.metadata end + # !metadata[:document] won't work, since nil means we should generate + # docs. + return if metadata[:document] == false return unless metadata.has_key?(:response) + swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc]) swagger_doc.deep_merge!(metadata_to_swagger(metadata)) end @@ -37,7 +42,7 @@ module Rswag FileUtils.mkdir_p dirname unless File.exists?(dirname) File.open(file_path, 'w') do |file| - file.write(JSON.pretty_generate(doc)) + file.write(pretty_generate(doc)) end @output.puts "Swagger doc generated at #{file_path}" @@ -46,6 +51,20 @@ module Rswag private + def pretty_generate(doc) + if @config.swagger_format == :yaml + clean_doc = yaml_prepare(doc) + YAML.dump(clean_doc) + else # config errors are thrown in 'def swagger_format', no throw needed here + JSON.pretty_generate(doc) + end + end + + def yaml_prepare(doc) + json_doc = JSON.pretty_generate(doc) + clean_doc = JSON.parse(json_doc) + end + def metadata_to_swagger(metadata) response_code = metadata[:response][:code] response = metadata[:response].reject { |k,v| k == :code } diff --git a/rswag-specs/lib/tasks/rswag-specs_tasks.rake b/rswag-specs/lib/tasks/rswag-specs_tasks.rake index adc128c..54412a2 100644 --- a/rswag-specs/lib/tasks/rswag-specs_tasks.rake +++ b/rswag-specs/lib/tasks/rswag-specs_tasks.rake @@ -5,7 +5,10 @@ namespace :rswag do desc 'Generate Swagger JSON files from integration specs' RSpec::Core::RakeTask.new('swaggerize') do |t| - t.pattern = 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb' + t.pattern = ENV.fetch( + 'PATTERN', + 'spec/requests/**/*_spec.rb, spec/api/**/*_spec.rb, spec/integration/**/*_spec.rb' + ) # NOTE: rspec 2.x support if Rswag::Specs::RSPEC_VERSION > 2 && Rswag::Specs.config.swagger_dry_run @@ -16,3 +19,5 @@ namespace :rswag do end end end + +task :rswag => ['rswag:specs:swaggerize'] diff --git a/rswag-specs/rswag-specs.gemspec b/rswag-specs/rswag-specs.gemspec index 0e4686e..ff46439 100644 --- a/rswag-specs/rswag-specs.gemspec +++ b/rswag-specs/rswag-specs.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.files = Dir["{lib}/**/*"] + ["MIT-LICENSE", "Rakefile" ] - s.add_dependency 'activesupport', '>= 3.1', '< 6.0' - s.add_dependency 'railties', '>= 3.1', '< 6.0' + s.add_dependency 'activesupport', '>= 3.1', '< 7.0' + s.add_dependency 'railties', '>= 3.1', '< 7.0' s.add_dependency 'json-schema', '~> 2.2' end diff --git a/rswag-specs/spec/generators/rspec/fixtures/spec/.gitkeep b/rswag-specs/spec/generators/rspec/fixtures/spec/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb b/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb new file mode 100644 index 0000000..f11ea65 --- /dev/null +++ b/rswag-specs/spec/generators/rspec/swagger_generator_spec.rb @@ -0,0 +1,44 @@ +require 'generator_spec' +require 'generators/rspec/swagger_generator' +require 'tmpdir' + +module Rspec + describe SwaggerGenerator do + include GeneratorSpec::TestCase + destination Dir.mktmpdir + + before(:all) do + prepare_destination + fixtures_dir = File.expand_path('../fixtures', __FILE__) + FileUtils.cp_r("#{fixtures_dir}/spec", destination_root) + end + + after(:all) do + + end + + it 'installs the swagger_helper for rspec' do + allow_any_instance_of(Rswag::RouteParser).to receive(:routes).and_return(fake_routes) + run_generator ['Posts::CommentsController'] + + assert_file('spec/requests/posts/comments_spec.rb') do |content| + assert_match(/parameter name: 'post_id', in: :path, type: :string/, content) + assert_match(/patch\('update_comments comment'\)/, content) + end + end + + private + + def fake_routes + { + "/posts/{post_id}/comments/{id}" => { + :params => ["post_id", "id"], + :actions => { + "get" => { :summary=>"show comment" }, + "patch" => { :summary=>"update_comments comment" } + } + } + } + end + end +end diff --git a/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb b/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb index 39dc6fe..809e4f6 100644 --- a/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb +++ b/rswag-specs/spec/generators/rswag/specs/install_generator_spec.rb @@ -4,7 +4,7 @@ require 'generators/rswag/specs/install/install_generator' module Rswag module Specs - describe InstallGenerator do + RSpec.describe InstallGenerator do include GeneratorSpec::TestCase destination File.expand_path('../tmp', __FILE__) diff --git a/rswag-specs/spec/rswag/specs/configuration_spec.rb b/rswag-specs/spec/rswag/specs/configuration_spec.rb index b75d843..e3aacdc 100644 --- a/rswag-specs/spec/rswag/specs/configuration_spec.rb +++ b/rswag-specs/spec/rswag/specs/configuration_spec.rb @@ -3,10 +3,12 @@ require 'rswag/specs/configuration' module Rswag module Specs - describe Configuration do + RSpec.describe Configuration do subject { described_class.new(rspec_config) } - let(:rspec_config) { OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs) } + let(:rspec_config) do + OpenStruct.new(swagger_root: swagger_root, swagger_docs: swagger_docs, swagger_format: swagger_format) + end let(:swagger_root) { 'foobar' } let(:swagger_docs) do { @@ -14,6 +16,7 @@ module Rswag 'v2/swagger.json' => { info: { title: 'v2' } } } end + let(:swagger_format) { :yaml } describe '#swagger_root' do let(:response) { subject.swagger_root } @@ -46,6 +49,26 @@ module Rswag end end + describe '#swagger_format' do + let(:response) { subject.swagger_format } + + context 'provided in rspec config' do + it { expect(response).to be_an_instance_of(Symbol) } + end + + context 'unsupported format provided' do + let(:swagger_format) { :xml } + + it { expect { response }.to raise_error ConfigurationError } + end + + context 'not provided' do + let(:swagger_format) { nil } + + it { expect(response).to eq(:json) } + end + end + describe '#get_swagger_doc(tag=nil)' do let(:swagger_doc) { subject.get_swagger_doc(tag) } diff --git a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb index 619a8d7..ed667ca 100644 --- a/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_group_helpers_spec.rb @@ -3,7 +3,7 @@ require 'rswag/specs/example_group_helpers' module Rswag module Specs - describe ExampleGroupHelpers do + RSpec.describe ExampleGroupHelpers do subject { double('example_group') } before do @@ -24,7 +24,7 @@ module Rswag end end - describe '#get|post|patch|put|delete|head(verb, summary)' do + describe '#get|post|patch|put|delete|head|options|trace(verb, summary)' do before { subject.post('Creates a blog') } it "delegates to 'describe' with 'operation' metadata" do diff --git a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb index 3b22af4..283d4fd 100644 --- a/rswag-specs/spec/rswag/specs/example_helpers_spec.rb +++ b/rswag-specs/spec/rswag/specs/example_helpers_spec.rb @@ -3,7 +3,7 @@ require 'rswag/specs/example_helpers' module Rswag module Specs - describe ExampleHelpers do + RSpec.describe ExampleHelpers do subject { double('example') } before do @@ -12,7 +12,7 @@ module Rswag allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) stub_const('Rswag::Specs::RAILS_VERSION', 3) end - let(:config) { double('config') } + let(:config) { double('config') } let(:swagger_doc) do { securityDefinitions: { diff --git a/rswag-specs/spec/rswag/specs/request_factory_spec.rb b/rswag-specs/spec/rswag/specs/request_factory_spec.rb index f883952..0a70f19 100644 --- a/rswag-specs/spec/rswag/specs/request_factory_spec.rb +++ b/rswag-specs/spec/rswag/specs/request_factory_spec.rb @@ -3,13 +3,13 @@ require 'rswag/specs/request_factory' module Rswag module Specs - describe RequestFactory do + RSpec.describe RequestFactory do subject { RequestFactory.new(config) } before do allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) end - let(:config) { double('config') } + let(:config) { double('config') } let(:swagger_doc) { {} } let(:example) { double('example') } let(:metadata) do diff --git a/rswag-specs/spec/rswag/specs/response_validator_spec.rb b/rswag-specs/spec/rswag/specs/response_validator_spec.rb index 1d05427..9f52b5b 100644 --- a/rswag-specs/spec/rswag/specs/response_validator_spec.rb +++ b/rswag-specs/spec/rswag/specs/response_validator_spec.rb @@ -3,13 +3,13 @@ require 'rswag/specs/response_validator' module Rswag module Specs - describe ResponseValidator do + RSpec.describe ResponseValidator do subject { ResponseValidator.new(config) } before do allow(config).to receive(:get_swagger_doc).and_return(swagger_doc) end - let(:config) { double('config') } + let(:config) { double('config') } let(:swagger_doc) { {} } let(:example) { double('example') } let(:metadata) do diff --git a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb index f904fa5..b0a53b5 100644 --- a/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb +++ b/rswag-specs/spec/rswag/specs/swagger_formatter_spec.rb @@ -3,8 +3,8 @@ require 'ostruct' module Rswag module Specs - - describe SwaggerFormatter do + + RSpec.describe SwaggerFormatter do subject { described_class.new(output, config) } # Mock out some infrastructure @@ -26,41 +26,71 @@ module Rswag { path_item: { template: '/blogs' }, operation: { verb: :post, summary: 'Creates a blog' }, - response: { code: '201', description: 'blog created' } + response: { code: '201', description: 'blog created' }, + document: document } end - it 'converts to swagger and merges into the corresponding swagger doc' do - expect(swagger_doc).to match( - paths: { - '/blogs' => { - post: { - summary: 'Creates a blog', - responses: { - '201' => { description: 'blog created' } + context 'with the document tag set to false' do + let(:document) { false } + + it 'does not update the swagger doc' do + expect(swagger_doc).to be_empty + end + end + + context 'with the document tag set to anything but false' do + # anything works, including its absence when specifying responses. + let(:document) { nil } + + it 'converts to swagger and merges into the corresponding swagger doc' do + expect(swagger_doc).to match( + paths: { + '/blogs' => { + post: { + summary: 'Creates a blog', + responses: { + '201' => { description: 'blog created' } + } } } } - } - ) + ) + end end end describe '#stop' do - before do + before do FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) allow(config).to receive(:swagger_docs).and_return( 'v1/swagger.json' => { info: { version: 'v1' } }, 'v2/swagger.json' => { info: { version: 'v2' } } ) + allow(config).to receive(:swagger_format).and_return(swagger_format) subject.stop(notification) end let(:notification) { double('notification') } + context 'with default format' do + let(:swagger_format) { :json } - it 'writes the swagger_doc(s) to file' do - expect(File).to exist("#{swagger_root}/v1/swagger.json") - expect(File).to exist("#{swagger_root}/v2/swagger.json") + it 'writes the swagger_doc(s) to file' do + expect(File).to exist("#{swagger_root}/v1/swagger.json") + expect(File).to exist("#{swagger_root}/v2/swagger.json") + expect { JSON.parse(File.read("#{swagger_root}/v2/swagger.json")) }.not_to raise_error + end + end + + context 'with yaml format' do + let(:swagger_format) { :yaml } + + it 'writes the swagger_doc(s) as yaml' do + expect(File).to exist("#{swagger_root}/v1/swagger.json") + expect { JSON.parse(File.read("#{swagger_root}/v1/swagger.json")) }.to raise_error(JSON::ParserError) + # Psych::DisallowedClass would be raised if we do not pre-process ruby symbols + expect { YAML.safe_load(File.read("#{swagger_root}/v1/swagger.json")) }.not_to raise_error + end end after do diff --git a/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb b/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb index 084a512..0b9a4ab 100644 --- a/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb +++ b/rswag-ui/lib/generators/rswag/ui/install/templates/rswag-ui.rb @@ -2,9 +2,13 @@ Rswag::Ui.configure do |c| # List the Swagger endpoints that you want to be documented through the swagger-ui # The first parameter is the path (absolute or relative to the UI host) to the corresponding - # JSON endpoint and the second is a title that will be displayed in the document selector - # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON endpoints, + # endpoint and the second is a title that will be displayed in the document selector + # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON or YAML endpoints, # then the list below should correspond to the relative paths for those endpoints - c.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs' + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' + + # Add Basic Auth in case your API is private + # c.basic_auth_enabled = true + # c.basic_auth_credentials 'username', 'password' end diff --git a/rswag-ui/lib/rswag/ui/basic_auth.rb b/rswag-ui/lib/rswag/ui/basic_auth.rb new file mode 100644 index 0000000..ee42d36 --- /dev/null +++ b/rswag-ui/lib/rswag/ui/basic_auth.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'rack/auth/basic' + +module Rswag + module Ui + # Extend Rack HTTP Basic Authentication, as per RFC 2617. + # @api private + # + class BasicAuth < ::Rack::Auth::Basic + def call(env) + return @app.call(env) unless env_matching_path(env) + + super(env) + end + + private + + def env_matching_path(env) + path = base_path(env['PATH_INFO']) + Rswag::Ui.config.config_object[:urls].find do |endpoint| + base_path(endpoint[:url]) == path + end + end + + def base_path(url) + url.downcase.split('/')[1] + end + end + end +end diff --git a/rswag-ui/lib/rswag/ui/configuration.rb b/rswag-ui/lib/rswag/ui/configuration.rb index 5f33c2c..ad46a49 100644 --- a/rswag-ui/lib/rswag/ui/configuration.rb +++ b/rswag-ui/lib/rswag/ui/configuration.rb @@ -1,9 +1,11 @@ require 'ostruct' +require 'rack' module Rswag module Ui class Configuration attr_reader :template_locations + attr_accessor :basic_auth_enabled attr_accessor :config_object attr_accessor :oauth_config_object attr_reader :assets_root @@ -20,6 +22,7 @@ module Rswag @assets_root = File.expand_path('../../../../node_modules/swagger-ui-dist', __FILE__) @config_object = {} @oauth_config_object = {} + @basic_auth_enabled = false end def swagger_endpoint(url, name) @@ -27,9 +30,15 @@ module Rswag @config_object[:urls] << { url: url, name: name } end + def basic_auth_credentials(username, password) + @config_object[:basic_auth] = { username: username, password: password } + end + + # rubocop:disable Naming/AccessorMethodName def get_binding binding end + # rubocop:enable Naming/AccessorMethodName end end end diff --git a/rswag-ui/lib/rswag/ui/engine.rb b/rswag-ui/lib/rswag/ui/engine.rb index 78ee075..e90b4f2 100644 --- a/rswag-ui/lib/rswag/ui/engine.rb +++ b/rswag-ui/lib/rswag/ui/engine.rb @@ -1,4 +1,5 @@ require 'rswag/ui/middleware' +require 'rswag/ui/basic_auth' module Rswag module Ui @@ -7,6 +8,13 @@ module Rswag initializer 'rswag-ui.initialize' do |app| middleware.use Rswag::Ui::Middleware, Rswag::Ui.config + + if Rswag::Ui.config.basic_auth_enabled + c = Rswag::Ui.config + app.middleware.use Rswag::Ui::BasicAuth do |username, password| + c.config_object[:basic_auth].values == [username, password] + end + end end rake_tasks do diff --git a/rswag-ui/package-lock.json b/rswag-ui/package-lock.json index 664d3fa..8bc72e8 100644 --- a/rswag-ui/package-lock.json +++ b/rswag-ui/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "swagger-ui-dist": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.17.3.tgz", - "integrity": "sha1-37lkCMzEZ3UVX3NpGQxdSyAW/lw=" + "version": "3.23.11", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.23.11.tgz", + "integrity": "sha512-ipENHHH/sqpngTpHXUwg55eAOZ7b2UVayUwwuWPA6nQSPhjBVXX4zOPpNKUwQIFOl3oIwVvZF7mqoxH7pMgVzA==" } } } diff --git a/rswag-ui/package.json b/rswag-ui/package.json index 1fce627..fd299d1 100644 --- a/rswag-ui/package.json +++ b/rswag-ui/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "swagger-ui-dist": "3.17.3" + "swagger-ui-dist": "3.23.11" } } diff --git a/rswag-ui/rswag-ui.gemspec b/rswag-ui/rswag-ui.gemspec index 5b53548..d1ea0b1 100644 --- a/rswag-ui/rswag-ui.gemspec +++ b/rswag-ui/rswag-ui.gemspec @@ -13,6 +13,6 @@ Gem::Specification.new do |s| s.files = Dir.glob("{lib,node_modules}/**/*") + ["MIT-LICENSE", "Rakefile" ] - s.add_dependency 'actionpack', '>=3.1', '< 6.0' - s.add_dependency 'railties', '>= 3.1', '< 6.0' + s.add_dependency 'actionpack', '>=3.1', '< 7.0' + s.add_dependency 'railties', '>= 3.1', '< 7.0' end diff --git a/rswag-ui/spec/rswag/ui/configuration_spec.rb b/rswag-ui/spec/rswag/ui/configuration_spec.rb new file mode 100644 index 0000000..6e32590 --- /dev/null +++ b/rswag-ui/spec/rswag/ui/configuration_spec.rb @@ -0,0 +1,52 @@ +require 'rswag/ui/configuration' + +require_relative '../../spec_helper' + +RSpec.describe Rswag::Ui::Configuration do + describe '#swagger_endpoints' + + describe '#basic_auth_enabled' do + context 'when unspecified' do + it 'defaults to false' do + configuration = described_class.new + basic_auth_enabled = configuration.basic_auth_enabled + + expect(basic_auth_enabled).to be(false) + end + end + + context 'when specified' do + context 'when set to true' do + it 'returns true' do + configuration = described_class.new + configuration.basic_auth_enabled = true + basic_auth_enabled = configuration.basic_auth_enabled + + expect(basic_auth_enabled).to be(true) + end + end + + context 'when set to false' do + it 'returns false' do + configuration = described_class.new + configuration.basic_auth_enabled = false + basic_auth_enabled = configuration.basic_auth_enabled + + expect(basic_auth_enabled).to be(false) + end + end + end + end + + describe '#basic_auth_credentials' do + it 'sets the username and password' do + configuration = described_class.new + configuration.basic_auth_credentials 'foo', 'bar' + credentials = configuration.config_object[:basic_auth] + + expect(credentials).to eq(username: 'foo', password: 'bar') + end + end + + describe '#get_binding' +end diff --git a/rswag-ui/spec/spec_helper.rb b/rswag-ui/spec/spec_helper.rb index e69de29..251aa51 100644 --- a/rswag-ui/spec/spec_helper.rb +++ b/rswag-ui/spec/spec_helper.rb @@ -0,0 +1,100 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/test-app/app/assets/config/manifest.js b/test-app/app/assets/config/manifest.js new file mode 100644 index 0000000..5918193 --- /dev/null +++ b/test-app/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/test-app/app/assets/images/.keep b/test-app/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test-app/db/schema.rb b/test-app/db/schema.rb index 8accba1..e01f8f3 100644 --- a/test-app/db/schema.rb +++ b/test-app/db/schema.rb @@ -2,15 +2,15 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160218212104) do +ActiveRecord::Schema.define(version: 2016_02_18_212104) do create_table "blogs", force: :cascade do |t| t.string "title" diff --git a/test-app/spec/features/swagger_ui_spec.rb b/test-app/spec/features/swagger_ui_spec.rb index 7faa14e..484ad65 100644 --- a/test-app/spec/features/swagger_ui_spec.rb +++ b/test-app/spec/features/swagger_ui_spec.rb @@ -1,12 +1,12 @@ require 'rails_helper' -feature 'swagger-ui', js: true do +RSpec.feature 'swagger-ui', js: true do scenario 'browsing api-docs' do visit '/api-docs' - expect(page).to have_content('GET /blogs Searches blogs') - expect(page).to have_content('POST /blogs Creates a blog') - expect(page).to have_content('GET /blogs/{id} Retrieves a blog') + expect(page).to have_content('GET /blogs Searches blogs', normalize_ws: true) + expect(page).to have_content('POST /blogs Creates a blog', normalize_ws: true) + expect(page).to have_content('GET /blogs/{id} Retrieves a blog', normalize_ws: true) end end diff --git a/test-app/spec/integration/auth_tests_spec.rb b/test-app/spec/integration/auth_tests_spec.rb index 8e47d2e..573219e 100644 --- a/test-app/spec/integration/auth_tests_spec.rb +++ b/test-app/spec/integration/auth_tests_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do +RSpec.describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do path '/auth-tests/basic' do post 'Authenticates with basic auth' do diff --git a/test-app/spec/integration/blogs_spec.rb b/test-app/spec/integration/blogs_spec.rb index abca570..28ee892 100644 --- a/test-app/spec/integration/blogs_spec.rb +++ b/test-app/spec/integration/blogs_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do +RSpec.describe 'Blogs API', type: :request, swagger_doc: 'v1/swagger.json' do let(:api_key) { 'fake_key' } path '/blogs' do diff --git a/test-app/spec/rails_helper.rb b/test-app/spec/rails_helper.rb index 98c8fc3..3d9bd5f 100644 --- a/test-app/spec/rails_helper.rb +++ b/test-app/spec/rails_helper.rb @@ -51,9 +51,12 @@ RSpec.configure do |config| # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") - Capybara.javascript_driver = :webkit -end + Capybara.register_driver :firefox_headless do |app| + options = ::Selenium::WebDriver::Firefox::Options.new + options.args << '--headless' -Capybara::Webkit.configure do |config| - config.block_unknown_urls + Capybara::Selenium::Driver.new(app, browser: :firefox, options: options) + end + + Capybara.javascript_driver = :firefox_headless end diff --git a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb index 0a590ee..27fc195 100644 --- a/test-app/spec/rake/rswag_specs_swaggerize_spec.rb +++ b/test-app/spec/rake/rswag_specs_swaggerize_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' require 'rake' -describe 'rswag:specs:swaggerize' do +RSpec.describe 'rswag:specs:swaggerize' do let(:swagger_root) { Rails.root.to_s + '/swagger' } - before do + before do TestApp::Application.load_tasks FileUtils.rm_r(swagger_root) if File.exists?(swagger_root) end