diff --git a/Gemfile b/Gemfile
index ffaf52edbf..907a5e32a9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -117,4 +117,6 @@ group :test do
# code coverage
gem 'simplecov', require: false
gem 'coveralls', require: false
+ # api
+ gem 'apivore', require: false
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 84b9a31540..2b45e7e96a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -58,6 +58,7 @@ GEM
railties (>= 4.0)
activejob (4.2.6)
activesupport (= 4.2.6)
+ globalid (>= 0.3.0)
activemodel (4.2.6)
activesupport (= 4.2.6)
builder (~> 3.1)
@@ -74,6 +75,13 @@ GEM
acts_as_tree (2.4.0)
activerecord (>= 3.0.0)
addressable (2.4.0)
+ apivore (1.6.0)
+ actionpack (~> 4)
+ hashie (~> 3.3)
+ json-schema (~> 2.5)
+ rspec (~> 3)
+ rspec-expectations (~> 3.1)
+ rspec-mocks (~> 3.1)
arel (6.0.3)
attribute_normalizer (1.2.0)
base32 (0.3.2)
@@ -166,6 +174,8 @@ GEM
gaffe (1.1.0)
rails (>= 4.0.0)
git-version-bump (0.15.1)
+ globalid (0.3.6)
+ activesupport (>= 4.1.0)
haml (4.0.7)
tilt
haml-rails (0.9.0)
@@ -177,6 +187,7 @@ GEM
has_scope (0.6.0)
actionpack (>= 3.2, < 5)
activesupport (>= 3.2, < 5)
+ hashie (3.4.4)
html2haml (2.0.0)
erubis (~> 2.7.0)
haml (~> 4.0.0)
@@ -202,6 +213,8 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.3)
+ json-schema (2.5.0)
+ addressable (~> 2.3)
kaminari (0.16.3)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
@@ -474,6 +487,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10.0.rc4)
acts_as_tree
acts_as_versioned!
+ apivore
attribute_normalizer
better_errors
binding_of_caller
diff --git a/app/controllers/api/v1/article_categories_controller.rb b/app/controllers/api/v1/article_categories_controller.rb
index 05f248e7b7..ce29cb0e85 100644
--- a/app/controllers/api/v1/article_categories_controller.rb
+++ b/app/controllers/api/v1/article_categories_controller.rb
@@ -5,6 +5,10 @@ def index
render json: search_scope
end
+ def show
+ render json: scope.find(params.require(:id))
+ end
+
private
def max_per_page
diff --git a/config/routes.rb b/config/routes.rb
index 514da59b6b..3682aa4123 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -204,7 +204,7 @@
resources :orders, only: [:index, :show]
resources :order_articles, only: [:index, :show]
resources :group_order_articles
- resources :article_categories, only: [:index]
+ resources :article_categories, only: [:index, :show]
end
end
diff --git a/doc/swagger.v1.yml b/doc/swagger.v1.yml
new file mode 100644
index 0000000000..a00ee8a9cd
--- /dev/null
+++ b/doc/swagger.v1.yml
@@ -0,0 +1,501 @@
+swagger: '2.0'
+info:
+ title: Foodsoft API v1
+ version: '1.0.0'
+
+# development url with default scope
+host: localhost:3002
+schemes:
+ - 'http'
+basePath: /f/api/v1
+
+produces:
+ - 'application/json'
+
+paths:
+ /user:
+ get:
+ summary: info about the currently logged-in user
+ tags:
+ - 1. User
+ responses:
+ 200:
+ description: user data
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/definitions/User'
+ 401:
+ description: not logged-in
+ schema:
+ $ref: '#/definitions/Error401'
+ security:
+ - foodsoft_auth: ['all']
+
+ /article_categories:
+ get:
+ summary: article categories
+ tags:
+ - 2. Category
+ parameters:
+ - $ref: '#/parameters/page'
+ - $ref: '#/parameters/per_page'
+ responses:
+ 200:
+ description: article categories
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: '#/definitions/ArticleCategory'
+ meta:
+ $ref: '#/definitions/Meta'
+ security:
+ - foodsoft_auth: ['all']
+ /article_categories/{id}:
+ parameters:
+ - $ref: '#/parameters/idQ'
+ get:
+ summary: find article category by id
+ tags:
+ - 2. Category
+ responses:
+ 200:
+ description: article category
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/definitions/ArticleCategory'
+ 404:
+ description: not found
+ schema:
+ $ref: '#/definitions/Error404'
+ security:
+ - foodsoft_auth: ['all']
+
+ /orders:
+ get:
+ summary: currently open orders
+ tags:
+ - 3. Order
+ parameters:
+ - $ref: '#/parameters/page'
+ - $ref: '#/parameters/per_page'
+ responses:
+ 200:
+ description: orders
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: '#/definitions/Order'
+ meta:
+ $ref: '#/definitions/Meta'
+ security:
+ - foodsoft_auth: ['all']
+ /orders/{id}:
+ parameters:
+ - $ref: '#/parameters/idQ'
+ get:
+ summary: find order by id
+ tags:
+ - 3. Order
+ responses:
+ 200:
+ description: order
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/definitions/Order'
+ 404:
+ description: not found
+ schema:
+ $ref: '#/definitions/Error404'
+ security:
+ - foodsoft_auth: ['all']
+
+ /order_articles:
+ get:
+ summary: articles in open orders
+ tags:
+ - 4. OrderArticle
+ parameters:
+ - $ref: '#/parameters/page'
+ - $ref: '#/parameters/per_page'
+ responses:
+ 200:
+ description: order articles
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: '#/definitions/OrderArticle'
+ meta:
+ $ref: '#/definitions/Meta'
+ security:
+ - foodsoft_auth: ['all']
+ /order_articles/{id}:
+ parameters:
+ - $ref: '#/parameters/idQ'
+ get:
+ summary: find order article by id
+ tags:
+ - 4. OrderArticle
+ responses:
+ 200:
+ description: order article
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/definitions/OrderArticle'
+ 404:
+ description: not found
+ schema:
+ $ref: '#/definitions/Error404'
+ security:
+ - foodsoft_auth: ['all']
+
+ /group_order_articles:
+ get:
+ summary: a member's group articles in open orders
+ tags:
+ - 5. GroupOrderArticle
+ parameters:
+ - $ref: '#/parameters/page'
+ - $ref: '#/parameters/per_page'
+ responses:
+ 200:
+ description: order articles
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: '#/definitions/GroupOrderArticle'
+ meta:
+ $ref: '#/definitions/Meta'
+ 403:
+ description: user has no ordergroup
+ schema:
+ $ref: '#/definitions/Error403'
+ security:
+ - foodsoft_auth: ['all']
+ /group_order_articles/{id}:
+ parameters:
+ - $ref: '#/parameters/idQ'
+ get:
+ summary: find a member's group order article by id
+ tags:
+ - 5. GroupOrderArticle
+ responses:
+ 200:
+ description: order article
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/definitions/GroupOrderArticle'
+ 403:
+ description: user has no ordergroup
+ schema:
+ $ref: '#/definitions/Error403'
+ 404:
+ description: not found
+ schema:
+ $ref: '#/definitions/Error404'
+ security:
+ - foodsoft_auth: ['all']
+
+ /financial_transactions:
+ get:
+ summary: financial transactions of the member's ordergroup
+ tags:
+ - 6. Financial Transaction
+ parameters:
+ - $ref: '#/parameters/page'
+ - $ref: '#/parameters/per_page'
+ responses:
+ 200:
+ description: financial transactions
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: '#/definitions/FinancialTransaction'
+ meta:
+ $ref: '#/definitions/Meta'
+ 403:
+ description: user has no ordergroup
+ schema:
+ $ref: '#/definitions/Error403'
+ security:
+ - foodsoft_auth: ['all']
+ /financial_transactions/{id}:
+ parameters:
+ - $ref: '#/parameters/idQ'
+ get:
+ summary: find financial transaction by id
+ tags:
+ - 6. Financial Transaction
+ responses:
+ 200:
+ description: financial transaction
+ schema:
+ type: object
+ properties:
+ data:
+ $ref: '#/definitions/FinancialTransaction'
+ 403:
+ description: user has no ordergroup
+ schema:
+ $ref: '#/definitions/Error403'
+ 404:
+ description: not found
+ schema:
+ $ref: '#/definitions/Error404'
+ security:
+ - foodsoft_auth: ['all']
+
+parameters:
+ # url parameters
+ idQ:
+ name: id
+ type: integer
+ in: path
+ minimum: 1
+ required: true
+
+ # query parameters
+ page:
+ name: page
+ type: integer
+ in: query
+ description: page number
+ minimum: 0
+ default: 0
+ per_page:
+ name: per_page
+ type: integer
+ in: query
+ description: items per page
+ minimum: 0
+ default: 20
+
+definitions:
+ # models
+ User:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ description: full name
+ email:
+ type: string
+ description: email address
+ required: ['id', 'name', 'email']
+ Article:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ description: article name
+ unit:
+ type: string
+ description: unit of single items
+ unit_quantity:
+ type: integer
+ description: number of units per box
+ minimum: 1
+ default: 1
+ note:
+ type: ['string', 'null']
+ manufacturer:
+ type: ['string', 'null']
+ origin:
+ type: ['string', 'null']
+ article_category_id:
+ type: integer
+ quantity_available:
+ type: integer
+ required: ['id', 'name', 'unit', 'unit_quantity', 'article_category_id']
+ ArticleCategory:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ required: ['id', 'name']
+ Order:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ description: name of the supplier
+ starts:
+ type: string
+ format: date-time
+ description: opening date
+ ends:
+ type: ['string', 'null']
+ format: date-time
+ description: closing date
+ boxfill:
+ type: ['string', 'null']
+ format: date-time
+ description: when boxfill period begins
+ is_open:
+ type: boolean
+ description: whether the order is currently open (right now always true)
+ is_boxfill:
+ type: ['boolean', 'null']
+ description: whether the order is currently in the boxfill period (null is not currently used, but may be in the future)
+ required: ['id', 'name', 'starts', 'is_open']
+ OrderArticle:
+ type: object
+ properties:
+ id:
+ type: integer
+ order_id:
+ type: integer
+ description: id of order this order article belongs to
+ price:
+ type: number
+ description: foodcoop price
+ quantity:
+ type: integer
+ description: number of articles ordered by all members as quantity
+ tolerance:
+ type: integer
+ description: number of articles ordered by all members as tolerance
+ units_to_order:
+ type: integer
+ description: number of full boxes ordered if the order would close now
+ article:
+ $ref: '#/definitions/Article'
+ required: ['id', 'order_id', 'price', 'quantity', 'tolerance', 'units_to_order']
+ GroupOrderArticle:
+ type: object
+ properties:
+ id:
+ type: integer
+ order_article_id:
+ type: integer
+ quantity:
+ type: integer
+ tolerance:
+ type: integer
+ result:
+ type: integer
+ total_price:
+ type: number
+ required: ['id', 'order_article_id', 'quantity', 'tolerance', 'result', 'total_price']
+ FinancialTransaction:
+ type: object
+ properties:
+ id:
+ type: integer
+ user_id:
+ type: ['integer', 'null']
+ description: id of user who entered the transaction (may be null for deleted users or 0 for a system user)
+ user_name:
+ type: ['string', 'null']
+ description: name of user who entered the transaction (may be null or empty string for deleted users or system users)
+ amount:
+ type: number
+ description: amount credited (negative for a debit transaction)
+ note:
+ type: string
+ description: note entered with the transaction
+ created_on:
+ type: string
+ format: date-time
+ description: when the transaction was entered
+ required: ['id', 'user_id', 'user_name', 'amount', 'note', 'created_on']
+
+ # collection meta object in root of a response
+ Meta:
+ type: object
+ properties:
+ page:
+ type: integer
+ description: page number of the returned collection
+ per_page:
+ type: integer
+ description: number of items per page
+ total_pages:
+ type: integer
+ description: total number of pages
+ total_count:
+ type: integer
+ description: total number of items in the collection
+ required: ['page', 'per_page', 'total_pages', 'total_count']
+
+ Error:
+ type: object
+ properties:
+ error:
+ type: string
+ description: error code
+ error_description:
+ type: string
+ description: human-readable error message (localized)
+ Error404:
+ type: object
+ properties:
+ error:
+ type: string
+ description: 'not_found'
+ error_description:
+ $ref: '#/definitions/Error/properties/error_description'
+ Error401:
+ type: object
+ properties:
+ error:
+ type: string
+ description: 'unauthorized'
+ error_description:
+ $ref: '#/definitions/Error/properties/error_description'
+ Error403:
+ type: object
+ properties:
+ error:
+ type: string
+ description: 'forbidden'
+ error_description:
+ $ref: '#/definitions/Error/properties/error_description'
+ Error422:
+ type: object
+ properties:
+ error:
+ type: string
+ description: 'not_acceptable'
+ error_description:
+ $ref: '#/definitions/Error/properties/error_description'
+
+securityDefinitions:
+ foodsoft_auth:
+ type: oauth2
+ flow: implicit
+ authorizationUrl: http://localhost:3002/f/oauth/authorize
+ scopes:
+ all: full access to user functions
diff --git a/spec/api/v1/swagger_spec.rb b/spec/api/v1/swagger_spec.rb
new file mode 100644
index 0000000000..593070d3ae
--- /dev/null
+++ b/spec/api/v1/swagger_spec.rb
@@ -0,0 +1,129 @@
+require 'spec_helper'
+require 'apivore'
+
+# we want to load a local file in YAML-format instead of a served JSON file
+class SwaggerCheckerFile < Apivore::SwaggerChecker
+ def fetch_swagger!
+ YAML.load(File.read(swagger_path))
+ end
+end
+
+describe 'API v1', type: :apivore, order: :defined do
+ include ApiHelper
+
+ subject { SwaggerCheckerFile.instance_for Rails.root.join('doc/swagger.v1.yml') }
+
+ context 'has valid paths' do
+
+ # users
+ context do
+ # create multiple users to make sure we're getting the authenticated user, not just any
+ let!(:other_user_1) { create :user }
+ let!(:user) { create :user }
+ let!(:other_user_2) { create :user }
+
+ it { is_expected.to validate(:get, '/user', 200, auth) }
+ it { is_expected.to validate(:get, '/user', 401) }
+
+ context 'with invalid access token' do
+ let(:access_token) { 'abc' }
+ it { is_expected.to validate(:get, '/user', 401, auth) }
+ end
+ end
+
+ # article_categories
+ context do
+ let!(:cat_1) { create :article_category }
+ let!(:cat_2) { create :article_category }
+ let!(:cat_3) { create :article_category }
+
+ it { is_expected.to validate(:get, '/article_categories', 200, auth) }
+ it { is_expected.to validate(:get, '/article_categories/{id}', 200, auth({'id' => cat_2.id})) }
+ it { is_expected.to validate(:get, '/article_categories/{id}', 404, auth({'id' => cat_3.id + 1})) }
+ end
+
+ # orders
+ context do
+ let!(:order_1) { create :order, article_count: 1 }
+ let!(:order_2) { create :order, article_count: 1 }
+ let!(:order_3) { create :stock_order, article_count: 1 }
+ let!(:order_4) { create :order, state: 'finished', article_count: 1 }
+ let!(:order_5) { create :order, state: 'closed', article_count: 1 }
+
+ it { is_expected.to validate(:get, '/orders', 200, auth) }
+ it { is_expected.to validate(:get, '/orders/{id}', 200, auth({'id' => order_2.id})) }
+ it { is_expected.to validate(:get, '/orders/{id}', 404, auth({'id' => order_4.id})) }
+ it { is_expected.to validate(:get, '/orders/{id}', 404, auth({'id' => order_5.id})) }
+ it { is_expected.to validate(:get, '/orders/{id}', 404, auth({'id' => Order.last.id + 1})) }
+ end
+
+ # order_articles
+ context do
+ let!(:order) { create :order, article_count: 3 + rand(10) }
+ let(:oa_1) { order.order_articles[0] }
+ let(:oa_2) { order.order_articles[1] }
+
+ it { is_expected.to validate(:get, '/order_articles', 200, auth) }
+ it { is_expected.to validate(:get, '/order_articles/{id}', 200, auth({'id' => oa_2.id})) }
+ it { is_expected.to validate(:get, '/order_articles/{id}', 404, auth({'id' => OrderArticle.last.id + 1})) }
+ end
+
+ # group_order_articles
+ context do
+ let!(:order) { create :order, article_count: 3 }
+ let(:oa_1) { order.order_articles[0] }
+ let(:oa_2) { order.order_articles[1] }
+
+ let(:other_go) { create :group_order, order: order }
+ let!(:other_goa_1) { create :group_order_article, group_order: other_go, order_article: oa_1 }
+
+ context 'without ordergroup' do
+ it { is_expected.to validate(:get, '/group_order_articles', 403, auth) }
+ it { is_expected.to validate(:get, '/group_order_articles/{id}', 403, auth({'id' => other_goa_1.id})) }
+ end
+
+ context 'in ordergroup' do
+ let(:user) { create :user, :ordergroup }
+ let(:go) { create :group_order, order: order, ordergroup: user.ordergroup }
+ let!(:goa_1) { create :group_order_article, group_order: go, order_article: oa_1 }
+ let!(:goa_2) { create :group_order_article, group_order: go, order_article: oa_2 }
+
+ it { is_expected.to validate(:get, '/group_order_articles', 200, auth) }
+ it { is_expected.to validate(:get, '/group_order_articles/{id}', 200, auth({'id' => goa_2.id})) }
+ it { is_expected.to validate(:get, '/group_order_articles/{id}', 404, auth({'id' => other_goa_1.id})) }
+ it { is_expected.to validate(:get, '/group_order_articles/{id}', 404, auth({'id' => GroupOrderArticle.last.id + 1})) }
+ end
+ end
+
+ # @todo finish
+ end
+
+ # financial_transactions
+ context do
+ let(:other_user) { create :user, :ordergroup }
+ let!(:other_ft_1) { create :financial_transaction, ordergroup: other_user.ordergroup }
+
+ context 'without ordergroup' do
+ it { is_expected.to validate(:get, '/financial_transactions', 403, auth) }
+ it { is_expected.to validate(:get, '/financial_transactions/{id}', 403, auth({'id' => other_ft_1.id})) }
+ end
+
+ context 'in ordergroup' do
+ let(:user) { create :user, :ordergroup }
+ let!(:ft_1) { create :financial_transaction, ordergroup: user.ordergroup }
+ let!(:ft_2) { create :financial_transaction, ordergroup: user.ordergroup }
+ let!(:ft_3) { create :financial_transaction, ordergroup: user.ordergroup }
+
+ it { is_expected.to validate(:get, '/financial_transactions', 200, auth) }
+ it { is_expected.to validate(:get, '/financial_transactions/{id}', 200, auth({'id' => ft_2.id})) }
+ it { is_expected.to validate(:get, '/financial_transactions/{id}', 404, auth({'id' => other_ft_1.id})) }
+ it { is_expected.to validate(:get, '/financial_transactions/{id}', 404, auth({'id' => FinancialTransaction.last.id + 1})) }
+ end
+ end
+
+ context 'and' do
+ it 'tests all documented routes' do
+ is_expected.to validate_all_paths
+ end
+ end
+end
diff --git a/spec/factories/article.rb b/spec/factories/article.rb
index d627110fe1..a8b74ae43e 100644
--- a/spec/factories/article.rb
+++ b/spec/factories/article.rb
@@ -3,22 +3,26 @@
FactoryGirl.define do
factory :_article do
+ sequence(:name) { |n| Faker::Lorem.words(rand(2..4)).join(' ') + " ##{n}" }
unit { Faker::Unit.unit }
price { rand(2600) / 100 }
tax { [6, 21].sample }
deposit { rand(10) < 8 ? 0 : [0.0, 0.80, 1.20, 12.00].sample }
unit_quantity { rand(5) < 3 ? 1 : rand(1..20) }
+ article_category
factory :article do
- sequence(:name) { |n| Faker::Lorem.words(rand(2..4)).join(' ') + " ##{n}" }
- supplier { create :supplier }
- article_category { create :article_category }
+ supplier
end
factory :shared_article, class: SharedArticle do
- sequence(:name) { |n| Faker::Lorem.words(rand(2..4)).join(' ') + " s##{n}" }
order_number { Faker::Lorem.characters(rand(1..12)) }
- supplier { create :shared_supplier }
+ supplier factory: :shared_supplier
+ end
+
+ factory :stock_article, class: StockArticle do
+ supplier_id 0
+ quantity { rand(20) + 1 }
end
end
diff --git a/spec/factories/doorkeeper.rb b/spec/factories/doorkeeper.rb
new file mode 100644
index 0000000000..d75b48c72c
--- /dev/null
+++ b/spec/factories/doorkeeper.rb
@@ -0,0 +1,15 @@
+require 'factory_girl'
+require 'doorkeeper'
+
+FactoryGirl.define do
+
+ factory :oauth2_application, class: Doorkeeper::Application do
+ name { Faker::App.name }
+ redirect_uri 'https://example.com:1234/app'
+ end
+
+ factory :oauth2_access_token, class: Doorkeeper::AccessToken do
+ application factory: :oauth2_application
+ end
+
+end
diff --git a/spec/factories/financial_transaction.rb b/spec/factories/financial_transaction.rb
new file mode 100644
index 0000000000..4b0cd5146e
--- /dev/null
+++ b/spec/factories/financial_transaction.rb
@@ -0,0 +1,10 @@
+require 'factory_girl'
+
+FactoryGirl.define do
+ factory :financial_transaction do
+ user
+ ordergroup
+ amount { rand(-99_999.00..99_999.00) }
+ note { Faker::Lorem.sentence }
+ end
+end
diff --git a/spec/factories/group_order.rb b/spec/factories/group_order.rb
index 1a665ca4b7..774c9cda45 100644
--- a/spec/factories/group_order.rb
+++ b/spec/factories/group_order.rb
@@ -4,7 +4,7 @@
# requires order
factory :group_order do
- ordergroup { create(:user, groups: [FactoryGirl.create(:ordergroup)]).ordergroup }
+ ordergroup { create(:user, :ordergroup).ordergroup }
end
end
diff --git a/spec/factories/order.rb b/spec/factories/order.rb
index 619ceb8b80..c67ef31ec4 100644
--- a/spec/factories/order.rb
+++ b/spec/factories/order.rb
@@ -11,10 +11,14 @@
article_count true
end
- # for an order from stock; need to add articles
+ # for an order from stock
factory :stock_order do
supplier_id 0
- # article_ids needs to be supplied
+ after :create do |order, evaluator|
+ article_count = evaluator.article_count
+ article_count = rand(1..99) if article_count == true
+ create_list :stock_article, article_count
+ end
end
# In the order's after_save callback order articles are created, so
diff --git a/spec/factories/user.rb b/spec/factories/user.rb
index f70c48c8aa..9b98c535bd 100644
--- a/spec/factories/user.rb
+++ b/spec/factories/user.rb
@@ -15,6 +15,12 @@
create :workgroup, role_admin: true, user_ids: [user.id]
end
end
+
+ trait :ordergroup do
+ after :create do |user, evaluator|
+ create :ordergroup, user_ids: [user.id]
+ end
+ end
end
factory :group do
diff --git a/spec/support/api_helper.rb b/spec/support/api_helper.rb
new file mode 100644
index 0000000000..2013d41076
--- /dev/null
+++ b/spec/support/api_helper.rb
@@ -0,0 +1,18 @@
+module ApiHelper
+ extend ActiveSupport::Concern
+
+ included do
+ let(:user) { create :user }
+ let(:access_token) { create(:oauth2_access_token, resource_owner_id: user.id).token }
+ let(:authorization) { "Bearer #{token}" }
+ end
+
+ # Add authentication to parameters for {Swagger::RspecHelpers#validate}
+ # @param params [Hash] Query parameters
+ # @return Query parameters with authentication header
+ # @see Swagger::RspecHelpers#validate
+ def auth(params = {})
+ {'_headers' => {'Authorization' => "Bearer #{access_token}" }}.deep_merge(params)
+ end
+
+end