diff --git a/Gemfile b/Gemfile index eb3e367894..2cc566eeaa 100644 --- a/Gemfile +++ b/Gemfile @@ -145,6 +145,7 @@ group :test, :development do gem 'letter_opener', '>= 1.4.1' gem 'rspec-rails', ">= 3.5.2" gem 'rspec-retry' + gem 'rswag', "2.2.0" gem 'selenium-webdriver' gem 'shoulda-matchers' gem 'timecop' @@ -165,7 +166,7 @@ group :development do gem "newrelic_rpm", "~> 3.0" gem "pry", "~> 0.12.0" # pry 0.13 is not compatible with pry-byebug 3.7 gem 'pry-byebug', '~> 3.7.0' # 3.8 requires ruby 2.4 - gem 'rubocop' + gem 'rubocop', '0.81' gem 'rubocop-rails' gem 'spring' gem 'spring-commands-rspec' diff --git a/Gemfile.lock b/Gemfile.lock index 02b4c3433b..77da7e9485 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -434,6 +434,8 @@ GEM jquery-ui-rails (4.2.1) railties (>= 3.2.16) json (1.8.6) + json-schema (2.8.1) + addressable (>= 2.4) json_spec (1.1.5) multi_json (~> 1.0) rspec (>= 2.0, < 4.0) @@ -590,6 +592,19 @@ GEM rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.9.2) + rswag (2.2.0) + rswag-api (= 2.2.0) + rswag-specs (= 2.2.0) + rswag-ui (= 2.2.0) + rswag-api (2.2.0) + railties (>= 3.1, < 6.1) + rswag-specs (2.2.0) + activesupport (>= 3.1, < 6.1) + json-schema (~> 2.2) + railties (>= 3.1, < 6.1) + rswag-ui (2.2.0) + actionpack (>= 3.1, < 6.1) + railties (>= 3.1, < 6.1) rubocop (0.81.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) @@ -780,7 +795,8 @@ DEPENDENCIES roo (~> 2.8.3) rspec-rails (>= 3.5.2) rspec-retry - rubocop + rswag (= 2.2.0) + rubocop (= 0.81) rubocop-rails sass sass-rails diff --git a/config/initializers/rswag-ui.rb b/config/initializers/rswag-ui.rb new file mode 100644 index 0000000000..0b9a4ab179 --- /dev/null +++ b/config/initializers/rswag-ui.rb @@ -0,0 +1,14 @@ +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 + # 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.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/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 0000000000..5f3ddc40f1 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,14 @@ +Rswag::Api.configure do |c| + + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.swagger_root = Rails.root.to_s + '/swagger' + + # Inject a lamda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + #c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/routes.rb b/config/routes.rb index 877ca407d3..9fcf21bef8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,8 @@ Openfoodnetwork::Application.routes.draw do + mount Rswag::Ui::Engine => '/api-docs' + + mount Rswag::Api::Engine => '/api-docs' + root :to => 'home#index' # Redirects from old URLs avoid server errors and helps search engines diff --git a/spec/requests/api/orders_spec.rb b/spec/requests/api/orders_spec.rb new file mode 100644 index 0000000000..fe85260382 --- /dev/null +++ b/spec/requests/api/orders_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'swagger_helper' + +describe 'api/orders', type: :request do + path '/api/orders' do + get('list orders') do + tags 'Orders' + # type should be replaced with swagger 3.01 valid schema: {type: string} when rswag #317 is resoved: + # https://github.com/rswag/rswag/pull/319 + parameter name: 'X-Spree-Token', in: :header, type: :string + parameter name: 'q[distributor_id_eq]', in: :query, type: :string, required: false, description: "Query orders for a specific distributor id." + parameter name: 'q[completed_at_gt]', in: :query, type: :string, required: false, description: "Query orders completed after a date." + parameter name: 'q[completed_at_lt]', in: :query, type: :string, required: false, description: "Query orders completed before a date." + parameter name: 'q[state_eq]', in: :query, type: :string, required: false, description: "Query orders by order state, eg 'cart', 'complete'." + parameter name: 'q[payment_state_eq]', in: :query, type: :string, required: false, description: "Query orders by order payment_state, eg 'balance_due', 'paid', 'failed'." + parameter name: 'q[email_cont]', in: :query, type: :string, required: false, description: "Query orders where the order email contains a string." + parameter name: 'q[order_cycle_id_eq]', in: :query, type: :string, required: false, description: "Query orders for a specific order_cycle id." + + response(200, 'get orders') do + # Adds model metadata for Swagger UI. Ideally we'd be able to just add: + # schema '$ref' => '#/components/schemas/Order_Concise' + # Which would also validate the response in the test, this is an open + # issue with rswag: https://github.com/rswag/rswag/issues/268 + metadata[:response][:content] = { "application/json": { + schema: {'$ref' => '#/components/schemas/Order_Concise'} + } + } + context "when there are four orders with different properties set" do + let(:order_dist_1) { create(:order_with_distributor, email: "specific_name@example.com") } + let(:order_dist_2) { create(:order_with_distributor) } + let(:order_dist_1_complete) { create(:order, distributor: order_dist_1.distributor, state: 'complete', completed_at: Time.zone.today - 7.days) } + let(:order_dist_1_credit_owed) { create(:order, distributor: order_dist_1.distributor, state: 'complete', payment_state: 'credit_owed', completed_at: Time.zone.today) } + + let(:user) { order_dist_1.distributor.owner } + let(:'X-Spree-Token') do + user.generate_spree_api_key! + user.spree_api_key + end + + context "and there are no query parameters" do + + run_test! do |response| + expect(response).to have_http_status(200) + + data = JSON.parse(response.body) + orders = data["orders"] + expect(orders.size).to eq 4 + end + end + + context "and queried by distributor id" do + let(:'q[distributor_id_eq]') { order_dist_2.distributor.id } + + run_test! do |response| + expect(response).to have_http_status(200) + + data = JSON.parse(response.body) + orders = data["orders"] + expect(orders.size).to eq 1 + expect(orders.first["id"]).to eq order_dist_2.id + end + end + + context "and queried within a date range" do + let(:'q[completed_at_gt]') { Time.zone.today - 7.days - 1.second } + let(:'q[completed_at_lt]') { Time.zone.today - 6.days } + + run_test! do |response| + expect(response).to have_http_status(200) + + data = JSON.parse(response.body) + orders = data["orders"] + expect(orders.size).to eq 1 + expect(orders.first["id"]).to eq order_dist_1_complete.id + end + end + + context "and queried by complete state" do + let(:'q[state_eq]') { "complete" } + run_test! do |response| + expect(response).to have_http_status(200) + + data = JSON.parse(response.body) + orders = data["orders"] + expect(orders.size).to eq 1 + expect(orders.first["id"]).to eq order_dist_1_complete.id + end + end + + context "and queried by credit_owed payment_state" do + let(:'q[payment_state_eq]') { "credit_owed" } + run_test! do |response| + expect(response).to have_http_status(200) + + data = JSON.parse(response.body) + orders = data["orders"] + expect(orders.size).to eq 1 + expect(orders.first["id"]).to eq order_dist_1_credit_owed.id + end + end + + context "and queried by buyer email contains a specific string" do + let(:'q[email_cont]') { order_dist_1.email.split("@").first } + run_test! do |response| + expect(response).to have_http_status(200) + + data = JSON.parse(response.body) + orders = data["orders"] + expect(orders.size).to eq 1 + expect(orders.first["id"]).to eq order_dist_1_credit_owed.id + end + end + + context "and queried by a specific order_cycle" do + let(:'q[order_cycle_id_eq]') { order_dist_2.order_cycle.id } + run_test! do |response| + expect(response).to have_http_status(200) + + data = JSON.parse(response.body) + orders = data["orders"] + expect(orders.size).to eq 1 + expect(orders.first["id"]).to eq order_dist_2.id + end + end + end + end + end + end +end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 0000000000..cca08655f6 --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.configure do |config| + config.swagger_root = Rails.root.join('swagger').to_s + config.swagger_docs = { + 'v1/swagger.yaml' => { + openapi: '3.0.1', + info: { + title: 'The Open Food Network', + description: 'Some endpoints are public and require no authorization; others require authorization. Talk to us to get your credentials set up. Check out our repo! https://github.com/openfoodfoundation/openfoodnetwork', + version: '0.1', + }, + components: { + securitySchemes: { + api_key: { + type: :apiKey, + name: 'X-Spree-Token', + in: :header + } + }, + schemas: { + Order_Concise: { + type: 'object', + properties: { + id: { type: 'integer' }, + number: { type: 'string' }, + full_name: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + completed_at: { type: 'string' }, + display_total: { type: 'string' }, + show_path: { type: 'string' }, + edit_path: { type: 'string' }, + state: { type: 'string' }, + payment_state: { type: 'string' }, + shipment_state: { type: 'string' }, + payments_path: { type: 'string' }, + shipments_path: { type: 'string' }, + ship_path: { type: 'string' }, + ready_to_ship: { type: 'string' }, + created_at: { type: 'string' }, + distributor_name: { type: 'string' }, + special_instructions: { type: 'string' }, + payment_capture_path: { type: 'string' }, + distributor: { + type: 'object', + properties: { + id: { type: 'integer' } + } + }, + order_cycle: { + type: 'object', + properties: { + id: { type: 'integer' } + } + } + } + } + } + }, + paths: {}, + servers: [ + { + url: 'https://staging.katuma.org/api' + } + ] + } + } + config.swagger_format = :yaml +end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml new file mode 100644 index 0000000000..739e210e86 --- /dev/null +++ b/swagger/v1/swagger.yaml @@ -0,0 +1,131 @@ +--- +openapi: 3.0.1 +info: + title: The Open Food Network + description: Some endpoints are public and require no authorization; others require + authorization. Talk to us to get your credentials set up. Check out our repo! + https://github.com/openfoodfoundation/openfoodnetwork + version: '0.1' +components: + securitySchemes: + api_key: + type: apiKey + name: X-Spree-Token + in: header + schemas: + Order_Concise: + type: object + properties: + id: + type: integer + number: + type: string + full_name: + type: string + email: + type: string + phone: + type: string + completed_at: + type: string + display_total: + type: string + show_path: + type: string + edit_path: + type: string + state: + type: string + payment_state: + type: string + shipment_state: + type: string + payments_path: + type: string + shipments_path: + type: string + ship_path: + type: string + ready_to_ship: + type: string + created_at: + type: string + distributor_name: + type: string + special_instructions: + type: string + payment_capture_path: + type: string + distributor: + type: object + properties: + id: + type: integer + order_cycle: + type: object + properties: + id: + type: integer +paths: + "/api/orders": + get: + summary: list orders + tags: + - Orders + parameters: + - name: X-Spree-Token + in: header + schema: + type: string + - name: q[distributor_id_eq] + in: query + schema: + type: string + style: deepObject + description: Query orders for a specific distributor id. + - name: q[completed_at_gt] + in: query + schema: + type: string + style: deepObject + description: Query orders completed after a date. + - name: q[completed_at_lt] + in: query + schema: + type: string + style: deepObject + description: Query orders completed before a date. + - name: q[state_eq] + in: query + schema: + type: string + style: deepObject + description: Query orders by order state, eg 'cart', 'complete'. + - name: q[payment_state_eq] + in: query + schema: + type: string + style: deepObject + description: Query orders by order payment_state, eg 'balance_due', 'paid', + 'failed'. + - name: q[email_cont] + in: query + schema: + type: string + style: deepObject + description: Query orders where the order email contains a string. + - name: q[order_cycle_id_eq] + in: query + schema: + type: string + style: deepObject + description: Query orders for a specific order_cycle id. + responses: + '200': + description: get orders + content: + application/json: + schema: + "$ref": "#/components/schemas/Order_Concise" +servers: +- url: https://staging.katuma.org/api