mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-13 23:37:47 +00:00
Merge pull request #8891 from mkllnk/api-customers
Create API v1 with customers endpoint
This commit is contained in:
@@ -27,11 +27,16 @@ Metrics/BlockLength:
|
||||
"class_eval",
|
||||
"collection",
|
||||
"context",
|
||||
"delete",
|
||||
"describe",
|
||||
"feature",
|
||||
"get",
|
||||
"it",
|
||||
"member",
|
||||
"namespace",
|
||||
"path",
|
||||
"post",
|
||||
"put",
|
||||
"resource",
|
||||
"resources",
|
||||
"scenario",
|
||||
|
||||
@@ -466,6 +466,7 @@ Metrics/BlockLength:
|
||||
- 'spec/lib/open_food_network/group_buy_report_spec.rb'
|
||||
- 'spec/requests/api/orders_spec.rb'
|
||||
- 'spec/spec_helper.rb'
|
||||
- 'spec/swagger_helper.rb'
|
||||
- 'spec/support/cancan_helper.rb'
|
||||
- 'spec/support/matchers/select2_matchers.rb'
|
||||
- 'spec/support/matchers/table_matchers.rb'
|
||||
@@ -582,6 +583,7 @@ Metrics/MethodLength:
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/checkout_helper.rb'
|
||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||
- "app/json_schemas/json_api_schema.rb"
|
||||
- 'app/models/spree/ability.rb'
|
||||
- 'app/models/spree/gateway/pay_pal_express.rb'
|
||||
- 'app/models/spree/order/checkout.rb'
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -57,6 +57,7 @@ gem 'devise-token_authenticatable'
|
||||
gem 'jwt', '~> 2.3'
|
||||
gem 'oauth2', '~> 1.4.7' # Used for Stripe Connect
|
||||
|
||||
gem 'jsonapi-serializer'
|
||||
gem 'pagy', '~> 5.1'
|
||||
|
||||
gem 'rswag-api'
|
||||
|
||||
@@ -346,6 +346,8 @@ GEM
|
||||
json_spec (1.1.5)
|
||||
multi_json (~> 1.0)
|
||||
rspec (>= 2.0, < 4.0)
|
||||
jsonapi-serializer (2.2.0)
|
||||
activesupport (>= 4.2)
|
||||
jwt (2.3.0)
|
||||
knapsack (4.0.0)
|
||||
rake
|
||||
@@ -739,6 +741,7 @@ DEPENDENCIES
|
||||
jquery-ui-rails (~> 4.2)
|
||||
json
|
||||
json_spec (~> 1.1.4)
|
||||
jsonapi-serializer
|
||||
jwt (~> 2.3)
|
||||
knapsack
|
||||
letter_opener (>= 1.4.1)
|
||||
|
||||
97
app/controllers/api/v1/base_controller.rb
Normal file
97
app/controllers/api/v1/base_controller.rb
Normal file
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class BaseController < ActionController::API
|
||||
include CanCan::ControllerAdditions
|
||||
include RequestTimeouts
|
||||
include Pagy::Backend
|
||||
include JsonApiPagination
|
||||
|
||||
check_authorization
|
||||
|
||||
attr_accessor :current_api_user
|
||||
|
||||
before_action :authenticate_user
|
||||
|
||||
rescue_from StandardError, with: :error_during_processing
|
||||
rescue_from CanCan::AccessDenied, with: :unauthorized
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from Pagy::VariableError, with: :invalid_pagination
|
||||
|
||||
private
|
||||
|
||||
def authenticate_user
|
||||
return if (@current_api_user = request.env['warden'].user)
|
||||
|
||||
if api_key.blank?
|
||||
# An anonymous user
|
||||
@current_api_user = Spree::User.new
|
||||
return
|
||||
end
|
||||
|
||||
return if (@current_api_user = Spree::User.find_by(spree_api_key: api_key.to_s))
|
||||
|
||||
invalid_api_key
|
||||
end
|
||||
|
||||
def current_ability
|
||||
Spree::Ability.new(current_api_user)
|
||||
end
|
||||
|
||||
def api_key
|
||||
request.headers["X-Api-Token"] || params[:token]
|
||||
end
|
||||
|
||||
def error_during_processing(exception)
|
||||
Bugsnag.notify(exception)
|
||||
|
||||
if Rails.env.development? || Rails.env.test?
|
||||
render status: :unprocessable_entity,
|
||||
json: json_api_error(exception.message, meta: exception.backtrace)
|
||||
else
|
||||
render status: :unprocessable_entity,
|
||||
json: json_api_error(I18n.t(:unknown_error, scope: "api"))
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_pagination(exception)
|
||||
render status: :unprocessable_entity,
|
||||
json: json_api_error(exception.message)
|
||||
end
|
||||
|
||||
def invalid_resource!(resource = nil)
|
||||
render status: :unprocessable_entity,
|
||||
json: json_api_invalid(
|
||||
I18n.t(:invalid_resource, scope: "api"),
|
||||
resource&.errors
|
||||
)
|
||||
end
|
||||
|
||||
def invalid_api_key
|
||||
render status: :unauthorized,
|
||||
json: json_api_error(I18n.t(:invalid_api_key, key: api_key, scope: "api"))
|
||||
end
|
||||
|
||||
def unauthorized
|
||||
render status: :unauthorized,
|
||||
json: json_api_error(I18n.t(:unauthorized, scope: "api"))
|
||||
end
|
||||
|
||||
def not_found
|
||||
render status: :not_found,
|
||||
json: json_api_error(I18n.t(:resource_not_found, scope: "api"))
|
||||
end
|
||||
|
||||
def json_api_error(message, **options)
|
||||
{ errors: [{ detail: message }] }.merge(options)
|
||||
end
|
||||
|
||||
def json_api_invalid(message, errors)
|
||||
error_response = { errors: [{ detail: message }] }
|
||||
error_response.merge!(meta: { validation_errors: errors.to_a }) if errors.any?
|
||||
error_response
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
81
app/controllers/api/v1/customers_controller.rb
Normal file
81
app/controllers/api/v1/customers_controller.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class CustomersController < Api::V1::BaseController
|
||||
skip_authorization_check only: :index
|
||||
|
||||
before_action :set_customer, only: [:show, :update, :destroy]
|
||||
before_action :authorize_action, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@pagy, customers = pagy(search_customers, pagy_options)
|
||||
|
||||
render json: Api::V1::CustomerSerializer.new(customers, pagination_options)
|
||||
end
|
||||
|
||||
def show
|
||||
render json: Api::V1::CustomerSerializer.new(@customer)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize! :update, Enterprise.find(customer_params[:enterprise_id])
|
||||
@customer = Customer.new(customer_params)
|
||||
|
||||
if @customer.save
|
||||
render json: Api::V1::CustomerSerializer.new(@customer), status: :created
|
||||
else
|
||||
invalid_resource! @customer
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @customer.update(customer_params)
|
||||
render json: Api::V1::CustomerSerializer.new(@customer)
|
||||
else
|
||||
invalid_resource! @customer
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @customer.destroy
|
||||
render json: Api::V1::CustomerSerializer.new(@customer)
|
||||
else
|
||||
invalid_resource! @customer
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_customer
|
||||
@customer = Customer.find(params[:id])
|
||||
end
|
||||
|
||||
def authorize_action
|
||||
authorize! action_name.to_sym, @customer
|
||||
end
|
||||
|
||||
def search_customers
|
||||
customers = visible_customers
|
||||
customers = customers.where(enterprise_id: params[:enterprise_id]) if params[:enterprise_id]
|
||||
customers.ransack(params[:q]).result
|
||||
end
|
||||
|
||||
def visible_customers
|
||||
current_api_user.customers.or(
|
||||
Customer.where(enterprise_id: editable_enterprises)
|
||||
)
|
||||
end
|
||||
|
||||
def customer_params
|
||||
params.require(:customer).permit(:email, :enterprise_id)
|
||||
end
|
||||
|
||||
def editable_enterprises
|
||||
OpenFoodNetwork::Permissions.new(current_api_user).editable_enterprises.select(:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
74
app/controllers/concerns/json_api_pagination.rb
Normal file
74
app/controllers/concerns/json_api_pagination.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module JsonApiPagination
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
DEFAULT_PER_PAGE = 50
|
||||
MAX_PER_PAGE = 200
|
||||
|
||||
def pagination_options
|
||||
{
|
||||
is_collection: true,
|
||||
meta: meta_options,
|
||||
links: links_options,
|
||||
}
|
||||
end
|
||||
|
||||
def pagy_options
|
||||
{ items: final_per_page_value }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def meta_options
|
||||
{
|
||||
pagination: {
|
||||
results: @pagy.count,
|
||||
pages: total_pages,
|
||||
page: current_page,
|
||||
per_page: final_per_page_value
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def links_options
|
||||
{
|
||||
self: pagination_url(current_page),
|
||||
first: pagination_url(1),
|
||||
prev: pagination_url(previous_page),
|
||||
next: pagination_url(next_page),
|
||||
last: pagination_url(total_pages)
|
||||
}
|
||||
end
|
||||
|
||||
def pagination_url(page_number)
|
||||
return if page_number.nil?
|
||||
|
||||
url_for(only_path: false, params: request.query_parameters.merge(page: page_number))
|
||||
end
|
||||
|
||||
# User-specified value, or DEFAULT_PER_PAGE, capped at MAX_PER_PAGE
|
||||
def final_per_page_value
|
||||
(params[:per_page] || DEFAULT_PER_PAGE).to_i.clamp(1, MAX_PER_PAGE)
|
||||
end
|
||||
|
||||
def current_page
|
||||
(params[:page] || 1).to_i
|
||||
end
|
||||
|
||||
def total_pages
|
||||
@pagy.pages
|
||||
end
|
||||
|
||||
def previous_page
|
||||
return nil if current_page < 2
|
||||
|
||||
current_page - 1
|
||||
end
|
||||
|
||||
def next_page
|
||||
return nil if current_page >= total_pages
|
||||
|
||||
current_page + 1
|
||||
end
|
||||
end
|
||||
26
app/json_schemas/customer_schema.rb
Normal file
26
app/json_schemas/customer_schema.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CustomerSchema < JsonApiSchema
|
||||
def self.object_name
|
||||
"customer"
|
||||
end
|
||||
|
||||
def self.attributes
|
||||
{
|
||||
id: { type: :integer, example: 1 },
|
||||
enterprise_id: { type: :integer, example: 2 },
|
||||
first_name: { type: :string, nullable: true, example: "Alice" },
|
||||
last_name: { type: :string, nullable: true, example: "Springs" },
|
||||
code: { type: :string, nullable: true, example: "BUYER1" },
|
||||
email: { type: :string, example: "alice@example.com" }
|
||||
}
|
||||
end
|
||||
|
||||
def self.required_attributes
|
||||
[:enterprise_id, :email]
|
||||
end
|
||||
|
||||
def self.relationships
|
||||
[:enterprise]
|
||||
end
|
||||
end
|
||||
24
app/json_schemas/errors_schema.rb
Normal file
24
app/json_schemas/errors_schema.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ErrorsSchema
|
||||
def self.schema
|
||||
{
|
||||
type: :object,
|
||||
properties: {
|
||||
errors: {
|
||||
type: :array,
|
||||
items: {
|
||||
type: :object,
|
||||
properties: {
|
||||
title: { type: :string },
|
||||
detail: { type: :string },
|
||||
source: { type: :object }
|
||||
},
|
||||
required: [:detail]
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [:errors]
|
||||
}
|
||||
end
|
||||
end
|
||||
114
app/json_schemas/json_api_schema.rb
Normal file
114
app/json_schemas/json_api_schema.rb
Normal file
@@ -0,0 +1,114 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class JsonApiSchema
|
||||
class << self
|
||||
def attributes
|
||||
{}
|
||||
end
|
||||
|
||||
def required_attributes
|
||||
[]
|
||||
end
|
||||
|
||||
def relationships
|
||||
[]
|
||||
end
|
||||
|
||||
def all_attributes
|
||||
attributes.keys
|
||||
end
|
||||
|
||||
def schema(options = {})
|
||||
{
|
||||
type: :object,
|
||||
properties: {
|
||||
data: {
|
||||
type: :object,
|
||||
properties: data_properties(**options)
|
||||
},
|
||||
meta: { type: :object },
|
||||
links: { type: :object }
|
||||
},
|
||||
required: [:data]
|
||||
}
|
||||
end
|
||||
|
||||
def collection(options)
|
||||
{
|
||||
type: :object,
|
||||
properties: {
|
||||
data: {
|
||||
type: :array,
|
||||
items: {
|
||||
type: :object,
|
||||
properties: data_properties(**options)
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
type: :object,
|
||||
properties: {
|
||||
pagination: {
|
||||
type: :object,
|
||||
properties: {
|
||||
results: { type: :integer, example: 250 },
|
||||
pages: { type: :integer, example: 5 },
|
||||
page: { type: :integer, example: 2 },
|
||||
per_page: { type: :integer, example: 50 },
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [:pagination]
|
||||
},
|
||||
links: {
|
||||
type: :object,
|
||||
properties: {
|
||||
self: { type: :string },
|
||||
first: { type: :string },
|
||||
prev: { type: :string, nullable: true },
|
||||
next: { type: :string, nullable: true },
|
||||
last: { type: :string }
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [:data, :meta, :links]
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def data_properties(require_all: false)
|
||||
required = require_all ? all_attributes : required_attributes
|
||||
|
||||
{
|
||||
id: { type: :string, example: "1" },
|
||||
type: { type: :string, example: object_name },
|
||||
attributes: {
|
||||
type: :object,
|
||||
properties: attributes,
|
||||
required: required
|
||||
},
|
||||
relationships: {
|
||||
type: :object,
|
||||
properties: relationships.to_h do |name|
|
||||
[
|
||||
name,
|
||||
relationship_schema(name)
|
||||
]
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def relationship_schema(name)
|
||||
if is_singular?(name)
|
||||
RelationshipSchema.schema(name)
|
||||
else
|
||||
RelationshipSchema.collection(name)
|
||||
end
|
||||
end
|
||||
|
||||
def is_singular?(name)
|
||||
name.to_s.singularize == name.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/json_schemas/relationship_schema.rb
Normal file
48
app/json_schemas/relationship_schema.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RelationshipSchema
|
||||
def self.schema(resource_name = nil)
|
||||
{
|
||||
type: :object,
|
||||
properties: {
|
||||
data: {
|
||||
type: :object,
|
||||
properties: {
|
||||
id: { type: :string },
|
||||
type: { type: :string, example: resource_name }
|
||||
}
|
||||
},
|
||||
links: {
|
||||
type: :object,
|
||||
properties: {
|
||||
related: { type: :string }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def self.collection(resource_name = nil)
|
||||
{
|
||||
type: :object,
|
||||
properties: {
|
||||
data: {
|
||||
type: :array,
|
||||
items: {
|
||||
type: :object,
|
||||
properties: {
|
||||
id: { type: :string },
|
||||
type: { type: :string, example: resource_name }
|
||||
}
|
||||
}
|
||||
},
|
||||
links: {
|
||||
type: :object,
|
||||
properties: {
|
||||
related: { type: :string }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
13
app/serializers/api/v1/base_serializer.rb
Normal file
13
app/serializers/api/v1/base_serializer.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class BaseSerializer
|
||||
include JSONAPI::Serializer
|
||||
|
||||
def self.url_helpers
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/serializers/api/v1/customer_serializer.rb
Normal file
15
app/serializers/api/v1/customer_serializer.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class CustomerSerializer < BaseSerializer
|
||||
attributes :id, :enterprise_id, :first_name, :last_name, :code, :email
|
||||
|
||||
belongs_to :enterprise, links: {
|
||||
related: ->(object) {
|
||||
url_helpers.api_v1_enterprise_url(id: object.enterprise_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/serializers/api/v1/enterprise_serializer.rb
Normal file
15
app/serializers/api/v1/enterprise_serializer.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class EnterpriseSerializer < BaseSerializer
|
||||
attributes :id, :name
|
||||
|
||||
has_many :customers, links: {
|
||||
related: ->(object) {
|
||||
url_helpers.api_v1_enterprise_customers_url(enterprise_id: object.id)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -165,7 +165,6 @@ en:
|
||||
no_default_card: "^No default card available for this customer"
|
||||
shipping_method:
|
||||
not_available_to_shop: "is not available to %{shop}"
|
||||
|
||||
card_details: "Card details"
|
||||
card_type: "Card type"
|
||||
cardholder_name: "Cardholder name"
|
||||
@@ -1410,6 +1409,11 @@ en:
|
||||
# API
|
||||
#
|
||||
api:
|
||||
unknown_error: "Something went wrong. Our team has been notified."
|
||||
invalid_api_key: "Invalid API key (%{key}) specified."
|
||||
unauthorized: "You are not authorized to perform that action."
|
||||
invalid_resource: "Invalid resource. Please fix errors and try again."
|
||||
resource_not_found: "The resource you were looking for could not be found."
|
||||
enterprise_logo:
|
||||
destroy_attachment_does_not_exist: "Logo does not exist"
|
||||
enterprise_promo_image:
|
||||
|
||||
@@ -87,6 +87,16 @@ Openfoodnetwork::Application.routes.draw do
|
||||
constraints: lambda { |_| Flipper.enabled?(:api_reports) }
|
||||
end
|
||||
|
||||
unless Rails.env.production?
|
||||
namespace :v1 do
|
||||
resources :customers
|
||||
|
||||
resources :enterprises do
|
||||
resources :customers, only: :index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
match '*path', to: redirect(path: "/api/v0/%{path}"), via: :all, constraints: { path: /(?!v[0-9]).+/ }
|
||||
end
|
||||
end
|
||||
|
||||
245
spec/requests/api/v1/customers_spec.rb
Normal file
245
spec/requests/api/v1/customers_spec.rb
Normal file
@@ -0,0 +1,245 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "swagger_helper"
|
||||
|
||||
describe "Customers", type: :request do
|
||||
let!(:enterprise1) { create(:enterprise) }
|
||||
let!(:enterprise2) { create(:enterprise) }
|
||||
let!(:customer1) { create(:customer, enterprise: enterprise1) }
|
||||
let!(:customer2) { create(:customer, enterprise: enterprise1) }
|
||||
let!(:customer3) { create(:customer, enterprise: enterprise2) }
|
||||
|
||||
before { login_as enterprise1.owner }
|
||||
|
||||
path "/api/v1/customers" do
|
||||
get "List customers" do
|
||||
tags "Customers"
|
||||
parameter name: :enterprise_id, in: :query, type: :string
|
||||
produces "application/json"
|
||||
|
||||
response "200", "Customers list" do
|
||||
param(:enterprise_id) { enterprise1.id }
|
||||
schema "$ref": "#/components/schemas/resources/customers_collection"
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
|
||||
describe "returning results based on permissions" do
|
||||
context "as guest user" do
|
||||
before { login_as nil }
|
||||
|
||||
it "returns no customers" do
|
||||
get "/api/v1/customers"
|
||||
expect(json_response_ids).to eq []
|
||||
end
|
||||
|
||||
it "returns not even customers without user id" do
|
||||
customer3.update!(user_id: nil)
|
||||
|
||||
get "/api/v1/customers"
|
||||
expect(json_response_ids).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise owner" do
|
||||
before { login_as enterprise1.owner }
|
||||
|
||||
it "returns customers of enterprises the user manages" do
|
||||
get "/api/v1/customers"
|
||||
expect(json_response_ids).to eq [customer1.id.to_s, customer2.id.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
context "as another enterprise owner" do
|
||||
before { login_as enterprise2.owner }
|
||||
|
||||
it "returns customers of enterprises the user manages" do
|
||||
get "/api/v1/customers"
|
||||
expect(json_response_ids).to eq [customer3.id.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
context "with ransack params searching for specific customers" do
|
||||
before { login_as enterprise2.owner }
|
||||
|
||||
it "does not show results the user doesn't have permissions to view" do
|
||||
get "/api/v1/customers", params: { q: { id_eq: customer2.id } }
|
||||
|
||||
expect(json_response_ids).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "pagination" do
|
||||
it "renders the first page" do
|
||||
get "/api/v1/customers", params: { page: "1" }
|
||||
expect(json_response_ids).to eq [customer1.id.to_s, customer2.id.to_s]
|
||||
end
|
||||
|
||||
it "renders the second page" do
|
||||
get "/api/v1/customers", params: { page: "2", per_page: "1" }
|
||||
expect(json_response_ids).to eq [customer2.id.to_s]
|
||||
end
|
||||
|
||||
it "renders beyond the available pages" do
|
||||
get "/api/v1/customers", params: { page: "2" }
|
||||
expect(json_response_ids).to eq []
|
||||
end
|
||||
|
||||
it "informs about invalid pages" do
|
||||
get "/api/v1/customers", params: { page: "0" }
|
||||
expect(json_response_ids).to eq nil
|
||||
expect(json_error_detail).to eq 'expected :page >= 1; got "0"'
|
||||
end
|
||||
end
|
||||
|
||||
post "Create customer" do
|
||||
tags "Customers"
|
||||
consumes "application/json"
|
||||
produces "application/json"
|
||||
|
||||
parameter name: :customer, in: :body, schema: {
|
||||
type: :object,
|
||||
properties: CustomerSchema.attributes.except(:id),
|
||||
required: CustomerSchema.required_attributes
|
||||
}
|
||||
|
||||
response "201", "Customer created" do
|
||||
param(:customer) do
|
||||
{
|
||||
email: "test@example.com",
|
||||
enterprise_id: enterprise1.id.to_s
|
||||
}
|
||||
end
|
||||
schema "$ref": "#/components/schemas/resources/customer"
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response "422", "Unprocessable entity" do
|
||||
param(:customer) { {} }
|
||||
schema "$ref": "#/components/schemas/error_response"
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
path "/api/v1/customers/{id}" do
|
||||
get "Show customer" do
|
||||
tags "Customers"
|
||||
parameter name: :id, in: :path, type: :string
|
||||
produces "application/json"
|
||||
|
||||
response "200", "Customer" do
|
||||
param(:id) { customer1.id }
|
||||
schema "$ref": "#/components/schemas/resources/customer"
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response "404", "Not found" do
|
||||
param(:id) { 0 }
|
||||
schema "$ref": "#/components/schemas/error_response"
|
||||
|
||||
run_test! do
|
||||
expect(json_error_detail).to eq "The resource you were looking for could not be found."
|
||||
end
|
||||
end
|
||||
|
||||
context "without authentication" do
|
||||
before { logout }
|
||||
|
||||
response "401", "Unauthorized" do
|
||||
param(:id) { customer1.id }
|
||||
schema "$ref": "#/components/schemas/error_response"
|
||||
|
||||
run_test! do
|
||||
expect(json_error_detail).to eq "You are not authorized to perform that action."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "related records" do
|
||||
it "serializes the enterprise relationship" do
|
||||
expected_enterprise_data = {
|
||||
"data" => {
|
||||
"id" => customer1.enterprise_id.to_s,
|
||||
"type" => "enterprise"
|
||||
},
|
||||
"links" => {
|
||||
"related" => "http://test.host/api/v1/enterprises/#{customer1.enterprise_id}"
|
||||
}
|
||||
}
|
||||
|
||||
get "/api/v1/customers/#{customer1.id}"
|
||||
expect(json_response[:data][:relationships][:enterprise]).to eq(expected_enterprise_data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
put "Update customer" do
|
||||
tags "Customers"
|
||||
parameter name: :id, in: :path, type: :string
|
||||
consumes "application/json"
|
||||
produces "application/json"
|
||||
|
||||
parameter name: :customer, in: :body, schema: {
|
||||
type: :object,
|
||||
properties: CustomerSchema.attributes,
|
||||
required: CustomerSchema.required_attributes
|
||||
}
|
||||
|
||||
response "200", "Customer updated" do
|
||||
param(:id) { customer1.id }
|
||||
param(:customer) do
|
||||
{
|
||||
id: customer1.id.to_s,
|
||||
email: "test@example.com",
|
||||
enterprise_id: enterprise1.id.to_s
|
||||
}
|
||||
end
|
||||
schema "$ref": "#/components/schemas/resources/customer"
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response "422", "Unprocessable entity" do
|
||||
param(:id) { customer1.id }
|
||||
param(:customer) { {} }
|
||||
schema "$ref": "#/components/schemas/error_response"
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
|
||||
delete "Delete customer" do
|
||||
tags "Customers"
|
||||
parameter name: :id, in: :path, type: :string
|
||||
produces "application/json"
|
||||
|
||||
response "200", "Customer deleted" do
|
||||
param(:id) { customer1.id }
|
||||
schema "$ref": "#/components/schemas/resources/customer"
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
path "/api/v1/enterprises/{enterprise_id}/customers" do
|
||||
get "List customers of an enterprise" do
|
||||
tags "Customers", "Enterprises"
|
||||
parameter name: :enterprise_id, in: :path, type: :string, required: true
|
||||
produces "application/json"
|
||||
|
||||
response "200", "Customers list" do
|
||||
param(:enterprise_id) { enterprise1.id }
|
||||
schema "$ref": "#/components/schemas/resources/customers_collection"
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -14,6 +14,14 @@ module OpenFoodNetwork
|
||||
end
|
||||
end
|
||||
|
||||
def json_response_ids
|
||||
json_response[:data]&.map{ |item| item["id"] }
|
||||
end
|
||||
|
||||
def json_error_detail
|
||||
json_response[:errors][0][:detail]
|
||||
end
|
||||
|
||||
def assert_unauthorized!
|
||||
expect(json_response).to eq("error" => "You are not authorized to perform that action.")
|
||||
expect(response.status).to eq 401
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include Devise::Test::IntegrationHelpers, type: :request
|
||||
config.include OpenFoodNetwork::ApiHelper, type: :request
|
||||
|
||||
# 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
|
||||
@@ -21,6 +24,35 @@ RSpec.configure do |config|
|
||||
title: 'API V1',
|
||||
version: 'v1'
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
error_response: ErrorsSchema.schema,
|
||||
resources: {
|
||||
customer: CustomerSchema.schema(require_all: true),
|
||||
customers_collection: CustomerSchema.collection(require_all: true)
|
||||
}
|
||||
},
|
||||
securitySchemas: {
|
||||
api_key_header: {
|
||||
type: :apiKey,
|
||||
name: 'X-Api-Token',
|
||||
in: :header,
|
||||
description: "Authenticates via API key passed in specified header"
|
||||
},
|
||||
api_key_param: {
|
||||
type: :apiKey,
|
||||
name: 'token',
|
||||
in: :query,
|
||||
description: "Authenticates via API key passed in specified query param"
|
||||
},
|
||||
session: {
|
||||
type: :http,
|
||||
name: '_ofn_session',
|
||||
in: :cookie,
|
||||
description: "Authenticates using the current user's session if logged in"
|
||||
},
|
||||
}
|
||||
},
|
||||
paths: {},
|
||||
servers: [
|
||||
{ url: "/" }
|
||||
@@ -41,3 +73,10 @@ RSpec.configure do |config|
|
||||
# Defaults to json. Accepts ':json' and ':yaml'.
|
||||
config.swagger_format = :yaml
|
||||
end
|
||||
|
||||
module RswagExtension
|
||||
def param(args, &block)
|
||||
public_send(:let, args) { instance_eval(&block) }
|
||||
end
|
||||
end
|
||||
Rswag::Specs::ExampleGroupHelpers.prepend RswagExtension
|
||||
|
||||
@@ -3,6 +3,395 @@ openapi: 3.0.1
|
||||
info:
|
||||
title: API V1
|
||||
version: v1
|
||||
paths: {}
|
||||
components:
|
||||
schemas:
|
||||
error_response:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
detail:
|
||||
type: string
|
||||
source:
|
||||
type: object
|
||||
required:
|
||||
- detail
|
||||
required:
|
||||
- errors
|
||||
resources:
|
||||
customer:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: '1'
|
||||
type:
|
||||
type: string
|
||||
example: customer
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
enterprise_id:
|
||||
type: integer
|
||||
example: 2
|
||||
first_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Alice
|
||||
last_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Springs
|
||||
code:
|
||||
type: string
|
||||
nullable: true
|
||||
example: BUYER1
|
||||
email:
|
||||
type: string
|
||||
example: alice@example.com
|
||||
required:
|
||||
- id
|
||||
- enterprise_id
|
||||
- first_name
|
||||
- last_name
|
||||
- code
|
||||
- email
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
enterprise:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
example: enterprise
|
||||
links:
|
||||
type: object
|
||||
properties:
|
||||
related:
|
||||
type: string
|
||||
meta:
|
||||
type: object
|
||||
links:
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
customers_collection:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: '1'
|
||||
type:
|
||||
type: string
|
||||
example: customer
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
enterprise_id:
|
||||
type: integer
|
||||
example: 2
|
||||
first_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Alice
|
||||
last_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Springs
|
||||
code:
|
||||
type: string
|
||||
nullable: true
|
||||
example: BUYER1
|
||||
email:
|
||||
type: string
|
||||
example: alice@example.com
|
||||
required:
|
||||
- id
|
||||
- enterprise_id
|
||||
- first_name
|
||||
- last_name
|
||||
- code
|
||||
- email
|
||||
relationships:
|
||||
type: object
|
||||
properties:
|
||||
enterprise:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
example: enterprise
|
||||
links:
|
||||
type: object
|
||||
properties:
|
||||
related:
|
||||
type: string
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
type: integer
|
||||
example: 250
|
||||
pages:
|
||||
type: integer
|
||||
example: 5
|
||||
page:
|
||||
type: integer
|
||||
example: 2
|
||||
per_page:
|
||||
type: integer
|
||||
example: 50
|
||||
required:
|
||||
- pagination
|
||||
links:
|
||||
type: object
|
||||
properties:
|
||||
self:
|
||||
type: string
|
||||
first:
|
||||
type: string
|
||||
prev:
|
||||
type: string
|
||||
nullable: true
|
||||
next:
|
||||
type: string
|
||||
nullable: true
|
||||
last:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
- links
|
||||
securitySchemas:
|
||||
api_key_header:
|
||||
type: apiKey
|
||||
name: X-Api-Token
|
||||
in: header
|
||||
description: Authenticates via API key passed in specified header
|
||||
api_key_param:
|
||||
type: apiKey
|
||||
name: token
|
||||
in: query
|
||||
description: Authenticates via API key passed in specified query param
|
||||
session:
|
||||
type: http
|
||||
name: _ofn_session
|
||||
in: cookie
|
||||
description: Authenticates using the current user's session if logged in
|
||||
paths:
|
||||
"/api/v1/customers":
|
||||
get:
|
||||
summary: List customers
|
||||
tags:
|
||||
- Customers
|
||||
parameters:
|
||||
- name: enterprise_id
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Customers list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/resources/customers_collection"
|
||||
post:
|
||||
summary: Create customer
|
||||
tags:
|
||||
- Customers
|
||||
parameters: []
|
||||
responses:
|
||||
'201':
|
||||
description: Customer created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/resources/customer"
|
||||
'422':
|
||||
description: Unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/error_response"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enterprise_id:
|
||||
type: integer
|
||||
example: 2
|
||||
first_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Alice
|
||||
last_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Springs
|
||||
code:
|
||||
type: string
|
||||
nullable: true
|
||||
example: BUYER1
|
||||
email:
|
||||
type: string
|
||||
example: alice@example.com
|
||||
required:
|
||||
- enterprise_id
|
||||
- email
|
||||
"/api/v1/customers/{id}":
|
||||
get:
|
||||
summary: Show customer
|
||||
tags:
|
||||
- Customers
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Customer
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/resources/customer"
|
||||
'404':
|
||||
description: Not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/error_response"
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/error_response"
|
||||
put:
|
||||
summary: Update customer
|
||||
tags:
|
||||
- Customers
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Customer updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/resources/customer"
|
||||
'422':
|
||||
description: Unprocessable entity
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/error_response"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
enterprise_id:
|
||||
type: integer
|
||||
example: 2
|
||||
first_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Alice
|
||||
last_name:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Springs
|
||||
code:
|
||||
type: string
|
||||
nullable: true
|
||||
example: BUYER1
|
||||
email:
|
||||
type: string
|
||||
example: alice@example.com
|
||||
required:
|
||||
- enterprise_id
|
||||
- email
|
||||
delete:
|
||||
summary: Delete customer
|
||||
tags:
|
||||
- Customers
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Customer deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/resources/customer"
|
||||
"/api/v1/enterprises/{enterprise_id}/customers":
|
||||
get:
|
||||
summary: List customers of an enterprise
|
||||
tags:
|
||||
- Customers
|
||||
- Enterprises
|
||||
parameters:
|
||||
- name: enterprise_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Customers list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/resources/customers_collection"
|
||||
servers:
|
||||
- url: "/"
|
||||
|
||||
Reference in New Issue
Block a user