diff --git a/.rubocop_styleguide.yml b/.rubocop_styleguide.yml index 31e65d089b..500b642c70 100644 --- a/.rubocop_styleguide.yml +++ b/.rubocop_styleguide.yml @@ -27,11 +27,16 @@ Metrics/BlockLength: "class_eval", "collection", "context", + "delete", "describe", "feature", + "get", "it", "member", "namespace", + "path", + "post", + "put", "resource", "resources", "scenario", diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 14cbe53446..1ba490f5ec 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -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' diff --git a/app/json_schemas/customer_schema.rb b/app/json_schemas/customer_schema.rb new file mode 100644 index 0000000000..5e7cb94e21 --- /dev/null +++ b/app/json_schemas/customer_schema.rb @@ -0,0 +1,22 @@ +# 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 +end diff --git a/app/json_schemas/errors_schema.rb b/app/json_schemas/errors_schema.rb new file mode 100644 index 0000000000..590dda849d --- /dev/null +++ b/app/json_schemas/errors_schema.rb @@ -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 diff --git a/app/json_schemas/json_api_schema.rb b/app/json_schemas/json_api_schema.rb new file mode 100644 index 0000000000..1bc13098ef --- /dev/null +++ b/app/json_schemas/json_api_schema.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class JsonApiSchema + class << self + def attributes + {} + end + + def required_attributes + [] + end + + def all_attributes + attributes.keys + end + + def schema(options = {}) + { + type: :object, + properties: { + data: { + type: :object, + properties: data_properties(**options) + }, + meta: { type: :object } + }, + required: [:data] + } + end + + def collection(options) + { + type: :object, + properties: { + data: { + type: :array, + items: { + type: :object, + properties: data_properties(**options) + } + }, + meta: { type: :object } + }, + required: [:data] + } + 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 } + } + end + end +end diff --git a/app/serializers/api/v1/customer_serializer.rb b/app/serializers/api/v1/customer_serializer.rb index e87391f997..cea4d53d2e 100644 --- a/app/serializers/api/v1/customer_serializer.rb +++ b/app/serializers/api/v1/customer_serializer.rb @@ -5,7 +5,7 @@ module Api class CustomerSerializer include JSONAPI::Serializer - attributes :id, :enterprise_id, :name, :code, :email + attributes :id, :enterprise_id, :first_name, :last_name, :code, :email belongs_to :enterprise, record_type: :enterprise, serializer: :id end diff --git a/spec/requests/api/v1/customers_spec.rb b/spec/requests/api/v1/customers_spec.rb new file mode 100644 index 0000000000..a97667489d --- /dev/null +++ b/spec/requests/api/v1/customers_spec.rb @@ -0,0 +1,166 @@ +# 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 CustomerSchema.collection(require_all: true) + + run_test! + end + end + + describe "returning results based on permissions" do + 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 + 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 CustomerSchema.schema(require_all: true) + + run_test! + end + + response "422", "Unprocessable entity" do + param(:customer) { {} } + schema ErrorsSchema.schema + + 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 CustomerSchema.schema(require_all: true) + + run_test! + end + + response "404", "Not found" do + param(:id) { 0 } + schema ErrorsSchema.schema + + run_test! do + expect(json_error_detail).to eq "The resource you were looking for could not be found." + 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 CustomerSchema.schema(require_all: true) + + run_test! + end + + response "422", "Unprocessable entity" do + param(:id) { customer1.id } + param(:customer) { {} } + schema ErrorsSchema.schema + + 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 CustomerSchema.schema(require_all: true) + + 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 CustomerSchema.collection(require_all: true) + + run_test! + end + end + end +end diff --git a/spec/support/api_helper.rb b/spec/support/api_helper.rb index 9ac5c7a1cb..d4591ece3d 100644 --- a/spec/support/api_helper.rb +++ b/spec/support/api_helper.rb @@ -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 diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index d47d6a49ae..9bcd2c4064 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -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,12 @@ RSpec.configure do |config| title: 'API V1', version: 'v1' }, + components: { + schemas: { + error_response: ErrorsSchema.schema, + customer: CustomerSchema.schema + } + }, paths: {}, servers: [ { url: "/" } @@ -41,3 +50,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