Replace apivore with rswag for api tests (#969)

* Replace apivore api tests with rswag
* move to OpenAPI Spec 3.0.1
* a swagger UI is now reachable at http://localhost:3000/api-docs/index.html
*  swagger file is generated by running  `RAILS_ENV=test rails rswag`
    and it was moved from /docs/swagger.v1.yml to /swagger/v1/swagger.yml

---------

Co-authored-by: viehlieb <pf@pragma-shift.net>
This commit is contained in:
Philipp Rothmann 2023-05-12 11:11:48 +02:00 committed by GitHub
parent 8604e27fe9
commit c67e9b5be8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1478 additions and 1858 deletions

View file

@ -451,6 +451,15 @@ RSpec/DescribedClass:
- "spec/models/ordergroup_spec.rb"
- "spec/models/user_spec.rb"
# Offense count: 15
# This cop supports unsafe autocorrection (--autocorrect-all).
RSpec/EmptyExampleGroup:
Exclude:
# exclude for rswag tests:
- 'spec/requests/api/**/*_spec.rb'
# Offense count: 65
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
@ -581,6 +590,14 @@ RSpec/ScatteredSetup:
- "spec/integration/balancing_spec.rb"
- "spec/integration/login_spec.rb"
# Offense count: 4
# Configuration parameters: AllowedPatterns, IgnoredPatterns.
# SupportedStyles: snake_case, camelCase
RSpec/VariableName:
EnforcedStyle: snake_case
AllowedPatterns:
- ^Authorization$
# Offense count: 1
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:

View file

@ -55,6 +55,9 @@ gem 'gaffe'
gem 'ruby-filemagic'
gem 'mime-types'
gem 'midi-smtp-server'
gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114
gem 'rswag-api'
gem 'rswag-ui'
# we use the git version of acts_as_versioned, and need to include it in this Gemfile
gem 'acts_as_versioned', git: 'https://github.com/technoweenie/acts_as_versioned.git'
@ -116,6 +119,5 @@ group :test do
gem 'simplecov', require: false
gem 'simplecov-lcov', require: false
# api
gem 'apivore', require: false
gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114
gem 'rswag-specs'
end

View file

@ -109,13 +109,6 @@ GEM
activerecord (>= 3.0.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
apivore (1.6.2)
actionpack (>= 4, < 6)
hashie (~> 3.3)
json-schema (~> 2.5)
rspec (~> 3)
rspec-expectations (~> 3.1)
rspec-mocks (~> 3.1)
apparition (0.6.0)
capybara (~> 3.13, < 4)
websocket-driver (>= 0.6.5)
@ -430,6 +423,16 @@ GEM
rspec-rerun (1.1.0)
rspec (~> 3.0)
rspec-support (3.11.1)
rswag-api (2.7.0)
railties (>= 3.1, < 7.1)
rswag-specs (2.7.0)
activesupport (>= 3.1, < 7.1)
json-schema (>= 2.2, < 4.0)
railties (>= 3.1, < 7.1)
rspec-core (>= 2.14)
rswag-ui (2.7.0)
actionpack (>= 3.1, < 7.1)
railties (>= 3.1, < 7.1)
rubocop (1.36.0)
json (~> 2.3)
parallel (~> 1.10)
@ -557,7 +560,6 @@ DEPENDENCIES
active_model_serializers (~> 0.10.0)
acts_as_tree
acts_as_versioned!
apivore
apparition
attribute_normalizer
better_errors
@ -617,6 +619,9 @@ DEPENDENCIES
rspec-core
rspec-rails
rspec-rerun
rswag-api
rswag-specs
rswag-ui
rubocop
rubocop-rails
rubocop-rspec

View file

@ -0,0 +1,13 @@
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 lambda 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

View file

@ -0,0 +1,15 @@
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

View file

@ -1,4 +1,7 @@
# rubocop:disable Metrics/BlockLength
Rails.application.routes.draw do
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
get "order_comments/new"
get "comments/new"
@ -290,3 +293,4 @@ Rails.application.routes.draw do
resources :users, only: [:index]
end # End of /:foodcoop scope
end
# rubocop:enable Metrics/BlockLength

View file

@ -5,9 +5,11 @@ like listing open orders, updating the ordergroup's order, and listing financial
transactions. Not all Foodsoft functionality is available through the API, but
we're open for new additions.
The API is documented using [Open API 2.0](https://github.com/OAI/OpenAPI-Specification)
/ [Swagger](https://swagger.io/) in [swagger.v1.yml](swagger.v1.yml).
The API is documented using [Open API 3.0.1](https://github.com/OAI/OpenAPI-Specification)
/ [Swagger](https://swagger.io/) in [swagger.yaml](/swagger/v1/swagger.yaml).
This provides a machine-readable reference that is used to provide documentation.
It is generated by [rswag](https://github.com/rswag) wich also provides api-tests.
It can be generated running `RAILS_ENV=test rails rswag`.
**Note:** the current OAuth scopes may be subject to change, until the next release of Foodsoft.

File diff suppressed because it is too large Load diff

View file

@ -1,59 +0,0 @@
require 'spec_helper'
# Most routes are tested in the swagger_spec, this tests (non-ransack) parameters.
describe Api::V1::OrderArticlesController, type: :controller do
include ApiOAuth
let(:api_scopes) { ['orders:read'] }
let(:json_order_articles) { json_response['order_articles'] }
let(:json_order_article_ids) { json_order_articles.map { |joa| joa["id"] } }
describe "GET :index" do
context "with param q[ordered]" do
let(:order) { create(:order, article_count: 4) }
let(:order_articles) { order.order_articles }
before do
order_articles[0].update!(quantity: 0, tolerance: 0, units_to_order: 0)
order_articles[1].update!(quantity: 1, tolerance: 0, units_to_order: 0)
order_articles[2].update!(quantity: 0, tolerance: 1, units_to_order: 0)
order_articles[3].update!(quantity: 0, tolerance: 0, units_to_order: 1)
end
it "(unset)" do
get :index, params: { foodcoop: 'f' }
expect(json_order_articles.count).to eq 4
end
it "all" do
get :index, params: { foodcoop: 'f', q: { ordered: 'all' } }
expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id)
end
it "supplier" do
get :index, params: { foodcoop: 'f', q: { ordered: 'supplier' } }
expect(json_order_article_ids).to match_array [order_articles[3].id]
end
it "member" do
get :index, params: { foodcoop: 'f', q: { ordered: 'member' } }
expect(json_order_articles.count).to eq 0
end
context "when ordered by user" do
let(:user) { create(:user, :ordergroup) }
let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) }
before do
create(:group_order_article, group_order: go, order_article: order_articles[1], quantity: 1)
create(:group_order_article, group_order: go, order_article: order_articles[2], tolerance: 0)
end
it "member" do
get :index, params: { foodcoop: 'f', q: { ordered: 'member' } }
expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id)
end
end
end
end
end

View file

@ -1,284 +0,0 @@
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
context 'user' do
let(:api_scopes) { ['user:read'] }
# 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, api_auth) }
it { is_expected.to validate(:get, '/user', 401) }
it_handles_invalid_token_and_scope(:get, '/user')
end
context 'user/financial_overview' do
let(:api_scopes) { ['finance:user'] }
let!(:user) { create :user, :ordergroup }
it { is_expected.to validate(:get, '/user/financial_overview', 200, api_auth) }
it { is_expected.to validate(:get, '/user/financial_overview', 401) }
it_handles_invalid_token_and_scope(:get, '/user/financial_overview')
end
context 'user/financial_transactions' do
let(:api_scopes) { ['finance:user'] }
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, '/user/financial_transactions', 403, api_auth) }
it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 403, api_auth({ 'id' => other_ft_1.id })) }
end
context 'with 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 }
let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id, note: 'note' } } } }
it { is_expected.to validate(:get, '/user/financial_transactions', 200, api_auth) }
it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 200, api_auth({ 'id' => ft_2.id })) }
it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({ 'id' => other_ft_1.id })) }
it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({ 'id' => FinancialTransaction.last.id + 1 })) }
context 'without using self service' do
it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) }
end
context 'with using self service' do
before { FoodsoftConfig[:use_self_service] = true }
it { is_expected.to validate(:post, '/user/financial_transactions', 200, api_auth(create_params)) }
context 'with invalid financial transaction type' do
let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: -1, note: 'note' } } } }
it { is_expected.to validate(:post, '/user/financial_transactions', 404, api_auth(create_params)) }
end
context 'without note' do
let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id } } } }
it { is_expected.to validate(:post, '/user/financial_transactions', 422, api_auth(create_params)) }
end
context 'without enough balance' do
before { FoodsoftConfig[:minimum_balance] = 1000 }
it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) }
end
end
it_handles_invalid_token_and_scope(:get, '/user/financial_transactions')
it_handles_invalid_token_and_scope(:post, '/user/financial_transactions', -> { api_auth(create_params) })
it_handles_invalid_token_and_scope(:get, '/user/financial_transactions/{id}', -> { api_auth('id' => ft_2.id) })
end
end
context 'user/group_order_articles' do
let(:api_scopes) { ['group_orders:user'] }
let(:order) { create(:order, article_count: 2) }
let(:user_2) { create :user, :ordergroup }
let(:group_order_2) { create(:group_order, order: order, ordergroup: user_2.ordergroup) }
let!(:goa_2) { create :group_order_article, order_article: order.order_articles[0], group_order: group_order_2 }
before { group_order_2.update_price!; user_2.ordergroup.update_stats! }
context 'without ordergroup' do
it { is_expected.to validate(:get, '/user/group_order_articles', 403, api_auth) }
it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 403, api_auth({ 'id' => goa_2.id })) }
end
context 'with ordergroup' do
let(:user) { create :user, :ordergroup }
let(:update_params) { { 'id' => goa.id, '_data' => { group_order_article: { quantity: goa.quantity + 1, tolerance: 0 } } } }
let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[1].id, quantity: 1 } } } }
let(:group_order) { create(:group_order, order: order, ordergroup: user.ordergroup) }
let!(:goa) { create :group_order_article, order_article: order.order_articles[0], group_order: group_order }
before { group_order.update_price!; user.ordergroup.update_stats! }
it { is_expected.to validate(:get, '/user/group_order_articles', 200, api_auth) }
it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) }
it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => goa_2.id })) }
it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => GroupOrderArticle.last.id + 1 })) }
it { is_expected.to validate(:post, '/user/group_order_articles', 200, api_auth(create_params)) }
it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 200, api_auth(update_params)) }
it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) }
context 'with an existing group_order_article' do
let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[0].id, quantity: 1 } } } }
it { is_expected.to validate(:post, '/user/group_order_articles', 422, api_auth(create_params)) }
end
context 'with invalid parameter values' do
let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[0].id, quantity: -1 } } } }
let(:update_params) { { 'id' => goa.id, '_data' => { group_order_article: { quantity: -1, tolerance: 0 } } } }
it { is_expected.to validate(:post, '/user/group_order_articles', 422, api_auth(create_params)) }
it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 422, api_auth(update_params)) }
end
context 'with a closed order' do
let(:order) { create(:order, article_count: 2, state: :finished) }
it { is_expected.to validate(:post, '/user/group_order_articles', 404, api_auth(create_params)) }
it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 404, api_auth(update_params)) }
it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => goa.id })) }
end
context 'without enough balance' do
before { FoodsoftConfig[:minimum_balance] = 1000 }
it { is_expected.to validate(:post, '/user/group_order_articles', 403, api_auth(create_params)) }
it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 403, api_auth(update_params)) }
it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) }
end
context 'without enough apple points' do
before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) }
it { is_expected.to validate(:post, '/user/group_order_articles', 403, api_auth(create_params)) }
it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 403, api_auth(update_params)) }
it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) }
end
it_handles_invalid_token_and_scope(:get, '/user/group_order_articles')
it_handles_invalid_token_and_scope(:post, '/user/group_order_articles', -> { api_auth(create_params) })
it_handles_invalid_token_and_scope(:get, '/user/group_order_articles/{id}', -> { api_auth({ 'id' => goa.id }) })
it_handles_invalid_token_and_scope(:patch, '/user/group_order_articles/{id}', -> { api_auth(update_params) })
it_handles_invalid_token_and_scope(:delete, '/user/group_order_articles/{id}', -> { api_auth({ 'id' => goa.id }) })
end
end
context 'config' do
let(:api_scopes) { ['config:user'] }
it { is_expected.to validate(:get, '/config', 200, api_auth) }
it { is_expected.to validate(:get, '/config', 401) }
it_handles_invalid_token_and_scope(:get, '/config')
end
context 'navigation' do
it { is_expected.to validate(:get, '/navigation', 200, api_auth) }
it { is_expected.to validate(:get, '/navigation', 401) }
it_handles_invalid_token(:get, '/navigation')
end
context 'financial_transactions' do
let(:api_scopes) { ['finance:read'] }
let(:user) { create(:user, :role_finance) }
let(:other_user) { create :user, :ordergroup }
let!(:ft_1) { create :financial_transaction, ordergroup: other_user.ordergroup }
let!(:ft_2) { create :financial_transaction, ordergroup: other_user.ordergroup }
it { is_expected.to validate(:get, '/financial_transactions', 200, api_auth) }
it { is_expected.to validate(:get, '/financial_transactions/{id}', 200, api_auth({ 'id' => ft_2.id })) }
it { is_expected.to validate(:get, '/financial_transactions/{id}', 404, api_auth({ 'id' => FinancialTransaction.last.id + 1 })) }
context 'without role_finance' do
let(:user) { create(:user) }
it { is_expected.to validate(:get, '/financial_transactions', 403, api_auth) }
it { is_expected.to validate(:get, '/financial_transactions/{id}', 403, api_auth({ 'id' => ft_2.id })) }
end
it_handles_invalid_token_and_scope(:get, '/financial_transactions')
it_handles_invalid_token_and_scope(:get, '/financial_transactions/{id}', -> { api_auth({ 'id' => ft_2.id }) })
end
context 'financial_transaction_classes' do
let!(:cla_1) { create :financial_transaction_class }
let!(:cla_2) { create :financial_transaction_class }
it { is_expected.to validate(:get, '/financial_transaction_classes', 200, api_auth) }
it { is_expected.to validate(:get, '/financial_transaction_classes/{id}', 200, api_auth({ 'id' => cla_2.id })) }
it { is_expected.to validate(:get, '/financial_transaction_classes/{id}', 404, api_auth({ 'id' => cla_2.id + 1 })) }
it_handles_invalid_token(:get, '/financial_transaction_classes')
it_handles_invalid_token(:get, '/financial_transaction_classes/{id}', -> { api_auth({ 'id' => cla_1.id }) })
end
context 'financial_transaction_types' do
let!(:tpy_1) { create :financial_transaction_type }
let!(:tpy_2) { create :financial_transaction_type }
it { is_expected.to validate(:get, '/financial_transaction_types', 200, api_auth) }
it { is_expected.to validate(:get, '/financial_transaction_types/{id}', 200, api_auth({ 'id' => tpy_2.id })) }
it { is_expected.to validate(:get, '/financial_transaction_types/{id}', 404, api_auth({ 'id' => tpy_2.id + 1 })) }
it_handles_invalid_token(:get, '/financial_transaction_types')
it_handles_invalid_token(:get, '/financial_transaction_types/{id}', -> { api_auth({ 'id' => tpy_1.id }) })
end
context 'orders' do
let(:api_scopes) { ['orders:read'] }
let!(:order) { create :order }
it { is_expected.to validate(:get, '/orders', 200, api_auth) }
it { is_expected.to validate(:get, '/orders/{id}', 200, api_auth({ 'id' => order.id })) }
it { is_expected.to validate(:get, '/orders/{id}', 404, api_auth({ 'id' => Order.last.id + 1 })) }
it_handles_invalid_token_and_scope(:get, '/orders')
it_handles_invalid_token_and_scope(:get, '/orders/{id}', -> { api_auth({ 'id' => order.id }) })
end
context 'order_articles' do
let(:api_scopes) { ['orders:read'] }
let!(:order_article) { create(:order, article_count: 1).order_articles.first }
let!(:stock_article) { create(:stock_article) }
let!(:stock_order_article) { create(:stock_order, article_ids: [stock_article.id]).order_articles.first }
it { is_expected.to validate(:get, '/order_articles', 200, api_auth) }
it { is_expected.to validate(:get, '/order_articles/{id}', 200, api_auth({ 'id' => order_article.id })) }
it { is_expected.to validate(:get, '/order_articles/{id}', 200, api_auth({ 'id' => stock_order_article.id })) }
it { is_expected.to validate(:get, '/order_articles/{id}', 404, api_auth({ 'id' => Article.last.id + 1 })) }
it_handles_invalid_token_and_scope(:get, '/order_articles')
it_handles_invalid_token_and_scope(:get, '/order_articles/{id}', -> { api_auth({ 'id' => order_article.id }) })
end
context 'article_categories' do
let!(:cat_1) { create :article_category }
let!(:cat_2) { create :article_category }
it { is_expected.to validate(:get, '/article_categories', 200, api_auth) }
it { is_expected.to validate(:get, '/article_categories/{id}', 200, api_auth({ 'id' => cat_2.id })) }
it { is_expected.to validate(:get, '/article_categories/{id}', 404, api_auth({ 'id' => cat_2.id + 1 })) }
it_handles_invalid_token(:get, '/article_categories')
it_handles_invalid_token(:get, '/article_categories/{id}', -> { api_auth({ 'id' => cat_1.id }) })
end
end
# needs to be last context so it is always run at the end
context 'and finally' do
it 'tests all documented routes' do
is_expected.to validate_all_paths
end
end
end

View file

@ -1,109 +0,0 @@
require 'spec_helper'
# Most routes are tested in the swagger_spec, this tests endpoints that change data.
describe Api::V1::User::FinancialTransactionsController, type: :controller do
include ApiOAuth
let(:user) { create(:user, :ordergroup) }
let(:api_scopes) { ['finance:user'] }
let(:ftc1) { create :financial_transaction_class }
let(:ftc2) { create :financial_transaction_class }
let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 }
let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 }
let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 }
let(:amount) { rand(-100..100) }
let(:note) { Faker::Lorem.sentence }
let(:json_ft) { json_response['financial_transaction'] }
shared_examples "financial_transactions endpoint success" do
before { request }
it "returns status 200" do
expect(response).to have_http_status :ok
end
end
shared_examples "financial_transactions create/update success" do
include_examples "financial_transactions endpoint success"
it "returns the financial_transaction" do
expect(json_ft['id']).to be_present
expect(json_ft['financial_transaction_type_id']).to eq ftt1.id
expect(json_ft['financial_transaction_type_name']).to eq ftt1.name
expect(json_ft['amount']).to eq amount
expect(json_ft['note']).to eq note
expect(json_ft['user_id']).to eq user.id
end
it "updates the financial_transaction" do
resulting_ft = FinancialTransaction.where(id: json_ft['id']).first
expect(resulting_ft).to be_present
expect(resulting_ft.financial_transaction_type).to eq ftt1
expect(resulting_ft.amount).to eq amount
expect(resulting_ft.note).to eq note
expect(resulting_ft.user).to eq user
end
end
shared_examples "financial_transactions endpoint failure" do |status|
it "returns status #{status}" do
request
expect(response.status).to eq status
end
it "does not change the ordergroup" do
expect { request }.to_not change {
user.ordergroup.attributes
}
end
it "does not change the financial_transactions of ordergroup" do
expect { request }.to_not change {
user.ordergroup.financial_transactions.count
}
end
end
describe "POST :create" do
let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id, note: note } }
let(:request) { post :create, params: { financial_transaction: ft_params, foodcoop: 'f' } }
context 'without using self service' do
include_examples "financial_transactions endpoint failure", 403
end
context 'with using self service' do
before { FoodsoftConfig[:use_self_service] = true }
context "with no existing financial transaction" do
include_examples "financial_transactions create/update success"
end
context "with existing financial transaction" do
before { user.ordergroup.add_financial_transaction! 5000, 'for ordering', user, ftt3 }
include_examples "financial_transactions create/update success"
end
context "with invalid financial transaction type" do
let(:ft_params) { { amount: amount, financial_transaction_type_id: -1, note: note } }
include_examples "financial_transactions endpoint failure", 404
end
context "without note" do
let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id } }
include_examples "financial_transactions endpoint failure", 422
end
context 'without enough balance' do
before { FoodsoftConfig[:minimum_balance] = 1000 }
include_examples "financial_transactions endpoint failure", 403
end
end
end
end

View file

@ -1,220 +0,0 @@
require 'spec_helper'
# Most routes are tested in the swagger_spec, this tests endpoints that change data.
describe Api::V1::User::GroupOrderArticlesController, type: :controller do
include ApiOAuth
let(:user) { create(:user, :ordergroup) }
let(:json_goa) { json_response['group_order_article'] }
let(:json_oa) { json_response['order_article'] }
let(:api_scopes) { ['group_orders:user'] }
let(:order) { create(:order, article_count: 1) }
let(:oa_1) { order.order_articles.first }
let(:other_quantity) { rand(1..10) }
let(:other_tolerance) { rand(1..10) }
let(:user_other) { create(:user, :ordergroup) }
let!(:go_other) { create(:group_order, order: order, ordergroup: user_other.ordergroup) }
let!(:goa_other) { create(:group_order_article, group_order: go_other, order_article: oa_1, quantity: other_quantity, tolerance: other_tolerance) }
before { go_other.update_price!; user_other.ordergroup.update_stats! }
shared_examples "group_order_articles endpoint success" do
before { request }
it "returns status 200" do
expect(response).to have_http_status :ok
end
it "returns the order_article" do
expect(json_oa['id']).to eq oa_1.id
expect(json_oa['quantity']).to eq new_quantity + other_quantity
expect(json_oa['tolerance']).to eq new_tolerance + other_tolerance
end
it "updates the group_order" do
go = nil
expect {
request
go = user.ordergroup.group_orders.where(order: order).last
}.to change { go&.updated_by }.to(user)
.and change { go&.price }
end
end
shared_examples "group_order_articles create/update success" do
include_examples "group_order_articles endpoint success"
it "returns the group_order_article" do
expect(json_goa['id']).to be_present
expect(json_goa['order_article_id']).to eq oa_1.id
expect(json_goa['quantity']).to eq new_quantity
expect(json_goa['tolerance']).to eq new_tolerance
end
it "updates the group_order_article" do
resulting_goa = GroupOrderArticle.where(id: json_goa['id']).first
expect(resulting_goa).to be_present
expect(resulting_goa.quantity).to eq new_quantity
expect(resulting_goa.tolerance).to eq new_tolerance
end
end
shared_examples "group_order_articles endpoint failure" do |status|
it "returns status #{status}" do
request
expect(response.status).to eq status
end
it "does not change the group_order" do
expect { request }.to_not change {
go = user.ordergroup.group_orders.where(order: order).last
go&.attributes
}
end
it "does not change the group_order_article" do
expect { request }.to_not change {
goa = GroupOrderArticle.joins(:group_order)
.where(order_article_id: oa_1.id, group_orders: { ordergroup: user.ordergroup }).last
goa&.attributes
}
end
end
describe "POST :create" do
let(:new_quantity) { rand(1..10) }
let(:new_tolerance) { rand(1..10) }
let(:goa_params) { { order_article_id: oa_1.id, quantity: new_quantity, tolerance: new_tolerance } }
let(:request) { post :create, params: { group_order_article: goa_params, foodcoop: 'f' } }
context "with no existing group_order" do
include_examples "group_order_articles create/update success"
end
context "with an existing group_order" do
let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) }
include_examples "group_order_articles create/update success"
end
context "with an existing group_order_article" do
let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) }
let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 0, tolerance: 1) }
before { go.update_price!; user.ordergroup.update_stats! }
include_examples "group_order_articles endpoint failure", 422
end
context "with invalid parameter values" do
let(:goa_params) { { order_article_id: oa_1.id, quantity: -1, tolerance: new_tolerance } }
include_examples "group_order_articles endpoint failure", 422
end
context 'with a closed order' do
let(:order) { create(:order, article_count: 1, state: :finished) }
include_examples "group_order_articles endpoint failure", 404
end
context 'without enough balance' do
before { FoodsoftConfig[:minimum_balance] = 1000 }
include_examples "group_order_articles endpoint failure", 403
end
context 'without enough apple points' do
before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) }
include_examples "group_order_articles endpoint failure", 403
end
end
describe "PATCH :update" do
let(:new_quantity) { rand(2..10) }
let(:goa_params) { { quantity: new_quantity, tolerance: new_tolerance } }
let(:request) { patch :update, params: { id: goa.id, group_order_article: goa_params, foodcoop: 'f' } }
let(:new_tolerance) { rand(2..10) }
let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) }
let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 1, tolerance: 0) }
before { go.update_price!; user.ordergroup.update_stats! }
context "happy flow" do
include_examples "group_order_articles create/update success"
end
context "with invalid parameter values" do
let(:goa_params) { { order_article_id: oa_1.id, quantity: -1, tolerance: new_tolerance } }
include_examples "group_order_articles endpoint failure", 422
end
context 'with a closed order' do
let(:order) { create(:order, article_count: 1, state: :finished) }
include_examples "group_order_articles endpoint failure", 404
end
context 'without enough balance' do
before { FoodsoftConfig[:minimum_balance] = 1000 }
include_examples "group_order_articles endpoint failure", 403
end
context 'without enough apple points' do
before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) }
include_examples "group_order_articles endpoint failure", 403
end
end
describe "DELETE :destroy" do
let(:new_quantity) { 0 }
let(:request) { delete :destroy, params: { id: goa.id, foodcoop: 'f' } }
let(:new_tolerance) { 0 }
let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) }
let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1) }
before { go.update_price!; user.ordergroup.update_stats! }
shared_examples "group_order_articles destroy success" do
include_examples "group_order_articles endpoint success"
it "does not return the group_order_article" do
expect(json_goa).to be_nil
end
it "deletes the group_order_article" do
expect(GroupOrderArticle.where(id: goa.id)).to be_empty
end
end
context "happy flow" do
include_examples "group_order_articles destroy success"
end
context 'with a closed order' do
let(:order) { create(:order, article_count: 1, state: :finished) }
include_examples "group_order_articles endpoint failure", 404
end
context 'without enough balance' do
before { FoodsoftConfig[:minimum_balance] = 1000 }
include_examples "group_order_articles destroy success"
end
context 'without enough apple points' do
before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) }
include_examples "group_order_articles destroy success"
end
end
end

View file

@ -1,55 +0,0 @@
require 'spec_helper'
describe Api::V1::User::OrdergroupController, type: :controller do
include ApiOAuth
let(:user) { create :user, :ordergroup }
let(:api_scopes) { ['finance:user'] }
let(:ftc1) { create :financial_transaction_class }
let(:ftc2) { create :financial_transaction_class }
let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 }
let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 }
let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 }
describe "GET :financial_overview" do
let(:order) { create(:order, article_count: 1) }
let(:json_financial_overview) { json_response['financial_overview'] }
let(:oa_1) { order.order_articles.first }
let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) }
let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 1, tolerance: 0) }
before { go.update_price!; user.ordergroup.update_stats! }
before do
og = user.ordergroup
og.add_financial_transaction!(-1, '-1', user, ftt1)
og.add_financial_transaction!(2, '2', user, ftt1)
og.add_financial_transaction!(3, '3', user, ftt1)
og.add_financial_transaction!(-10, '-10', user, ftt2)
og.add_financial_transaction!(20, '20', user, ftt2)
og.add_financial_transaction!(30, '30', user, ftt2)
og.add_financial_transaction!(-100, '-100', user, ftt3)
og.add_financial_transaction!(200, '200', user, ftt3)
og.add_financial_transaction!(300, '300', user, ftt3)
end
it "returns correct values" do
get :financial_overview, params: { foodcoop: 'f' }
expect(json_financial_overview['account_balance']).to eq 444
expect(json_financial_overview['available_funds']).to eq 444 - go.price
ftcs = Hash[json_financial_overview['financial_transaction_class_sums'].map { |x| [x['id'], x] }]
ftcs1 = ftcs[ftc1.id]
expect(ftcs1['name']).to eq ftc1.name
expect(ftcs1['amount']).to eq 4
ftcs2 = ftcs[ftc2.id]
expect(ftcs2['name']).to eq ftc2.name
expect(ftcs2['amount']).to eq 440
end
end
end

View file

@ -6,6 +6,7 @@
default: &defaults
multi_coop_install: false
use_self_service: true
default_scope: 'f'
name: FC Minimal

View file

@ -0,0 +1,53 @@
require 'swagger_helper'
describe 'Article Categories', type: :request do
include ApiHelper
path '/article_categories' do
get 'article categories' do
tags 'Category'
produces 'application/json'
pagination_param
let(:order_article) { create(:order, article_count: 1).order_articles.first }
let(:stock_article) { create(:stock_article) }
let(:stock_order_article) { create(:stock_order, article_ids: [stock_article.id]).order_articles.first }
response '200', 'success' do
schema type: :object, properties: {
article_categories: {
type: :array,
items: {
'$ref': '#/components/schemas/ArticleCategory'
}
}
}
run_test!
end
it_handles_invalid_token
end
end
path '/article_categories/{id}' do
get 'find article category by id' do
tags 'Category'
produces 'application/json'
id_url_param
response '200', 'article category found' do
schema type: :object, properties: {
article_categories: {
type: :array,
items: {
'$ref': '#/components/schemas/ArticleCategory'
}
}
}
let(:id) { create(:article_category, name: 'dairy').id }
run_test!
end
it_handles_invalid_token_with_id
it_cannot_find_object
end
end
end

View file

@ -0,0 +1,20 @@
require 'swagger_helper'
describe 'Config', type: :request do
include ApiHelper
path '/config' do
get 'configuration variables' do
tags 'General'
produces 'application/json'
let(:api_scopes) { ['config:user'] }
response '200', 'success' do
schema type: :object, properties: {}
run_test!
end
it_handles_invalid_token_and_scope
end
end
end

View file

@ -0,0 +1,54 @@
require 'swagger_helper'
describe 'Financial Transaction Classes', type: :request do
include ApiHelper
path '/financial_transaction_classes' do
get 'financial transaction classes' do
tags 'Category'
produces 'application/json'
pagination_param
let(:financial_transaction_class) { create(:financial_transaction_class) }
response '200', 'success' do
schema type: :object, properties: {
meta: { '$ref' => '#/components/schemas/Meta' },
financial_transaction_class: {
type: :array,
items: {
'$ref': '#/components/schemas/FinancialTransactionClass'
}
}
}
run_test!
end
it_handles_invalid_token
end
end
path '/financial_transaction_classes/{id}' do
get 'Retrieves a financial transaction class' do
tags 'Category'
produces 'application/json'
id_url_param
response '200', 'financial transaction class found' do
schema type: :object, properties: {
financial_transaction_classes: {
type: :array,
items: {
'$ref': '#/components/schemas/FinancialTransactionClass'
}
}
}
let(:id) { create(:financial_transaction_class).id }
run_test!
end
it_handles_invalid_token_with_id
it_cannot_find_object 'financial transaction class not found'
end
end
end

View file

@ -0,0 +1,52 @@
require 'swagger_helper'
describe 'Financial Transaction types', type: :request do
include ApiHelper
path '/financial_transaction_types' do
get 'financial transaction types' do
tags 'Category'
produces 'application/json'
pagination_param
let(:financial_transaction_type) { create(:financial_transaction_type) }
response '200', 'success' do
schema type: :object, properties: {
meta: { '$ref' => '#/components/schemas/Meta' },
financial_transaction_type: {
type: :array,
items: {
'$ref': '#/components/schemas/FinancialTransactionType'
}
}
}
run_test!
end
it_handles_invalid_token
end
end
path '/financial_transaction_types/{id}' do
get 'find financial transaction type by id' do
tags 'Category'
produces 'application/json'
id_url_param
response '200', 'financial transaction type found' do
schema type: :object, properties: {
financial_transaction_types: {
type: :array,
items: {
'$ref': '#/components/schemas/FinancialTransactionType'
}
}
}
let(:id) { create(:financial_transaction_type).id }
run_test!
end
it_handles_invalid_token_with_id
it_cannot_find_object 'financial transaction type not found'
end
end
end

View file

@ -0,0 +1,56 @@
require 'swagger_helper'
describe 'Financial Transaction', type: :request do
include ApiHelper
let!(:finance_user) { create(:user, groups: [create(:workgroup, role_finance: true)]) }
let!(:api_scopes) { ['finance:read', 'finance:write'] }
let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: finance_user.id, scopes: api_scopes&.join(' ')).token }
let(:financial_transaction) { create(:financial_transaction, user: user) }
path '/financial_transactions' do
get 'financial transactions' do
tags 'Financial Transaction'
produces 'application/json'
pagination_param
response '200', 'success' do
schema type: :object, properties: {
meta: { '$ref' => '#/components/schemas/Meta' },
financial_transaction: {
type: :array,
items: {
'$ref': '#/components/schemas/FinancialTransaction'
}
}
}
run_test!
end
it_handles_invalid_token_and_scope
end
end
path '/financial_transactions/{id}' do
get 'Retrieves a financial transaction ' do
tags 'Financial Transaction'
produces 'application/json'
id_url_param
response '200', 'financial transaction found' do
schema type: :object, properties: {
financial_transaction: {
type: :array,
items: {
'$ref': '#/components/schemas/FinancialTransaction'
}
}
}
let(:id) { FinancialTransaction.create(user: user).id }
run_test!
end
it_handles_invalid_token_with_id
it_handles_invalid_scope_with_id
it_cannot_find_object 'financial transaction not found'
end
end
end

View file

@ -0,0 +1,24 @@
require 'swagger_helper'
describe 'Navigation', type: :request do
include ApiHelper
path '/navigation' do
get 'navigation' do
tags 'General'
produces 'application/json'
response '200', 'success' do
schema type: :object, properties: {
navigation: {
'$ref' => '#/components/schemas/Navigation'
}
}
run_test!
end
it_handles_invalid_token
end
end
end

View file

@ -0,0 +1,115 @@
require 'swagger_helper'
describe 'Order Articles', type: :request do
include ApiHelper
path '/order_articles' do
get 'order articles' do
tags 'Order'
produces 'application/json'
pagination_param
q_ordered_url_param
let(:api_scopes) { ['orders:read', 'orders:write'] }
let(:order) { create(:order, article_count: 4) }
let(:order_articles) { order.order_articles }
before do
order_articles[0].update! quantity: 0, tolerance: 0, units_to_order: 0
order_articles[1].update! quantity: 1, tolerance: 0, units_to_order: 0
order_articles[2].update! quantity: 0, tolerance: 1, units_to_order: 0
order_articles[3].update! quantity: 0, tolerance: 0, units_to_order: 1
end
response '200', 'success' do
schema type: :object, properties: {
meta: { '$ref' => '#/components/schemas/Meta' },
order_articles: {
type: :array,
items: {
'$ref': '#/components/schemas/OrderArticle'
}
}
}
describe '(unset)' do
run_test!
end
describe 'all' do
let(:q) { { q: { ordered: 'all' } } }
run_test! do |response|
json_order_articles = JSON.parse(response.body)['order_articles']
json_order_article_ids = json_order_articles.map { |d| d['id'].to_i }
expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id)
end
end
describe 'when ordered by supplier' do
let(:q) { { q: { ordered: 'supplier' } } }
run_test! do |response|
json_order_articles = JSON.parse(response.body)['order_articles']
json_order_article_ids = json_order_articles.map { |d| d['id'].to_i }
expect(json_order_article_ids).to match_array [order_articles[3].id]
end
end
describe 'when ordered by member' do
let(:q) { { q: { ordered: 'member' } } }
run_test! do |response|
json_order_articles = JSON.parse(response.body)['order_articles']
expect(json_order_articles.count).to eq 0
end
end
context 'when ordered by user' do
let(:user) { create(:user, :ordergroup) }
let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) }
before do
create(:group_order_article, group_order: go, order_article: order_articles[1], quantity: 1)
create(:group_order_article, group_order: go, order_article: order_articles[2], tolerance: 0)
end
describe 'member' do
let(:q) { { q: { ordered: 'member' } } }
run_test! do |response|
json_order_articles = JSON.parse(response.body)['order_articles']
json_order_article_ids = json_order_articles.map { |d| d['id'].to_i }
expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id)
end
end
end
end
it_handles_invalid_token_and_scope
end
end
path '/order_articles/{id}' do
get 'order articles' do
tags 'Order'
produces 'application/json'
id_url_param
let(:api_scopes) { ['orders:read', 'orders:write'] }
response '200', 'success' do
schema type: :object, properties: {
order_article: {
'$ref': '#/components/schemas/OrderArticle'
}
}
let(:order) { create(:order, article_count: 1) }
let(:id) { order.order_articles.first.id }
run_test!
end
it_handles_invalid_token_and_scope
it_cannot_find_object 'order article not found'
end
end
end

View file

@ -0,0 +1,55 @@
require 'swagger_helper'
describe 'Orders', type: :request do
include ApiHelper
let(:api_scopes) { ['orders:read'] }
path '/orders' do
get 'orders' do
tags 'Order'
produces 'application/json'
pagination_param
let(:order) { create(:order) }
response '200', 'success' do
schema type: :object, properties: {
meta: { '$ref' => '#/components/schemas/Meta' },
ordes: {
type: :array,
items: {
'$ref': '#/components/schemas/Order'
}
}
}
run_test!
end
it_handles_invalid_token_and_scope
end
end
path '/orders/{id}' do
get 'Order' do
tags 'Order'
produces 'application/json'
id_url_param
let(:order) { create(:order) }
response '200', 'success' do
schema type: :object, properties: {
order: { '$ref' => '#/components/schemas/Order' }
}
let(:id) { order.id }
run_test! do |response|
expect(JSON.parse(response.body)['order']['id']).to eq order.id
end
end
it_handles_invalid_token_and_scope
it_cannot_find_object 'order not found'
end
end
end

View file

@ -0,0 +1,106 @@
require 'swagger_helper'
describe 'User', type: :request do
include ApiHelper
let(:api_scopes) { ['finance:user'] }
let(:user) { create :user, groups: [create(:ordergroup)] }
let(:other_user2) { create :user }
let(:ft) { create(:financial_transaction, user: user, ordergroup: user.ordergroup) }
before do
ft
end
path '/user/financial_transactions' do
post 'create new financial transaction (requires enabled self service)' do
tags 'Financial Transaction'
consumes 'application/json'
produces 'application/json'
parameter name: :financial_transaction, in: :body, schema: {
type: :object,
properties: {
amount: { type: :integer },
financial_transaction_type: { type: :integer },
note: { type: :string }
}
}
let(:financial_transaction) { { amount: 3, financial_transaction_type_id: create(:financial_transaction_type).id, note: 'lirum larum' } }
response '200', 'success' do
schema type: :object, properties: {
financial_transaction: { '$ref': '#/components/schemas/FinancialTransaction' }
}
run_test!
end
it_handles_invalid_token_with_id
it_handles_invalid_scope_with_id 'user has no ordergroup, is below minimum balance, self service is disabled, or missing scope'
response '404', 'financial transaction type not found' do
schema '$ref' => '#/components/schemas/Error404'
let(:financial_transaction) { { amount: 3, financial_transaction_type_id: 'invalid', note: 'lirum larum' } }
run_test!
end
response '422', 'invalid parameter value' do
xit 'TODO: fix controller to actually send a 422 for invalid params: https://github.com/foodcoops/foodsoft/issues/999'
# schema '$ref' => '#/components/schemas/Error422'
# let(:financial_transaction) { { amount: -3, financial_transaction_type_id: create(:financial_transaction_type).id, note: -2 } }
# run_test!
end
end
get "financial transactions of the member's ordergroup" do
tags 'User', 'Financial Transaction'
produces 'application/json'
pagination_param
response '200', 'success' do
schema type: :object, properties: {
meta: { '$ref': '#/components/schemas/Meta' },
financial_transaction: {
type: :array,
items: {
'$ref': '#/components/schemas/FinancialTransaction'
}
}
}
run_test! do |response|
data = JSON.parse(response.body)
expect(data['financial_transactions'].first['id']).to eq(ft.id)
end
end
it_handles_invalid_token_and_scope
end
end
path '/user/financial_transactions/{id}' do
get 'find financial transaction by id' do
tags 'User', 'Financial Transaction'
produces 'application/json'
id_url_param
response '200', 'success' do
schema type: :object, properties: {
financial_transaction: {
'$ref': '#/components/schemas/FinancialTransaction'
}
}
let(:id) { ft.id }
run_test! do |response|
data = JSON.parse(response.body)
expect(data['financial_transaction']['id']).to eq(ft.id)
end
end
it_handles_invalid_token_with_id
it_handles_invalid_scope_with_id 'user has no ordergroup or missing scope'
it_cannot_find_object 'financial transaction not found'
end
end
end

View file

@ -0,0 +1,192 @@
require 'swagger_helper'
describe 'User', type: :request do
include ApiHelper
let(:api_scopes) { ['group_orders:user'] }
let(:user) { create :user, groups: [create(:ordergroup)] }
let(:other_user2) { create :user }
let(:order) { create(:order, article_count: 4) }
let(:order_articles) { order.order_articles }
let(:group_order) { create :group_order, ordergroup: user.ordergroup, order_id: order.id }
let(:goa) { create :group_order_article, group_order: group_order, order_article: order_articles.first }
before do
goa
end
path '/user/group_order_articles' do
get 'group order articles' do
tags 'User', 'Order'
produces 'application/json'
pagination_param
q_ordered_url_param
response '200', 'success' do
schema type: :object, properties: {
meta: { '$ref': '#/components/schemas/Meta' },
group_order_article: {
type: :array,
items: {
'$ref': '#/components/schemas/GroupOrderArticle'
}
}
}
run_test! do |response|
data = JSON.parse(response.body)
expect(data['group_order_articles'].first['id']).to eq(goa.id)
end
end
it_handles_invalid_token
it_handles_invalid_scope 'user has no ordergroup or missing scope'
end
post 'create new group order article' do
tags 'User', 'Order'
consumes 'application/json'
produces 'application/json'
parameter name: :group_order_article, in: :body,
description: 'group order article to create',
required: true,
schema: { '$ref': '#/components/schemas/GroupOrderArticleForCreate' }
let(:group_order_article) { { order_article_id: order_articles.last.id, quantity: 1, tolerance: 2 } }
response '200', 'success' do
schema type: :object, properties: {
group_order_article: {
'$ref': '#/components/schemas/GroupOrderArticle'
},
order_article: {
'$ref': '#/components/schemas/OrderArticle'
}
}
run_test!
end
it_handles_invalid_token_with_id
it_handles_invalid_scope_with_id 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope'
response '404', 'order article not found in open orders' do
let(:group_order_article) { { order_article_id: 'invalid', quantity: 1, tolerance: 2 } }
schema '$ref' => '#/components/schemas/Error404'
run_test!
end
response '422', 'invalid parameter value or group order article already exists' do
let(:group_order_article) { { order_article_id: goa.order_article_id, quantity: 1, tolerance: 2 } }
schema '$ref' => '#/components/schemas/Error422'
run_test!
end
end
end
path '/user/group_order_articles/{id}' do
get 'find group order article by id' do
tags 'User', 'Order'
produces 'application/json'
id_url_param
response '200', 'success' do
schema type: :object, properties: {
group_order_article: {
'$ref': '#/components/schemas/GroupOrderArticle'
},
order_article: {
'$ref': '#/components/schemas/OrderArticle'
}
}
let(:id) { goa.id }
run_test! do |response|
data = JSON.parse(response.body)
expect(data['group_order_article']['id']).to eq(goa.id)
end
end
it_handles_invalid_scope_with_id
it_handles_invalid_token_with_id
it_cannot_find_object 'group order article not found'
end
patch 'update a group order article (but delete if quantity and tolerance are zero)' do
tags 'User', 'Order'
consumes 'application/json'
produces 'application/json'
id_url_param
parameter name: :group_order_article, in: :body,
description: 'group order article update',
required: true,
schema: { '$ref': '#/components/schemas/GroupOrderArticleForUpdate' }
let(:id) { goa.id }
let(:group_order_article) { { order_article_id: goa.order_article_id, quantity: 2, tolerance: 2 } }
response '200', 'success' do
schema type: :object, properties: {
group_order_article: {
'$ref': '#/components/schemas/GroupOrderArticle'
}
}
run_test!
end
response 401, 'not logged-in' do
schema '$ref' => '#/components/schemas/Error401'
let(:Authorization) { 'abc' }
run_test!
end
response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do
let(:api_scopes) { ['none'] }
schema '$ref' => '#/components/schemas/Error403'
run_test!
end
response '404', 'order article not found in open orders' do
schema type: :object, properties: {
group_order_article: {
'$ref': '#/components/schemas/GroupOrderArticle'
}
}
let(:id) { 'invalid' }
run_test!
end
response '422', 'invalid parameter value' do
let(:group_order_article) { { order_article_id: 'invalid', quantity: -5, tolerance: 'invalid' } }
schema '$ref' => '#/components/schemas/Error422'
run_test!
end
end
delete 'remove group order article' do
tags 'User', 'Order'
consumes 'application/json'
produces 'application/json'
id_url_param
let(:api_scopes) { ['group_orders:user'] }
response '200', 'success' do
schema type: :object, properties: {
group_order_article: {
'$ref': '#/components/schemas/GroupOrderArticle'
}
}
let(:id) { goa.id }
run_test!
end
it_handles_invalid_token_with_id
response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do
let(:api_scopes) { ['none'] }
schema '$ref' => '#/components/schemas/Error403'
run_test!
end
it_cannot_find_object 'order article not found in open orders'
end
end
end

View file

@ -0,0 +1,103 @@
require 'swagger_helper'
describe 'User', type: :request do
include ApiHelper
path '/user' do
get 'info about the currently logged-in user' do
tags 'User'
produces 'application/json'
let(:api_scopes) { ['user:read'] }
let(:other_user1) { create :user }
let(:user) { create :user }
let(:other_user2) { create :user }
response '200', 'success' do
schema type: :object,
properties: {
user: {
type: :object,
properties: {
id: {
type: :integer
},
name: {
type: :string,
description: 'full name'
},
email: {
type: :string,
description: 'email address'
},
locale: {
type: :string,
description: 'language code'
}
},
required: %w[id name email]
}
}
run_test! do |response|
data = JSON.parse(response.body)
expect(data['user']['id']).to eq(user.id)
end
end
it_handles_invalid_token_and_scope
end
end
path '/user/financial_overview' do
get 'financial summary about the currently logged-in user' do
tags 'User', 'Financial Transaction'
produces 'application/json'
let(:user) { create :user, :ordergroup }
let(:api_scopes) { ['finance:user'] }
FinancialTransactionClass.create(name: 'TestTransaction')
response 200, 'success' do
schema type: :object,
properties: {
financial_overview: {
type: :object,
properties: {
account_balance: {
type: :number,
description: 'booked accout balance of ordergroup'
},
available_funds: {
type: :number,
description: 'fund available to order articles'
},
financial_transaction_class_sums: {
type: :array,
properties: {
id: {
type: :integer,
description: 'id of the financial transaction class'
},
name: {
type: :string,
description: 'name of the financial transaction class'
},
amount: {
type: :number,
description: 'sum of the amounts belonging to the financial transaction class'
}
},
required: %w[id name amount]
}
},
required: %w[account_balance available_funds financial_transaction_class_sums]
}
}
run_test!
end
it_handles_invalid_token_and_scope
end
end
end

View file

@ -5,21 +5,60 @@ module ApiHelper
let(:user) { create(:user) }
let(:api_scopes) { [] } # empty scopes for stricter testing (in reality this would be default_scopes)
let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: user.id, scopes: api_scopes&.join(' ')).token }
let(:api_authorization) { "Bearer #{api_access_token}" }
let(:Authorization) { "Bearer #{api_access_token}" }
def self.it_handles_invalid_token(method, path, params_block = -> { api_auth })
def self.it_handles_invalid_token
context 'with invalid access token' do
let(:api_access_token) { 'abc' }
let(:Authorization) { 'abc' }
it { is_expected.to validate(method, path, 401, instance_exec(&params_block)) }
response 401, 'not logged-in' do
schema '$ref' => '#/components/schemas/Error401'
run_test!
end
end
end
def self.it_handles_invalid_scope(method, path, params_block = -> { api_auth })
def self.it_handles_invalid_token_with_id
context 'with invalid access token' do
let(:Authorization) { 'abc' }
let(:id) { 42 } # id doesn't matter here
response 401, 'not logged-in' do
schema '$ref' => '#/components/schemas/Error401'
run_test!
end
end
end
def self.it_handles_invalid_scope(description = 'missing scope')
context 'with invalid scope' do
let(:api_scopes) { ['none'] }
it { is_expected.to validate(method, path, 403, instance_exec(&params_block)) }
response 403, description do
schema '$ref' => '#/components/schemas/Error403'
run_test!
end
end
end
def self.it_handles_invalid_scope_with_id(description = 'missing scope')
context 'with invalid scope' do
let(:api_scopes) { ['none'] }
let(:id) { 42 } # id doesn't matter here
response 403, description do
schema '$ref' => '#/components/schemas/Error403'
run_test!
end
end
end
def self.it_cannot_find_object(description = 'not found')
let(:id) { 'invalid' }
response 404, description do
schema '$ref' => '#/components/schemas/Error404'
run_test!
end
end
@ -27,13 +66,25 @@ module ApiHelper
it_handles_invalid_token(*args)
it_handles_invalid_scope(*args)
end
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 api_auth(params = {})
{ '_headers' => { 'Authorization' => api_authorization } }.deep_merge(params)
def self.id_url_param
parameter name: :id, in: :path, type: :integer, required: true
end
def self.pagination_param
parameter name: :per_page, in: :query, type: :integer, required: false
parameter name: :page, in: :query, type: :integer, required: false
end
def self.q_ordered_url_param
parameter name: :q, in: :query, required: false,
description: "'member' show articles ordered by the user's ordergroup, 'all' by all members, and 'supplier' ordered at the supplier",
schema: {
type: :object,
properties: {
ordered: { '$ref' => '#/components/schemas/q_ordered' }
}
}
end
end
end

513
spec/swagger_helper.rb Normal file
View file

@ -0,0 +1,513 @@
# frozen_string_literal: true
require 'spec_helper'
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.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
# be generated at the provided relative path under swagger_root
# By default, the operations defined in spec files are added to the first
# 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.yaml' => {
openapi: '3.0.3',
info: {
title: 'API V1',
version: 'v1'
},
paths: {},
components: {
schemas: {
pagination: {
type: :object,
properties: {
recordCount: { type: :integer },
pageCount: { type: :integer },
currentPage: { type: :integer },
pageSize: { type: :integer }
},
required: %w(recordCount pageCount currentPage pageSize)
},
Order: {
type: :object,
properties: {
id: {
type: :integer
},
name: {
type: :string,
description: "name of the order's supplier (or stock)"
},
starts: {
type: :string,
format: 'date-time',
description: 'when the order was opened'
},
ends: {
type: :string,
nullable: true,
format: 'date-time',
description: 'when the order will close or was closed'
},
boxfill: {
type: :string,
nullable: true,
format: 'date-time',
description: 'when the order will enter or entered the boxfill phase'
},
pickup: {
type: :string,
nullable: true,
format: :date,
description: 'pickup date'
},
is_open: {
type: :boolean,
description: 'if the order is currently open or not'
},
is_boxfill: {
type: :boolean,
description: 'if the order is currently in the boxfill phase or not'
}
}
},
Article: {
type: :object,
properties: {
id: {
type: :integer
},
name: {
type: :string
},
supplier_id: {
type: :integer,
description: 'id of supplier, or 0 for stock articles'
},
supplier_name: {
type: :string,
nullable: true,
description: 'name of the supplier, or null for stock articles'
},
unit: {
type: :string,
description: 'amount of each unit, e.g. "100 g" or "kg"'
},
unit_quantity: {
type: :integer,
description: 'units can only be ordered from the supplier in multiples of unit_quantity'
},
note: {
type: :string,
nullable: true,
description: 'generic note'
},
manufacturer: {
type: :string,
nullable: true,
description: 'manufacturer'
},
origin: {
type: :string,
nullable: true,
description: 'origin, preferably (starting with a) 2-letter ISO country code'
},
article_category_id: {
type: :integer,
description: 'id of article category'
},
quantity_available: {
type: :integer,
description: 'number of units available (only present on stock articles)'
}
},
required: %w[id name supplier_id supplier_name unit unit_quantity note manufacturer origin article_category_id]
},
OrderArticle: {
type: :object,
properties: {
id: {
type: :integer
},
order_id: {
type: :integer,
description: 'id of order this order article belongs to'
},
price: {
type: :number,
format: :float,
description: 'foodcoop price'
},
quantity: {
type: :integer,
description: 'number of units ordered by members'
},
tolerance: {
type: :integer,
description: 'number of extra units that members are willing to buy to fill a box'
},
units_to_order: {
type: :integer,
description: 'number of units to order from the supplier'
},
article: {
'$ref': '#/components/schemas/Article'
}
}
},
ArticleCategory: {
type: :object,
properties: {
id: {
type: :integer
},
name: {
type: :string
}
},
required: %w[id name]
},
FinancialTransaction: {
allOf: [
{ '$ref': '#/components/schemas/FinancialTransactionForCreate' },
{
type: :object,
properties: {
id: {
type: :integer
},
amount: {
type: :number,
format: :float,
description: 'amount credited (negative for a debit transaction)'
},
financial_transaction_type_id: {
type: :integer,
description: 'id of the type of the transaction'
},
note: {
type: :string,
description: 'note entered with the transaction'
},
user_id: {
type: :integer,
nullable: true,
description: 'id of user who entered the transaction (may be <tt>null</tt> for deleted users or 0 for a system user)'
},
user_name: {
type: :string,
nullable: true,
description: 'name of user who entered the transaction (may be <tt>null</tt> or empty string for deleted users or system users)'
},
financial_transaction_type_name: {
type: :string,
description: 'name of the type of the transaction'
},
created_at: {
type: :string,
format: 'date-time',
description: 'when the transaction was entered'
}
},
required: %w[id user_id user_name financial_transaction_type_name created_at]
}
]
},
FinancialTransactionForCreate: {
type: :object,
properties: {
amount: {
type: :number,
format: :float,
description: 'amount credited (negative for a debit transaction)'
},
financial_transaction_type_id:
{
type: :integer,
description: 'id of the type of the transaction'
},
note: {
type: :string,
description: 'note entered with the transaction'
}
},
required: %w[amount note user_id]
},
FinancialTransactionClass: {
type: :object,
properties: {
id: {
type: :integer
},
name: {
type: :string
}
},
required: %w[id name]
},
FinancialTransactionType: {
type: :object,
properties: {
id: {
type: :integer
},
name: {
type: :string
},
name_short: {
type: :string,
nullable: true,
description: 'short name (used for bank transfers)'
},
bank_account_id: {
type: :integer,
nullable: true,
description: 'id of the bank account used for this transaction type'
},
bank_account_name: {
type: :string,
nullable: true,
description: 'name of the bank account used for this transaction type'
},
bank_account_iban: {
type: :string,
nullable: true,
description: 'IBAN of the bank account used for this transaction type'
},
financial_transaction_class_id: {
type: :integer,
description: 'id of the class of the transaction'
},
financial_transaction_class_name: {
type: :string,
description: 'name of the class of the transaction'
}
},
required: %w[id name financial_transaction_class_id financial_transaction_class_name]
},
GroupOrderArticleForUpdate: {
type: :object,
properties: {
quantity:
{
type: :integer,
description: 'number of units ordered by the users ordergroup'
},
tolerance:
{
type: :integer,
description: 'number of extra units the users ordergroup is willing to buy for filling a box'
}
}
},
GroupOrderArticleForCreate: {
allOf: [
{ '$ref': '#/components/schemas/GroupOrderArticleForUpdate' },
{
type: :object,
properties: {
order_article_id:
{
type: :integer,
description: 'id of order article'
}
}
}
]
},
GroupOrderArticle: {
allOf: [
{ '$ref': '#/components/schemas/GroupOrderArticleForCreate' },
{
type: :object,
properties: {
id: {
type: :integer
},
result: {
type: :number,
format: :float,
description: 'number of units the users ordergroup will receive or has received'
},
total_price:
{
type: :number,
format: :float,
description: 'total price of this group order article'
},
order_article_id:
{
type: :integer,
description: 'id of order article'
}
},
required: %w[order_article_id]
}
]
},
q_ordered: {
type: :object,
properties: {
ordered: {
type: :string,
enum: %w[member all supplier]
}
}
},
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: %w[page per_page total_pages total_count]
},
Navigation: {
type: :array,
items: {
type: :object,
properties: {
name: {
type: :string,
description: 'title'
},
url: {
type: :string,
description: 'link'
},
items: {
'$ref': "#/components/schemas/Navigation"
}
},
required: ['name'],
minProperties: 2 # name+url or name+items
}
},
Error: {
type: :object,
properties: {
error: {
type: :string,
description: 'error code'
},
error_description: {
type: :string,
description: 'human-readable error message (localized)'
}
}
},
Error401: {
type: :object,
properties: {
error: {
type: :string,
description: '<tt>unauthorized</tt>'
},
error_description: {
'$ref': '#/components/schemas/Error/properties/error_description'
}
}
},
Error403: {
type: :object,
properties: {
error: {
type: :string,
description: '<tt>forbidden</tt> or <tt>invalid_scope</tt>'
},
error_description: {
'$ref': '#/components/schemas/Error/properties/error_description'
}
}
},
Error404: {
type: :object,
properties: {
error: {
type: :string,
description: '<tt>not_found</tt>'
},
error_description: {
'$ref': '#/components/schemas/Error/properties/error_description'
}
}
},
Error422: {
type: :object,
properties: {
error: {
type: :string,
description: '<tt>unprocessable entity</tt>'
},
error_description: {
'$ref': '#/components/schemas/Error/properties/error_description'
}
}
}
},
securitySchemes: {
oauth2: {
type: :oauth2,
flows: {
implicit: {
authorizationUrl: 'http://localhost:3000/f/oauth/authorize',
scopes: {
'config:user': 'reading Foodsoft configuration for regular users',
'config:read': 'reading Foodsoft configuration values',
'config:write': 'reading and updating Foodsoft configuration values',
'finance:user': 'accessing your own financial transactions',
'finance:read': 'reading all financial transactions',
'finance:write': 'reading and creating financial transactions',
'user:read': 'reading your own user profile',
'user:write': 'reading and updating your own user profile',
offline_access: 'retain access after user has logged out'
}
}
}
}
}
},
servers: [
{
url: 'http://{defaultHost}/f/api/v1',
variables: {
defaultHost: {
default: 'localhost:3000'
}
}
}
],
security: [
oauth2: [
'user:read'
]
]
}
}
# 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