Compare commits

..

15 Commits

Author SHA1 Message Date
google-labs-jules[bot]
111bdb2062 feat: Improve Swagger documentation
This commit improves the Swagger documentation by using rswag to generate it from the request specs.

The following changes were made:
- All request specs in `spec/requests/api/v1/` were updated to use the rswag DSL.
- The `spec/swagger_helper.rb` was configured to generate a `swagger.json` file.
- The `config/database.yml` was updated to use environment variables, which makes it easier to use in different environments.
- The generated `swagger.json` file is now based on the OpenAPI 3.0 specification.
2025-09-10 12:36:32 +00:00
Daniel O'Connor
99478e3920 Rubocop (#4242) 2025-09-10 20:46:12 +09:30
Daniel O'Connor
a2f05097af Merge branch 'mainline' into dev 2025-09-10 20:02:31 +09:30
Daniel O'Connor
e5bf9d98e6 Rubocop (#4241) 2025-09-10 19:56:12 +09:30
Daniel O'Connor
7988080054 Update .rubocop.yml 2025-09-10 19:52:44 +09:30
google-labs-jules[bot]
02db5b8130 Add API token generation, authentication, and CRUD for a number of the API resources (#4237)
* feat: Add API token generation and authentication

This commit introduces API token generation and authentication for write operations.

- Adds a section to the user's profile edit page to generate and display an API token.
- Reuses the `authentications` table to store the API token, avoiding the need for a database migration.
- Implements token-based authentication for the API using the `Authorization: Token token=...` header.
- Enables write operations for all API resources and ensures they are protected by the new authentication mechanism.
- Adds feature and request specs to test the new functionality.

* feat: Add API token generation and authentication

This commit introduces API token generation and authentication for write operations.

- Adds a section to the user's profile edit page to generate and display an API token.
- Reuses the `authentications` table to store the API token, avoiding the need for a database migration.
- Implements token-based authentication for the API using the `Authorization: Token token=...` header.
- Enables write operations for all API resources and ensures they are protected by the new authentication mechanism.
- Adds feature and request specs to test the new functionality.

* Mark as editable

* Refactor

* WIP - Authentication

* Implement more test coverage

* Split 401 and 403

* Before Create hooks

* Update harvest specs, defaulting to the first plant part - this may not be right

* Update coverage

* Update coverage

* Rubocop

* Rubocop

* Rubocop

* Fix coverage

* For now, mark photos immutable again

* Fix specs

* Fix specs

* Rubocop

* Fix specs

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
2025-09-10 19:50:06 +09:30
Daniel O'Connor
cf8380029a Rubocop 2025-09-10 10:19:08 +00:00
Daniel O'Connor
eefda21d1a Merge pull request #4226 from Growstuff/dev
Release 70
2025-09-09 22:23:27 +09:30
Daniel O'Connor
cfc486ce86 Merge pull request #4219 from Growstuff/dev
release 69.1
2025-09-07 15:07:58 +09:30
Daniel O'Connor
a900c2eb2f Merge pull request #4185 from Growstuff/dev
Release 69
2025-09-07 14:55:40 +09:30
Daniel O'Connor
29543d1d37 Release 68 (#4170)
* Improve menu again

* Fix crop button annoyance

* feat: Add PWA installation instructions to homepage

This commit adds instructions for mobile users on how to install the Growstuff website as a Progressive Web App (PWA).

The changes include:
- A new section on the homepage with instructions for both iOS and Android devices. This section is only visible to logged-out users.
- New translations for the instructions in the `en.yml` locale file.
- Basic styling for the new section.
- Updated feature tests to verify the new section's visibility.

* Restyle slightly

* Styling

* Github lure

* Make links bold, not all of the stats text

* Adjust specs

* Fix width of ready to harvest

* Update spec/features/home/home_spec.rb

* Fix display

* Fix text display wonkyness

* Merge pull request #4173 from Growstuff/translate-confirm

Garden Delete - Extract strings and fix missing translation bug

* Seeds for trade - avoid showing expired seeds on homepage. (#4176)

* Improve date visibility

* Ensure when seeding seeds, it's false

* Typo

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-08-31 15:23:16 +09:30
Daniel O'Connor
dfb791bf55 Merge pull request #4167 from Growstuff/dev
Release67, take 3
2025-08-30 01:16:01 +09:30
Daniel O'Connor
484797421e Merge pull request #4165 from Growstuff/dev
Release 67, attempt 2
2025-08-29 23:32:29 +09:30
Daniel O'Connor
a366d68c22 Merge pull request #4160 from Growstuff/dev
Release67 - September 2025?
2025-08-29 20:03:05 +09:30
Daniel O'Connor
e7dba3f0e9 Merge pull request #4147 from Growstuff/dev
August 24 Release (Release 66)
2025-08-24 17:02:03 +09:30
32 changed files with 3712 additions and 4142 deletions

View File

@@ -1,5 +1,5 @@
inherit_from: .rubocop_todo.yml
require:
plugins:
- rubocop-factory_bot
- rubocop-capybara
- rubocop-rails

View File

@@ -314,7 +314,7 @@ RSpec/MultipleExpectations:
# Offense count: 138
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:
Max: 14
Max: 20
# Offense count: 133
# Configuration parameters: EnforcedStyle, IgnoreSharedExamples.

View File

@@ -29,7 +29,7 @@ class ActivitiesController < DataController
def new
@activity = Activity.new(
owner: current_member,
owner: current_member,
due_date: Date.today
)
if params[:garden_id]

View File

@@ -2,8 +2,6 @@
module Api
module V1
# This controller is intentionally empty.
# The `jsonapi-resources` gem provides the necessary actions.
class ActivitiesController < BaseController
end
end

View File

@@ -4,6 +4,40 @@ module Api
module V1
class BaseController < JSONAPI::ResourceController
abstract
protect_from_forgery with: :null_session
before_action :authenticate_member_from_token!
before_action :enforce_member_for_write_operations!, only: %i(create update destroy)
rescue_from CanCan::AccessDenied do
head :forbidden
end
def context
{
current_user: current_user,
current_ability: current_ability,
controller: self,
action: params[:action]
}
end
private
attr_reader :current_user
def enforce_member_for_write_operations!
head :unauthorized unless current_user
end
def authenticate_member_from_token!
authenticate_with_http_token do |token, _options|
auth = Authentication.find_by(token: token, provider: 'api')
if auth.present?
@current_user = auth.member
return true
end
end
end
end
end
end

View File

@@ -6,7 +6,7 @@ class RegistrationsController < Devise::RegistrationsController
prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect with recaptcha.
def edit
@flickr_auth = current_member.auth('flickr')
@flickr_auth = current_member.auth('flickr')
render "edit"
end
@@ -38,6 +38,12 @@ class RegistrationsController < Devise::RegistrationsController
end
end
def regenerate_api_token
current_member.regenerate_api_token
set_flash_message :notice, :api_token_regenerated
redirect_to edit_member_registration_path + '#apps'
end
def destroy
if @member.valid_password?(params.require(:member)[:current_password])
@member.discard

View File

@@ -24,6 +24,20 @@ class Member < ApplicationRecord
has_many :notifications, foreign_key: 'recipient_id', inverse_of: :recipient
has_many :sent_notifications, foreign_key: 'sender_id', inverse_of: :sender, class_name: "Notification"
has_many :authentications, dependent: :destroy
has_one :api_token, -> { where(provider: 'api') }, class_name: 'Authentication', dependent: :destroy
def api_token?
api_token.present?
end
def regenerate_api_token
api_token.destroy if api_token?
create_api_token(
provider: 'api',
uid: id,
token: SecureRandom.hex(16)
)
end
has_many :photos, inverse_of: :owner
has_many :likes, dependent: :destroy

View File

@@ -3,7 +3,9 @@
module Api
module V1
class ActivityResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :garden

View File

@@ -3,8 +3,7 @@
module Api
module V1
class CropResource < BaseResource
immutable
immutable # TODO: Re-evaluate this later
filter :approval_status, default: 'approved'
has_many :plantings

View File

@@ -3,7 +3,9 @@
module Api
module V1
class GardenResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_many :plantings

View File

@@ -3,11 +3,17 @@
module Api
module V1
class HarvestResource < BaseResource
immutable
before_save do
@model.owner = context[:current_user]
@model.crop_id = @model.planting.crop_id if @model.planting_id
@model.harvested_at = Time.zone.now if @model.harvested_at.blank?
@model.plant_part = PlantPart.first
end
has_one :crop
has_one :planting
has_one :owner, class_name: 'Member'
# has_one :plant_part
has_many :photos
attribute :harvested_at

View File

@@ -3,7 +3,10 @@
module Api
module V1
class PhotoResource < BaseResource
immutable
immutable # TODO: Re-evaluate this.
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_many :plantings

View File

@@ -3,7 +3,9 @@
module Api
module V1
class PlantingResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :garden
has_one :crop

View File

@@ -3,7 +3,9 @@
module Api
module V1
class SeedResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :crop

View File

@@ -1,6 +1,16 @@
# frozen_string_literal: true
class BaseResource < JSONAPI::Resource
immutable
abstract
[:create, :update, :remove].each do |action|
set_callback action, :before, :authorize
end
# Check authorisation for write operations.
# NOTE: At a later time, we may require API tokens for READ operations.
def authorize
# context[:action] is simply context[:controller].params[:action]
context[:current_ability].authorize! context[:action].to_sym, @model
end
end

View File

@@ -15,3 +15,16 @@
method: :delete, class: "remove btn btn-danger"
- else
= link_to 'Connect to Flickr', '/members/auth/flickr', class: 'btn'
%hr
.row
.col-md-12
%p
= image_tag "icons/post.svg", size: "32x32", alt: 'API logo'
- if current_member.api_token?
Your API token is
%code= current_member.api_token.token
= link_to "Regenerate", regenerate_api_token_path,
data: { confirm: "Are you sure? Your old token will stop working immediately." },
method: :post, class: "remove btn btn-danger"
- else
= link_to 'Generate API Token', regenerate_api_token_path, method: :post, class: 'btn btn-primary'

View File

@@ -1,31 +1,27 @@
development:
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: growstuff_dev
user: postgres
password: postgres
host: db
host: <%= ENV.fetch("DATABASE_HOST") { 'db' } %>
test:
adapter: postgresql
<<: *default
database: growstuff_test
user: postgres
password: postgres
host: db
host: <%= ENV.fetch("DATABASE_HOST") { 'db' } %>
production:
adapter: postgresql
database: growstuff_prod
pool: 5
timeout: 5000
username: growstuff
host: localhost
password: thisisnottherealpassword
<<: *default
url: <%= ENV['DATABASE_URL'] %>
staging:
adapter: postgresql
database: growstuff_prod
pool: 5
timeout: 5000
username: growstuff
host: localhost
password: thisisnottherealpassword
<<: *default
url: <%= ENV['DATABASE_URL'] %>

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true
class UnauthorisedError < JSONAPI::Error
end
JSONAPI.configure do |config|
# built in paginators are :none, :offset, :paged
config.default_paginator = :offset
config.default_page_size = 10
config.maximum_page_size = 100
config.exception_class_whitelist = [CanCan::AccessDenied, UnauthorisedError]
end

View File

@@ -54,6 +54,7 @@ en:
You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm
link to finalize confirming your new email address.
destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
api_token_regenerated: 'Your API token has been regenerated.'
unlocks:
send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
unlocked: 'Your account has been unlocked successfully. Please sign in to continue.'

View File

@@ -16,6 +16,7 @@ Rails.application.routes.draw do
}
devise_scope :member do
get '/members/unsubscribe/:message' => 'members#unsubscribe', as: 'unsubscribe_member'
post '/members/regenerate_api_token' => 'registrations#regenerate_api_token', as: 'regenerate_api_token'
end
match '/members/:id/finish_signup' => 'members#finish_signup', via: %i(get patch), as: :finish_signup

View File

@@ -36,21 +36,21 @@ namespace :wikidata do
aliases = wikidata_data['entities'][wikidata_id]['aliases']
aliases.each do |lang, values|
values.each do |value|
unless AlternateName.exists?(name: value['value'], language: lang, crop: crop)
AlternateName.create!(
name: value['value'],
language: lang,
crop: crop,
creator: creator
)
puts " Added alternate name: #{value['value']} (#{lang})"
end
next if AlternateName.exists?(name: value['value'], language: lang, crop: crop)
AlternateName.create!(
name: value['value'],
language: lang,
crop: crop,
creator: creator
)
puts " Added alternate name: #{value['value']} (#{lang})"
end
end
else
puts " Could not find Wikidata ID for #{crop.name}"
end
rescue => e
rescue StandardError => e
puts " Error processing crop #{crop.name}: #{e.message}"
end
end

View File

@@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
describe "member token management", :js do
include_context 'signed in member'
before do
visit edit_member_registration_path
click_on "Apps"
end
it "can generate an API token" do
expect(page).to have_no_content("Your API token is")
click_on "Generate API Token"
expect(page).to have_content("Your API token is")
member.reload
expect(member.api_token).to be_present
end
context "with an existing token" do
before do
member.regenerate_api_token
visit edit_member_registration_path
click_on "Apps"
end
it "can regenerate an API token" do
old_token = member.api_token.token
expect(page).to have_content("Your API token is")
accept_confirm do
click_on "Regenerate"
end
expect(page).to have_content("Your API token is")
expect(member.reload.api_token.token).not_to eq(old_token)
end
end
end

View File

@@ -1,56 +1,95 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Activities', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Activities API', type: :request do
path '/api/v1/activities' do
get 'Lists activities' do
tags 'Activities'
produces 'application/vnd.api+json'
parameter name: 'filter[owner-id]', in: :query, type: :string, required: false
parameter name: 'filter[garden-id]', in: :query, type: :string, required: false
parameter name: 'filter[planting-id]', in: :query, type: :string, required: false
parameter name: 'filter[category]', in: :query, type: :string, required: false
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:activity) { FactoryBot.create(:activity, garden: create(:garden), planting: create(:planting)) }
let!(:activity2) { FactoryBot.create(:activity) }
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string },
description: { type: :string },
category: { type: :string },
finished: { type: :boolean },
'due-date': { type: :string, format: 'date-time' }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
garden: { '$ref' => '#/components/schemas/relationship' },
planting: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
it '#index' do
get('/api/v1/activities', params: {}, headers:)
expect(subject['data'].size).to eq(2)
let!(:activity) { FactoryBot.create(:activity, garden: create(:garden), planting: create(:planting)) }
run_test!
end
end
end
it '#show' do
get("/api/v1/activities/#{activity.id}", params: {}, headers:)
expect(subject['data']['id']).to eq(activity.id.to_s)
end
path '/api/v1/activities/{id}' do
get 'Retrieves an activity' do
tags 'Activities'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
context 'filtering' do
it 'filters by owner' do
get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
end
it 'filters by garden' do
get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
end
it 'filters by planting' do
get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
end
it 'filters by category' do
get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(2)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
expect(subject['data'][1]['id']).to eq(activity2.id.to_s)
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string },
description: { type: :string },
category: { type: :string },
finished: { type: :boolean },
'due-date': { type: :string, format: 'date-time' }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
garden: { '$ref' => '#/components/schemas/relationship' },
planting: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:activity) { FactoryBot.create(:activity, garden: create(:garden), planting: create(:planting)) }
let(:id) { activity.id }
run_test!
end
end
end
end

View File

@@ -1,103 +1,98 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Crops', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Crops API', type: :request do
path '/api/v1/crops' do
get 'Lists crops' do
tags 'Crops'
produces 'application/vnd.api+json'
parameter name: 'filter[approval_status]', in: :query, type: :string, required: false, description: 'Filter by approval status. Defaults to "approved".'
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:crop) { FactoryBot.create(:crop) }
let(:crop_encoded_as_json_api) do
{ "id" => crop.id.to_s,
"type" => "crops",
"links" => { "self" => resource_url },
"attributes" => attributes,
"relationships" => {
"plantings" => plantings_as_json_api,
"parent" => parent_as_json_api,
"harvests" => harvests_as_json_api,
"seeds" => seeds_as_json_api,
"photos" => photos_as_json_api
} }
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string },
'en-wikipedia-url': { type: :string, format: 'uri', 'x-nullable': true },
perennial: { type: :boolean, 'x-nullable': true },
'median-lifespan': { type: :integer, 'x-nullable': true },
'median-days-to-first-harvest': { type: :integer, 'x-nullable': true },
'median-days-to-last-harvest': { type: :integer, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
plantings: { '$ref' => '#/components/schemas/relationship' },
parent: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' },
seeds: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
let!(:crop) { FactoryBot.create(:crop) }
run_test!
end
end
end
let(:resource_url) { "http://www.example.com/api/v1/crops/#{crop.id}" }
path '/api/v1/crops/{id}' do
get 'Retrieves a crop' do
tags 'Crops'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
let(:seeds_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/seeds",
"related" => "#{resource_url}/seeds" } }
end
let(:harvests_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/harvests",
"related" => "#{resource_url}/harvests" } }
end
let(:parent_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/parent",
"related" => "#{resource_url}/parent" } }
end
let(:plantings_as_json_api) do
{ "links" =>
{ "self" =>
"#{resource_url}/relationships/plantings",
"related" => "#{resource_url}/plantings" } }
end
let(:photos_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/photos",
"related" => "#{resource_url}/photos" } }
end
let(:attributes) do
{
"name" => crop.name,
"en-wikipedia-url" => crop.en_wikipedia_url,
"perennial" => false,
"median-lifespan" => nil,
"median-days-to-first-harvest" => nil,
"median-days-to-last-harvest" => nil
}
end
describe '#index' do
before { get '/api/v1/crops', params: {}, headers: }
it { expect(subject['data']).to include(crop_encoded_as_json_api) }
end
describe '#show' do
before { get "/api/v1/crops/#{crop.id}", params: {}, headers: }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("plantings" => plantings_as_json_api) }
it { expect(subject['data']['relationships']).to include("harvests" => harvests_as_json_api) }
it { expect(subject['data']['relationships']).to include("seeds" => seeds_as_json_api) }
it { expect(subject['data']['relationships']).to include("photos" => photos_as_json_api) }
it { expect(subject['data']['relationships']).to include("parent" => parent_as_json_api) }
it { expect(subject['data']).to eq(crop_encoded_as_json_api) }
end
it '#create' do
expect do
post '/api/v1/crops', params: { 'crop' => { 'name' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#update' do
expect do
post "/api/v1/crops/#{crop.id}", params: { 'crop' => { 'name' => 'can i modify this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/crops/#{crop.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string },
'en-wikipedia-url': { type: :string, format: 'uri', 'x-nullable': true },
perennial: { type: :boolean, 'x-nullable': true },
'median-lifespan': { type: :integer, 'x-nullable': true },
'median-days-to-first-harvest': { type: :integer, 'x-nullable': true },
'median-days-to-last-harvest': { type: :integer, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
plantings: { '$ref' => '#/components/schemas/relationship' },
parent: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' },
seeds: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:crop) { FactoryBot.create(:crop) }
let(:id) { crop.id }
run_test!
end
end
end
end

View File

@@ -1,97 +1,223 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Gardens', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Gardens API', type: :request do
path '/api/v1/gardens' do
get 'Lists gardens' do
tags 'Gardens'
produces 'application/vnd.api+json'
parameter name: 'filter[active]', in: :query, type: :string, required: false
parameter name: 'filter[garden_type]', in: :query, type: :string, required: false
parameter name: 'filter[owner_id]', in: :query, type: :string, required: false
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:garden) { FactoryBot.create(:garden) }
let(:garden_encoded_as_json_api) do
{ "id" => garden.id.to_s,
"type" => "gardens",
"links" => { "self" => resource_url },
"attributes" => { "name" => garden.name },
"relationships" =>
{
"owner" => owner_as_json_api,
"plantings" => plantings_as_json_api,
"photos" => photos_as_json_api
} }
end
let(:resource_url) { "http://www.example.com/api/v1/gardens/#{garden.id}" }
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
plantings: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
let(:plantings_as_json_api) do
{ "links" =>
{ "self" =>
"#{resource_url}/relationships/plantings",
"related" => "#{resource_url}/plantings" } }
end
let(:owner_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/owner",
"related" => "#{resource_url}/owner" } }
end
let(:photos_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/photos",
"related" => "#{resource_url}/photos" } }
end
it '#index' do
get('/api/v1/gardens', params: {}, headers:)
expect(subject['data']).to include(garden_encoded_as_json_api)
end
it '#show' do
get("/api/v1/gardens/#{garden.id}", params: {}, headers:)
expect(subject['data']).to include(garden_encoded_as_json_api)
end
context 'filtering' do
let!(:garden2) { FactoryBot.create(:garden, active: false, garden_type: FactoryBot.create(:garden_type)) }
pending 'filters by active' do
get('/api/v1/gardens?filter[active]=true', params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(garden.id.to_s)
let!(:garden) { FactoryBot.create(:garden) }
run_test!
end
end
it 'filters by garden_type' do
get("/api/v1/gardens?filter[garden_type]=#{garden2.garden_type.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(garden2.id.to_s)
end
post 'Creates a garden' do
tags 'Gardens'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :garden, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string }
},
required: ['name']
}
},
required: ['type', 'attributes']
}
},
required: ['data']
}
it 'filters by owner' do
get("/api/v1/gardens?filter[owner_id]=#{garden2.owner.id}", params: {}, headers:)
response '201', 'created' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:garden) { { data: { type: 'gardens', attributes: { name: 'My API Garden' } } } }
run_test!
end
expect(response.status).to eq 200
expect(subject['data'].size).to eq(2)
expect(subject['data'][1]['id']).to eq(garden2.id.to_s)
response '401', 'unauthorized' do
let(:garden) { { data: { type: 'gardens', attributes: { name: 'My API Garden' } } } }
run_test!
end
end
end
it '#create' do
expect do
post '/api/v1/gardens', params: { 'garden' => { 'name' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
end
path '/api/v1/gardens/{id}' do
get 'Retrieves a garden' do
tags 'Gardens'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
it '#update' do
expect do
post "/api/v1/gardens/#{garden.id}", params: { 'garden' => { 'name' => 'can i modify this' } }, headers:
end.to raise_error ActionController::RoutingError
end
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
plantings: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:garden) { FactoryBot.create(:garden) }
let(:id) { garden.id }
run_test!
end
end
it '#delete' do
expect do
delete "/api/v1/gardens/#{garden.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
patch 'Updates a garden' do
tags 'Gardens'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
parameter name: :garden, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string },
attributes: {
type: :object,
properties: {
name: { type: :string }
}
}
},
required: ['type', 'id']
}
},
required: ['data']
}
response '200', 'ok' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:garden_to_update) { create(:garden, owner: member) }
let(:id) { garden_to_update.id }
let(:garden) { { data: { type: 'gardens', id: id, attributes: { name: 'An updated garden' } } } }
run_test!
end
response '401', 'unauthorized' do
let(:garden_to_update) { create(:garden) }
let(:id) { garden_to_update.id }
let(:garden) { { data: { type: 'gardens', id: id, attributes: { name: 'An updated garden' } } } }
run_test!
end
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_garden) { create(:garden) }
let(:id) { other_member_garden.id }
let(:garden) { { data: { type: 'gardens', id: id, attributes: { name: 'An updated garden' } } } }
run_test!
end
end
delete 'Deletes a garden' do
tags 'Gardens'
parameter name: :id, in: :path, type: :string
response '204', 'no content' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:garden_to_delete) { create(:garden, owner: member) }
let(:id) { garden_to_delete.id }
run_test!
end
response '401', 'unauthorized' do
let(:garden_to_delete) { create(:garden) }
let(:id) { garden_to_delete.id }
run_test!
end
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_garden) { create(:garden) }
let(:id) { other_member_garden.id }
run_test!
end
end
end
end

View File

@@ -1,133 +1,257 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Harvests', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Harvests API', type: :request do
path '/api/v1/harvests' do
get 'Lists harvests' do
tags 'Harvests'
produces 'application/vnd.api+json'
parameter name: 'filter[crop_id]', in: :query, type: :string, required: false
parameter name: 'filter[planting_id]', in: :query, type: :string, required: false
parameter name: 'filter[plant_part]', in: :query, type: :string, required: false
parameter name: 'filter[owner_id]', in: :query, type: :string, required: false
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:harvest) { FactoryBot.create(:harvest) }
let(:harvest_encoded_as_json_api) do
{ "id" => harvest.id.to_s,
"type" => "harvests",
"links" => { "self" => resource_url },
"attributes" => attributes,
"relationships" => {
"crop" => crop_as_json_api,
"planting" => planting_as_json_api,
"owner" => owner_as_json_api,
"photos" => photos_as_json_api
} }
end
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
'harvested-at': { type: :string, format: 'date' },
description: { type: :string, 'x-nullable': true },
unit: { type: :string, 'x-nullable': true },
'weight-quantity': { type: :string, 'x-nullable': true },
'weight-unit': { type: :string, 'x-nullable': true },
'si-weight': { type: :number, format: :float, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
crop: { '$ref' => '#/components/schemas/relationship' },
planting: { '$ref' => '#/components/schemas/relationship' },
owner: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
let(:resource_url) { "http://www.example.com/api/v1/harvests/#{harvest.id}" }
let(:crop_as_json_api) do
{ "links" =>
{ "self" =>
"#{resource_url}/relationships/crop",
"related" => "#{resource_url}/crop" } }
end
let(:owner_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/owner",
"related" => "#{resource_url}/owner" } }
end
let(:planting_as_json_api) do
{ "links" =>
{ "self" =>
"#{resource_url}/relationships/planting",
"related" => "#{resource_url}/planting" } }
end
let(:photos_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/photos",
"related" => "#{resource_url}/photos" } }
end
let(:attributes) do
{
"harvested-at" => "2015-09-17",
"description" => harvest.description,
"unit" => harvest.unit,
"weight-quantity" => harvest.weight_quantity.to_s,
"weight-unit" => harvest.weight_unit,
"si-weight" => harvest.si_weight
}
end
describe '#index' do
before { get '/api/v1/harvests', params: {}, headers: }
it { expect(subject['data']).to include(harvest_encoded_as_json_api) }
end
describe '#show' do
before { get "/api/v1/harvests/#{harvest.id}", params: {}, headers: }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("planting" => planting_as_json_api) }
it { expect(subject['data']['relationships']).to include("crop" => crop_as_json_api) }
it { expect(subject['data']['relationships']).to include("photos" => photos_as_json_api) }
it { expect(subject['data']['relationships']).to include("owner" => owner_as_json_api) }
it { expect(subject['data']).to eq(harvest_encoded_as_json_api) }
end
context 'filtering' do
let!(:harvest2) { FactoryBot.create(:harvest, planting: create(:planting)) }
it 'filters by crop' do
get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers:)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
let!(:harvest) { FactoryBot.create(:harvest) }
run_test!
end
end
it 'filters by planting' do
get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
end
it 'filters by plant_part' do
get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
end
it 'filters by owner' do
get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
end
end
it '#create' do
expect do
put '/api/v1/harvests', headers:, params: {
'harvest' => { 'description' => 'can i make this' }
post 'Creates a harvest' do
tags 'Harvests'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :harvest, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string }
}
},
relationships: {
type: :object,
properties: {
planting: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string }
},
required: ['type', 'id']
}
},
required: ['data']
}
},
required: ['planting']
}
},
required: ['type', 'attributes', 'relationships']
}
},
required: ['data']
}
end.to raise_error ActionController::RoutingError
response '201', 'created' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:planting) { create(:planting, owner: member) }
let(:harvest) { { data: { type: 'harvests', attributes: { description: 'My API harvest' }, relationships: { planting: { data: { type: 'plantings', id: planting.id } } } } } }
run_test!
end
response '401', 'unauthorized' do
let(:planting) { create(:planting) }
let(:harvest) { { data: { type: 'harvests', attributes: { description: 'My API harvest' }, relationships: { planting: { data: { type: 'plantings', id: planting.id } } } } } }
run_test!
end
end
end
it '#update' do
expect do
post "/api/v1/harvests/#{harvest.id}", headers:, params: {
'harvest' => { 'description' => 'can i modify this' }
path '/api/v1/harvests/{id}' do
get 'Retrieves a harvest' do
tags 'Harvests'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
'harvested-at': { type: :string, format: 'date' },
description: { type: :string, 'x-nullable': true },
unit: { type: :string, 'x-nullable': true },
'weight-quantity': { type: :string, 'x-nullable': true },
'weight-unit': { type: :string, 'x-nullable': true },
'si-weight': { type: :number, format: :float, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
crop: { '$ref' => '#/components/schemas/relationship' },
planting: { '$ref' => '#/components/schemas/relationship' },
owner: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:harvest) { FactoryBot.create(:harvest) }
let(:id) { harvest.id }
run_test!
end
end
patch 'Updates a harvest' do
tags 'Harvests'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
parameter name: :harvest, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string }
}
}
},
required: ['type', 'id']
}
},
required: ['data']
}
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/harvests/#{harvest.id}", headers:, params: {}
end.to raise_error ActionController::RoutingError
response '200', 'ok' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:harvest_to_update) { create(:harvest, owner: member) }
let(:id) { harvest_to_update.id }
let(:harvest) { { data: { type: 'harvests', id: id, attributes: { description: 'An updated harvest' } } } }
run_test!
end
response '401', 'unauthorized' do
let(:harvest_to_update) { create(:harvest) }
let(:id) { harvest_to_update.id }
let(:harvest) { { data: { type: 'harvests', id: id, attributes: { description: 'An updated harvest' } } } }
run_test!
end
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_harvest) { create(:harvest) }
let(:id) { other_member_harvest.id }
let(:harvest) { { data: { type: 'harvests', id: id, attributes: { description: 'An updated harvest' } } } }
run_test!
end
end
delete 'Deletes a harvest' do
tags 'Harvests'
parameter name: :id, in: :path, type: :string
response '204', 'no content' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:harvest_to_delete) { create(:harvest, owner: member) }
let(:id) { harvest_to_delete.id }
run_test!
end
response '401', 'unauthorized' do
let(:harvest_to_delete) { create(:harvest) }
let(:id) { harvest_to_delete.id }
run_test!
end
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_harvest) { create(:harvest) }
let(:id) { other_member_harvest.id }
run_test!
end
end
end
end

View File

@@ -1,100 +1,91 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Members', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Members API', type: :request do
path '/api/v1/members' do
get 'Lists members' do
tags 'Members'
produces 'application/vnd.api+json'
parameter name: 'filter[login_name]', in: :query, type: :string, required: false
parameter name: 'filter[slug]', in: :query, type: :string, required: false
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:member) { FactoryBot.create(:member) }
let(:member_encoded_as_json_api) do
{ "id" => member.id.to_s,
"type" => "members",
"links" => { "self" => resource_url },
"attributes" => attributes,
"relationships" => {
"gardens" => gardens_as_json_api,
"harvests" => harvests_as_json_api,
"photos" => photos_as_json_api,
"plantings" => plantings_as_json_api,
"seeds" => seeds_as_json_api
} }
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
'login-name': { type: :string },
slug: { type: :string }
}
},
relationships: {
type: :object,
properties: {
gardens: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' },
plantings: { '$ref' => '#/components/schemas/relationship' },
seeds: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
let!(:member) { FactoryBot.create(:member) }
run_test!
end
end
end
let(:resource_url) { "http://www.example.com/api/v1/members/#{member.id}" }
path '/api/v1/members/{id}' do
get 'Retrieves a member' do
tags 'Members'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
let(:harvests_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/harvests",
"related" => "#{resource_url}/harvests" } }
end
let(:photos_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/photos",
"related" => "#{resource_url}/photos" } }
end
let(:seeds_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/seeds",
"related" => "#{resource_url}/seeds" } }
end
let(:plantings_as_json_api) do
{ "links" =>
{ "self" =>
"#{resource_url}/relationships/plantings",
"related" => "#{resource_url}/plantings" } }
end
let(:gardens_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/gardens",
"related" => "#{resource_url}/gardens" } }
end
let(:attributes) do
{
"login-name" => member.login_name,
"slug" => member.slug
}
end
describe '#index' do
before { get '/api/v1/members', params: {}, headers: }
it { expect(subject['data']).to include(member_encoded_as_json_api) }
end
describe '#show' do
before { get "/api/v1/members/#{member.id}", params: {}, headers: }
it { expect(subject['data']['relationships']).to include("gardens" => gardens_as_json_api) }
it { expect(subject['data']['relationships']).to include("plantings" => plantings_as_json_api) }
it { expect(subject['data']['relationships']).to include("seeds" => seeds_as_json_api) }
it { expect(subject['data']['relationships']).to include("harvests" => harvests_as_json_api) }
it { expect(subject['data']['relationships']).to include("photos" => photos_as_json_api) }
it { expect(subject['data']).to eq(member_encoded_as_json_api) }
end
it '#create' do
expect do
post '/api/v1/members', params: { 'member' => { 'login_name' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#update' do
expect do
post "/api/v1/members/#{member.id}", params: {
'member' => { 'login_name' => 'can i modify this' }
},
headers:
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/members/#{member.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
'login-name': { type: :string },
slug: { type: :string }
}
},
relationships: {
type: :object,
properties: {
gardens: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' },
plantings: { '$ref' => '#/components/schemas/relationship' },
seeds: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:member) { FactoryBot.create(:member) }
let(:id) { member.id }
run_test!
end
end
end
end

View File

@@ -1,93 +1,93 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Photos', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Photos API', type: :request do
path '/api/v1/photos' do
get 'Lists photos' do
tags 'Photos'
produces 'application/vnd.api+json'
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:photo) { FactoryBot.create(:photo) }
let(:photo_encoded_as_json_api) do
{ "id" => photo.id.to_s,
"type" => "photos",
"links" => { "self" => resource_url },
"attributes" => attributes,
"relationships" => {
"owner" => owner_as_json_api,
"plantings" => plantings_as_json_api,
"harvests" => harvests_as_json_api,
"gardens" => gardens_as_json_api
} }
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
'thumbnail-url': { type: :string, format: :uri },
'fullsize-url': { type: :string, format: :uri },
'license-name': { type: :string },
'link-url': { type: :string, format: :uri },
title: { type: :string }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
plantings: { '$ref' => '#/components/schemas/relationship' },
gardens: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
let!(:photo) { FactoryBot.create(:photo) }
run_test!
end
end
end
let(:resource_url) { "http://www.example.com/api/v1/photos/#{photo.id}" }
path '/api/v1/photos/{id}' do
get 'Retrieves a photo' do
tags 'Photos'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
let(:owner_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/owner",
"related" => "#{resource_url}/owner" } }
end
let(:harvests_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/harvests",
"related" => "#{resource_url}/harvests" } }
end
let(:gardens_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/gardens",
"related" => "#{resource_url}/gardens" } }
end
let(:plantings_as_json_api) do
{ "links" =>
{ "self" =>
"#{resource_url}/relationships/plantings",
"related" => "#{resource_url}/plantings" } }
end
let(:attributes) do
{
"thumbnail-url" => photo.thumbnail_url,
"fullsize-url" => photo.fullsize_url,
"link-url" => photo.link_url,
"license-name" => photo.license_name,
"title" => photo.title
}
end
describe '#index' do
before { get '/api/v1/photos', params: {}, headers: }
it { expect(subject['data']).to include(photo_encoded_as_json_api) }
end
describe '#show' do
before { get "/api/v1/photos/#{photo.id}", params: {}, headers: }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("plantings" => plantings_as_json_api) }
it { expect(subject['data']['relationships']).to include("harvests" => harvests_as_json_api) }
it { expect(subject['data']['relationships']).to include("owner" => owner_as_json_api) }
it { expect(subject['data']).to eq(photo_encoded_as_json_api) }
end
it '#create' do
expect do
post '/api/v1/photos', params: { 'photo' => { 'name' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#update' do
expect do
post "/api/v1/photos/#{photo.id}", params: { 'photo' => { 'name' => 'can i modify this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/photos/#{photo.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
'thumbnail-url': { type: :string, format: :uri },
'fullsize-url': { type: :string, format: :uri },
'license-name': { type: :string },
'link-url': { type: :string, format: :uri },
title: { type: :string }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
plantings: { '$ref' => '#/components/schemas/relationship' },
gardens: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:photo) { FactoryBot.create(:photo) }
let(:id) { photo.id }
run_test!
end
end
end
end

View File

@@ -1,175 +1,301 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Plantings', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Plantings API', type: :request do
path '/api/v1/plantings' do
get 'Lists plantings' do
tags 'Plantings'
produces 'application/vnd.api+json'
parameter name: 'filter[failed]', in: :query, type: :string, required: false
parameter name: 'filter[sunniness]', in: :query, type: :string, required: false
parameter name: 'filter[perennial]', in: :query, type: :string, required: false
parameter name: 'filter[active]', in: :query, type: :string, required: false
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:planting) { FactoryBot.create(:planting) }
let(:planting_encoded_as_json_api) do
{ "id" => planting.id.to_s,
"type" => "plantings",
"links" => { "self" => resource_url },
"attributes" => attributes,
"relationships" => {
"garden" => garden_as_json_api,
"crop" => crop_as_json_api,
"owner" => owner_as_json_api,
"photos" => photos_as_json_api,
"harvests" => harvests_as_json_api
} }
end
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
slug: { type: :string },
'planted-at': { type: :string, format: 'date' },
failed: { type: :boolean },
finished: { type: :boolean },
'finished-at': { type: :string, format: 'date-time', 'x-nullable': true },
quantity: { type: :integer },
description: { type: :string, 'x-nullable': true },
sunniness: { type: :string, 'x-nullable': true },
'planted-from': { type: :string, 'x-nullable': true },
'expected-lifespan': { type: :integer, 'x-nullable': true },
'finish-predicted-at': { type: :string, format: 'date-time', 'x-nullable': true },
'first-harvest-date': { type: :string, format: 'date', 'x-nullable': true },
'last-harvest-date': { type: :string, format: 'date', 'x-nullable': true },
'crop-name': { type: :string },
'crop-slug': { type: :string },
thumbnail: { type: :string, format: :uri, 'x-nullable': true },
location: { type: :string, 'x-nullable': true },
longitude: { type: :number, format: :float, 'x-nullable': true },
latitude: { type: :number, format: :float, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
garden: { '$ref' => '#/components/schemas/relationship' },
crop: { '$ref' => '#/components/schemas/relationship' },
owner: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
let(:resource_url) { "http://www.example.com/api/v1/plantings/#{planting.id}" }
let(:harvests_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/harvests",
"related" => "#{resource_url}/harvests" } }
end
let(:photos_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/photos",
"related" => "#{resource_url}/photos" } }
end
let(:owner_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/owner",
"related" => "#{resource_url}/owner" } }
end
let(:crop_as_json_api) do
{ "links" =>
{ "self" =>
"#{resource_url}/relationships/crop",
"related" => "#{resource_url}/crop" } }
end
let(:garden_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/garden",
"related" => "#{resource_url}/garden" } }
end
let(:attributes) do
{
"slug" => planting.slug,
"planted-at" => "2014-07-30",
"failed" => false,
"finished-at" => nil,
"finished" => false,
"quantity" => 33,
"description" => planting.description,
"crop-name" => planting.crop.name,
"crop-slug" => planting.crop.slug,
"sunniness" => nil,
"planted-from" => nil,
"expected-lifespan" => nil,
"finish-predicted-at" => nil,
"percentage-grown" => nil,
"first-harvest-date" => nil,
"last-harvest-date" => nil,
"thumbnail" => nil,
"location" => planting.garden.location,
"longitude" => planting.garden.longitude,
"latitude" => planting.garden.latitude
}
end
it '#index' do
get('/api/v1/plantings', params: {}, headers:)
expect(subject['data'][0].keys).to eq(planting_encoded_as_json_api.keys)
expect(subject['data'][0]['attributes'].keys.sort!).to eq(planting_encoded_as_json_api['attributes'].keys.sort!)
expect(subject['data']).to include(planting_encoded_as_json_api)
end
it '#show' do
get("/api/v1/plantings/#{planting.id}", params: {}, headers:)
expect(subject['data']['relationships']).to include("garden" => garden_as_json_api)
expect(subject['data']['relationships']).to include("crop" => crop_as_json_api)
expect(subject['data']['relationships']).to include("owner" => owner_as_json_api)
expect(subject['data']['relationships']).to include("harvests" => harvests_as_json_api)
expect(subject['data']['relationships']).to include("photos" => photos_as_json_api)
expect(subject['data']).to eq(planting_encoded_as_json_api)
end
it '#create' do
expect do
post '/api/v1/plantings', params: { 'planting' => { 'description' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#update' do
expect do
post "/api/v1/plantings/#{planting.id}", headers:, params: {
'planting' => { 'description' => 'can i modify this' }
}
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/plantings/#{planting.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
end
describe "by member/owner" do
before :each do
@member1 = planting.owner
@planting2 = create(:planting, owner: create(:owner))
@member2 = @planting2.owner
let!(:planting) { FactoryBot.create(:planting) }
run_test!
end
end
describe "#show" do
it "locates the correct member" do
get "/api/v1/plantings?filter[owner-id]=#{@member1.id}"
expect(JSON.parse(response.body)['data'][0]['id']).to eq(planting.id.to_s)
post 'Creates a planting' do
tags 'Plantings'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :planting, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string }
}
},
relationships: {
type: :object,
properties: {
crop: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string }
},
required: ['type', 'id']
}
},
required: ['data']
},
garden: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string }
},
required: ['type', 'id']
}
},
required: ['data']
}
},
required: ['crop', 'garden']
}
},
required: ['type', 'attributes', 'relationships']
}
},
required: ['data']
}
get "/api/v1/plantings?filter[owner-id]=#{@member2.id}"
expect(JSON.parse(response.body)['data'][0]['id']).to eq(@planting2.id.to_s)
response '201', 'created' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:crop) { create(:crop) }
let(:garden) { create(:garden, owner: member) }
let(:planting) { { data: { type: 'plantings', attributes: { description: 'My API planting' }, relationships: { crop: { data: { type: 'crops', id: crop.id } }, garden: { data: { type: 'gardens', id: garden.id } } } } } }
run_test!
end
pending "The below should be identical to the above, but aren't."
get "/api/v1/members/#{@member1.id}/plantings"
expect(JSON.parse(response.body)['data'][0]['id']).to eq(planting.id.to_s)
get "/api/v1/members/#{@member2.id}/plantings"
expect(JSON.parse(response.body)['data'][0]['id']).to eq(@planting2.id.to_s)
response '401', 'unauthorized' do
let(:crop) { create(:crop) }
let(:garden) { create(:garden) }
let(:planting) { { data: { type: 'plantings', attributes: { description: 'My API planting' }, relationships: { crop: { data: { type: 'crops', id: crop.id } }, garden: { data: { type: 'gardens', id: garden.id } } } } } }
run_test!
end
end
end
context 'filtering' do
let!(:planting2) { FactoryBot.create(:planting, failed: true, sunniness: 'shade') }
let!(:perennial_planting) { FactoryBot.create(:planting, crop: FactoryBot.create(:crop, perennial: true)) }
it 'filters by failed' do
get('/api/v1/plantings?filter[failed]=true', params: {}, headers:)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting2.id.to_s)
path '/api/v1/plantings/{id}' do
get 'Retrieves a planting' do
tags 'Plantings'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
slug: { type: :string },
'planted-at': { type: :string, format: 'date' },
failed: { type: :boolean },
finished: { type: :boolean },
'finished-at': { type: :string, format: 'date-time', 'x-nullable': true },
quantity: { type: :integer },
description: { type: :string, 'x-nullable': true },
sunniness: { type: :string, 'x-nullable': true },
'planted-from': { type: :string, 'x-nullable': true },
'expected-lifespan': { type: :integer, 'x-nullable': true },
'finish-predicted-at': { type: :string, format: 'date-time', 'x-nullable': true },
'first-harvest-date': { type: :string, format: 'date', 'x-nullable': true },
'last-harvest-date': { type: :string, format: 'date', 'x-nullable': true },
'crop-name': { type: :string },
'crop-slug': { type: :string },
thumbnail: { type: :string, format: :uri, 'x-nullable': true },
location: { type: :string, 'x-nullable': true },
longitude: { type: :number, format: :float, 'x-nullable': true },
latitude: { type: :number, format: :float, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
garden: { '$ref' => '#/components/schemas/relationship' },
crop: { '$ref' => '#/components/schemas/relationship' },
owner: { '$ref' => '#/components/schemas/relationship' },
photos: { '$ref' => '#/components/schemas/relationship' },
harvests: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:planting) { FactoryBot.create(:planting) }
let(:id) { planting.id }
run_test!
end
end
it 'filters by sunniness' do
get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting2.id.to_s)
patch 'Updates a planting' do
tags 'Plantings'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
parameter name: :planting, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string }
}
}
},
required: ['type', 'id']
}
},
required: ['data']
}
response '200', 'ok' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:planting_to_update) { create(:planting, owner: member) }
let(:id) { planting_to_update.id }
let(:planting) { { data: { type: 'plantings', id: id, attributes: { description: 'An updated planting' } } } }
run_test!
end
response '401', 'unauthorized' do
let(:planting_to_update) { create(:planting) }
let(:id) { planting_to_update.id }
let(:planting) { { data: { type: 'plantings', id: id, attributes: { description: 'An updated planting' } } } }
run_test!
end
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_planting) { create(:planting) }
let(:id) { other_member_planting.id }
let(:planting) { { data: { type: 'plantings', id: id, attributes: { description: 'An updated planting' } } } }
run_test!
end
end
it 'filters by perennial' do
get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:)
delete 'Deletes a planting' do
tags 'Plantings'
parameter name: :id, in: :path, type: :string
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s)
end
response '204', 'no content' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:planting_to_delete) { create(:planting, owner: member) }
let(:id) { planting_to_delete.id }
run_test!
end
it 'filters by active' do
get('/api/v1/plantings?filter[active]=true', params: {}, headers:)
response '401', 'unauthorized' do
let(:planting_to_delete) { create(:planting) }
let(:id) { planting_to_delete.id }
run_test!
end
expect(response.status).to eq 200
expect(subject['data'].size).to eq(2)
expect(subject['data'][0]['id']).to eq(planting.id.to_s)
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_planting) { create(:planting) }
let(:id) { other_member_planting.id }
run_test!
end
end
end
end

View File

@@ -1,133 +1,261 @@
# frozen_string_literal: true
require 'rails_helper'
require 'swagger_helper'
RSpec.describe 'Seeds', type: :request do
subject { JSON.parse response.body }
RSpec.describe 'Seeds API', type: :request do
path '/api/v1/seeds' do
get 'Lists seeds' do
tags 'Seeds'
produces 'application/vnd.api+json'
parameter name: 'filter[crop]', in: :query, type: :string, required: false
parameter name: 'filter[tradable_to]', in: :query, type: :string, required: false
parameter name: 'filter[organic]', in: :query, type: :string, required: false
parameter name: 'filter[gmo]', in: :query, type: :string, required: false
parameter name: 'filter[heirloom]', in: :query, type: :string, required: false
parameter name: 'filter[owner_id]', in: :query, type: :string, required: false
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:seed) { FactoryBot.create(:seed) }
let(:seed_encoded_as_json_api) do
{ "id" => seed.id.to_s,
"type" => "seeds",
"links" => { "self" => resource_url },
"attributes" => attributes,
"relationships" => {
"owner" => owner_as_json_api,
"crop" => crop_as_json_api
} }
end
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string, 'x-nullable': true },
quantity: { type: :integer, 'x-nullable': true },
'plant-before': { type: :string, format: 'date', 'x-nullable': true },
'tradable-to': { type: :string, 'x-nullable': true },
'days-until-maturity-min': { type: :integer, 'x-nullable': true },
'days-until-maturity-max': { type: :integer, 'x-nullable': true },
organic: { type: :string, 'x-nullable': true },
gmo: { type: :string, 'x-nullable': true },
heirloom: { type: :string, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
crop: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
}
let(:resource_url) { "http://www.example.com/api/v1/seeds/#{seed.id}" }
let(:owner_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/owner",
"related" => "#{resource_url}/owner" } }
end
let(:crop_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/crop",
"related" => "#{resource_url}/crop" } }
end
let(:attributes) do
{
"description" => seed.description,
"quantity" => seed.quantity,
"plant-before" => "2013-07-15",
"tradable-to" => seed.tradable_to,
"days-until-maturity-min" => seed.days_until_maturity_min,
"days-until-maturity-max" => seed.days_until_maturity_max,
"organic" => seed.organic,
"gmo" => seed.gmo,
"heirloom" => seed.heirloom
}
end
describe '#index' do
before { get '/api/v1/seeds', params: {}, headers: }
it { expect(subject['data']).to include(seed_encoded_as_json_api) }
end
describe '#show' do
before { get "/api/v1/seeds/#{seed.id}", params: {}, headers: }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("owner" => owner_as_json_api) }
it { expect(subject['data']['relationships']).to include("crop" => crop_as_json_api) }
it { expect(subject['data']).to eq(seed_encoded_as_json_api) }
end
it '#create' do
expect do
post '/api/v1/seeds', params: { 'seed' => { 'name' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#update' do
expect do
post "/api/v1/seeds/#{seed.id}", params: { 'seed' => { 'name' => 'can i modify this' } }, headers:
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/seeds/#{seed.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
end
context 'filtering' do
let!(:seed2) { FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') }
it 'filters by crop' do
get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
let!(:seed) { FactoryBot.create(:seed) }
run_test!
end
end
post 'Creates a seed' do
tags 'Seeds'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :seed, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string }
}
},
relationships: {
type: :object,
properties: {
crop: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string }
},
required: ['type', 'id']
}
},
required: ['data']
}
},
required: ['crop']
}
},
required: ['type', 'attributes', 'relationships']
}
},
required: ['data']
}
it 'filters by tradable_to' do
get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers:)
response '201', 'created' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:crop) { create(:crop) }
let(:seed) { { data: { type: 'seeds', attributes: { description: 'My API seed' }, relationships: { crop: { data: { type: 'crops', id: crop.id } } } } } }
run_test!
end
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
response '401', 'unauthorized' do
let(:crop) { create(:crop) }
let(:seed) { { data: { type: 'seeds', attributes: { description: 'My API seed' }, relationships: { crop: { data: { type: 'crops', id: crop.id } } } } } }
run_test!
end
end
end
path '/api/v1/seeds/{id}' do
get 'Retrieves a seed' do
tags 'Seeds'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
response '200', 'successful' do
schema type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string, 'x-nullable': true },
quantity: { type: :integer, 'x-nullable': true },
'plant-before': { type: :string, format: 'date', 'x-nullable': true },
'tradable-to': { type: :string, 'x-nullable': true },
'days-until-maturity-min': { type: :integer, 'x-nullable': true },
'days-until-maturity-max': { type: :integer, 'x-nullable': true },
organic: { type: :string, 'x-nullable': true },
gmo: { type: :string, 'x-nullable': true },
heirloom: { type: :string, 'x-nullable': true }
}
},
relationships: {
type: :object,
properties: {
owner: { '$ref' => '#/components/schemas/relationship' },
crop: { '$ref' => '#/components/schemas/relationship' }
}
}
}
}
}
let(:seed) { FactoryBot.create(:seed) }
let(:id) { seed.id }
run_test!
end
end
it 'filters by organic' do
get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers:)
patch 'Updates a seed' do
tags 'Seeds'
consumes 'application/vnd.api+json'
produces 'application/vnd.api+json'
parameter name: :id, in: :path, type: :string
parameter name: :seed, in: :body, schema: {
type: :object,
properties: {
data: {
type: :object,
properties: {
type: { type: :string },
id: { type: :string },
attributes: {
type: :object,
properties: {
description: { type: :string }
}
}
},
required: ['type', 'id']
}
},
required: ['data']
}
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
response '200', 'ok' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:seed_to_update) { create(:seed, owner: member) }
let(:id) { seed_to_update.id }
let(:seed) { { data: { type: 'seeds', id: id, attributes: { description: 'An updated seed' } } } }
run_test!
end
response '401', 'unauthorized' do
let(:seed_to_update) { create(:seed) }
let(:id) { seed_to_update.id }
let(:seed) { { data: { type: 'seeds', id: id, attributes: { description: 'An updated seed' } } } }
run_test!
end
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_seed) { create(:seed) }
let(:id) { other_member_seed.id }
let(:seed) { { data: { type: 'seeds', id: id, attributes: { description: 'An updated seed' } } } }
run_test!
end
end
it 'filters by gmo' do
get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers:)
delete 'Deletes a seed' do
tags 'Seeds'
parameter name: :id, in: :path, type: :string
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end
response '204', 'no content' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:seed_to_delete) { create(:seed, owner: member) }
let(:id) { seed_to_delete.id }
run_test!
end
it 'filters by heirloom' do
get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers:)
response '401', 'unauthorized' do
let(:seed_to_delete) { create(:seed) }
let(:id) { seed_to_delete.id }
run_test!
end
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end
it 'filters by owner' do
get("/api/v1/seeds?filter[owner_id]=#{seed2.owner.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
response '403', 'forbidden' do
let(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:Authorization) { "Token token=#{token}" }
let(:other_member_seed) { create(:seed) }
let(:id) { other_member_seed.id }
run_test!
end
end
end
end

View File

@@ -15,13 +15,29 @@ RSpec.configure do |config|
# 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' => {
'v1/swagger.json' => {
openapi: '3.0.1',
info: {
title: 'API V1',
version: 'v1'
},
paths: {}
paths: {},
components: {
schemas: {
relationship: {
type: :object,
properties: {
data: {
type: :object,
properties: {
id: { type: :string },
type: { type: :string }
}
}
}
}
}
}
}
}
@@ -29,5 +45,5 @@ RSpec.configure do |config|
# 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
config.swagger_format = :json
end

View File

File diff suppressed because it is too large Load Diff