Compare commits

..

23 Commits

Author SHA1 Message Date
google-labs-jules[bot]
1b087bd161 Refactor API request specs to use token authentication
This commit introduces a new shared context, `with authenticated member`, to standardize authentication in the API request specs. The shared context creates a member, generates an API token using the `Token token=` scheme, and provides authenticated and unauthenticated headers for use in the tests.

All API request specs in `spec/requests/api/v1/` have been updated to use this shared context. This includes:

*   Associating test data with the authenticated member to correctly test authorization scopes.
*   Updating test expectations to reflect the new scoped behavior of the API endpoints.
*   Refactoring `create`, `update`, and `delete` tests to use the shared headers for checking authorization rules, making the tests cleaner and more robust.
2025-11-29 03:41:08 +00:00
dependabot[bot]
e9a187b3df Merge pull request #4282 from Growstuff/dependabot/bundler/icalendar-2.12.0 2025-09-29 10:54:38 +00:00
dependabot[bot]
7b7bf9f4e9 Bump icalendar from 2.11.2 to 2.12.0
Bumps [icalendar](https://github.com/icalendar/icalendar) from 2.11.2 to 2.12.0.
- [Changelog](https://github.com/icalendar/icalendar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/icalendar/icalendar/compare/v2.11.2...v2.12.0)

---
updated-dependencies:
- dependency-name: icalendar
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-29 08:35:09 +00:00
Daniel O'Connor
d9851231f2 Merge pull request #4274 from Growstuff/CloCkWeRX-patch-3
API > Render activities links under member
2025-09-29 01:11:24 +09:30
Daniel O'Connor
f82eabec42 Merge pull request #4273 from Growstuff/CloCkWeRX-patch-2
Create robots.txt based on wikipedia
2025-09-29 01:03:42 +09:30
Daniel O'Connor
83bf752a02 Merge pull request #4272 from Growstuff/feature/JULES-38-planting-api-garden-id
Adjust all API resources to include basic data for has one relationships
2025-09-29 00:55:18 +09:30
dependabot[bot]
6ffdd283e4 Merge pull request #4270 from Growstuff/dependabot/bundler/rack-2.2.18 2025-09-28 15:19:22 +00:00
Daniel O'Connor
d8e138ae2d Create robots.txt based on wikipedia 2025-09-29 00:27:57 +09:30
Daniel O'Connor
81b80d9bb7 Update activity_resource.rb 2025-09-29 00:12:51 +09:30
Daniel O'Connor
f080a8a566 Update crop_resource.rb 2025-09-29 00:12:39 +09:30
Daniel O'Connor
5a436f9d7f Update garden_resource.rb 2025-09-29 00:12:32 +09:30
Daniel O'Connor
a4fd1c4a8e Update harvest_resource.rb 2025-09-29 00:12:27 +09:30
Daniel O'Connor
7277cb3523 Update seed_resource.rb 2025-09-29 00:12:07 +09:30
Daniel O'Connor
522d10e053 Update photo_resource.rb 2025-09-29 00:11:41 +09:30
Daniel O'Connor
f3a9b26c8e Update planting_resource.rb 2025-09-29 00:10:05 +09:30
google-labs-jules[bot]
d8f5580ef9 Extend planting API to render garden id in relationships 2025-09-28 14:39:33 +00:00
dependabot[bot]
d9e58f6bf0 Bump rack from 2.2.17 to 2.2.18
Bumps [rack](https://github.com/rack/rack) from 2.2.17 to 2.2.18.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v2.2.17...v2.2.18)

---
updated-dependencies:
- dependency-name: rack
  dependency-version: 2.2.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-26 09:46:59 +00:00
Daniel O'Connor
60ec4e8d18 Merge pull request #4271 from Growstuff/dependabot/bundler/rubocop-1.81.0
Bump rubocop from 1.80.2 to 1.81.0
2025-09-26 19:15:45 +09:30
dependabot[bot]
ede7e6e7f7 Bump rubocop from 1.80.2 to 1.81.0
Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.80.2 to 1.81.0.
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.80.2...v1.81.0)

---
updated-dependencies:
- dependency-name: rubocop
  dependency-version: 1.81.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-26 07:01:42 +00:00
dependabot[bot]
9c62f955e2 Merge pull request #4267 from Growstuff/dependabot/bundler/geocoder-1.8.6 2025-09-24 09:47:54 +00:00
dependabot[bot]
6b313c190f Bump geocoder from 1.8.5 to 1.8.6
Bumps [geocoder](https://github.com/alexreisner/geocoder) from 1.8.5 to 1.8.6.
- [Changelog](https://github.com/alexreisner/geocoder/blob/master/CHANGELOG.md)
- [Commits](https://github.com/alexreisner/geocoder/compare/v1.8.5...v1.8.6)

---
updated-dependencies:
- dependency-name: geocoder
  dependency-version: 1.8.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-24 09:15:25 +00:00
Daniel O'Connor
c575f9d03c Merge pull request #4269 from Growstuff/dependabot/bundler/puma-7.0.4
Bump puma from 7.0.3 to 7.0.4
2025-09-24 18:44:13 +09:30
dependabot[bot]
95a4b0c66c Bump puma from 7.0.3 to 7.0.4
Bumps [puma](https://github.com/puma/puma) from 7.0.3 to 7.0.4.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v7.0.3...v7.0.4)

---
updated-dependencies:
- dependency-name: puma
  dependency-version: 7.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-24 07:01:40 +00:00
19 changed files with 363 additions and 265 deletions

View File

@@ -208,7 +208,7 @@ GEM
gli (>= 2.7.0)
i18n (>= 0.6.4)
rubyzip (>= 1.0.0)
csv (3.3.1)
csv (3.3.5)
csv_shaper (1.4.0)
activesupport (>= 3.0.0)
csv
@@ -277,7 +277,7 @@ GEM
friendly_id (5.5.1)
activerecord (>= 4.0.0)
gbifrb (0.2.0)
geocoder (1.8.5)
geocoder (1.8.6)
base64 (>= 0.1.0)
csv (>= 3.0.0)
gibbon (1.2.1)
@@ -337,7 +337,7 @@ GEM
terminal-table (>= 1.5.1)
i18n_data (1.1.0)
simple_po_parser (~> 1.1)
icalendar (2.11.2)
icalendar (2.12.0)
base64
ice_cube (~> 0.16)
logger
@@ -356,7 +356,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.13.2)
json (2.15.0)
json-schema (5.1.0)
addressable (~> 2.8)
jsonapi-resources (0.10.7)
@@ -467,7 +467,7 @@ GEM
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
prism (1.5.1)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -475,11 +475,11 @@ GEM
date
stringio
public_suffix (6.0.1)
puma (7.0.3)
puma (7.0.4)
nio4r (~> 2.0)
query_diet (0.7.2)
racc (1.8.1)
rack (2.2.17)
rack (2.2.18)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-protection (3.2.0)
@@ -546,7 +546,7 @@ GEM
recaptcha (5.21.1)
redis-client (0.23.2)
connection_pool
regexp_parser (2.11.2)
regexp_parser (2.11.3)
reline (0.6.2)
io-console (~> 0.5)
responders (3.1.1)
@@ -601,7 +601,7 @@ GEM
rswag-ui (2.16.0)
actionpack (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rubocop (1.80.2)
rubocop (1.81.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -609,10 +609,10 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.46.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.46.0)
rubocop-ast (1.47.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -697,9 +697,9 @@ GEM
timeout (0.4.3)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.5)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
unicorn (6.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)

View File

@@ -7,9 +7,9 @@ module Api
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :garden
has_one :planting
has_one :owner, class_name: 'Member', always_include_linkage_data: true
has_one :garden, always_include_linkage_data: true
has_one :planting, always_include_linkage_data: true
attribute :name
attribute :description

View File

@@ -12,7 +12,7 @@ module Api
has_many :photos
has_one :parent, class_name: 'Crop'
has_one :parent, class_name: 'Crop', always_include_linkage_data: true
attribute :name
attribute :en_wikipedia_url

View File

@@ -7,7 +7,7 @@ module Api
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :owner, class_name: 'Member', always_include_linkage_data: true
has_many :plantings
has_many :photos

View File

@@ -10,9 +10,9 @@ module Api
@model.plant_part = PlantPart.first
end
has_one :crop
has_one :planting
has_one :owner, class_name: 'Member'
has_one :crop, always_include_linkage_data: true
has_one :planting, always_include_linkage_data: true
has_one :owner, class_name: 'Member', always_include_linkage_data: true
# has_one :plant_part
has_many :photos

View File

@@ -9,6 +9,7 @@ module Api
has_many :plantings, foreign_key: 'owner_id'
has_many :harvests, foreign_key: 'owner_id'
has_many :seeds, foreign_key: 'owner_id'
has_many :activities, foreign_key: 'owner_id'
has_many :photos

View File

@@ -8,7 +8,7 @@ module Api
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :owner, class_name: 'Member', always_include_linkage_data: true
has_many :plantings
has_many :gardens
has_many :harvests

View File

@@ -7,9 +7,9 @@ module Api
@model.owner = context[:current_user]
end
has_one :garden
has_one :crop
has_one :owner, class_name: 'Member'
has_one :garden, always_include_linkage_data: true
has_one :crop, always_include_linkage_data: true
has_one :owner, class_name: 'Member', always_include_linkage_data: true
has_many :photos
has_many :harvests

View File

@@ -7,8 +7,8 @@ module Api
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :crop
has_one :owner, class_name: 'Member', always_include_linkage_data: true
has_one :crop, always_include_linkage_data: true
attribute :description
attribute :quantity

142
public/robots.txt Normal file
View File

@@ -0,0 +1,142 @@
# robots.txt for based on the one for http://www.wikipedia.org/ and friends
# Observed spamming large amounts of https://en.wikipedia.org/?curid=NNNNNN
# and ignoring 429 ratelimit responses, claims to respect robots:
# http://mj12bot.com/
User-agent: MJ12bot
Disallow: /
# advertising-related bots:
User-agent: Mediapartners-Google*
Disallow: /
# Wikipedia work bots:
User-agent: IsraBot
Disallow:
User-agent: Orthogaffe
Disallow:
# Crawlers that are kind enough to obey, but which we'd rather not have
# unless they're feeding search engines.
User-agent: UbiCrawler
Disallow: /
User-agent: DOC
Disallow: /
User-agent: Zao
Disallow: /
# Some bots are known to be trouble, particularly those designed to copy
# entire sites. Please obey robots.txt.
User-agent: sitecheck.internetseer.com
Disallow: /
User-agent: Zealbot
Disallow: /
User-agent: MSIECrawler
Disallow: /
User-agent: SiteSnagger
Disallow: /
User-agent: WebStripper
Disallow: /
User-agent: WebCopier
Disallow: /
User-agent: Fetch
Disallow: /
User-agent: Offline Explorer
Disallow: /
User-agent: Teleport
Disallow: /
User-agent: TeleportPro
Disallow: /
User-agent: WebZIP
Disallow: /
User-agent: linko
Disallow: /
User-agent: HTTrack
Disallow: /
User-agent: Microsoft.URL.Control
Disallow: /
User-agent: Xenu
Disallow: /
User-agent: larbin
Disallow: /
User-agent: libwww
Disallow: /
User-agent: ZyBORG
Disallow: /
User-agent: Download Ninja
Disallow: /
# Misbehaving: requests much too fast:
User-agent: fast
Disallow: /
#
# Sorry, wget in its recursive mode is a frequent problem.
# Please read the man page and use it properly; there is a
# --wait option you can use to set the delay between hits,
# for instance.
#
User-agent: wget
Disallow: /
#
# The 'grub' distributed client has been *very* poorly behaved.
#
User-agent: grub-client
Disallow: /
#
# Doesn't follow robots.txt anyway, but...
#
User-agent: k2spider
Disallow: /
#
# Hits many times per second, not acceptable
# http://www.nameprotect.com/botinfo.html
User-agent: NPBot
Disallow: /
# A capture bot, downloads gazillions of pages with no public benefit
# http://www.webreaper.net/
User-agent: WebReaper
Disallow: /
# Per their statement, semrushbot respects crawl-delay directives
# We want them to overall stay within reasonable request rates to
# the backend (20 rps); keeping in mind that the crawl-delay will
# be applied by site and not globally by the bot, 5 seconds seem
# like a reasonable approximation
User-agent: SemrushBot
Crawl-delay: 5
#
# Friendly, low-speed bots are welcome viewing pages, but not
# dynamically-generated pages please.
#
# Another exception is for REST API documentation, located at
# /api/rest_v1/?doc.
#
User-agent: *
Disallow: /api/

View File

@@ -3,25 +3,28 @@
require 'rails_helper'
RSpec.describe 'Activities', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:activity) { FactoryBot.create(:activity, garden: create(:garden), planting: create(:planting)) }
let(:garden) { create(:garden, owner: member) }
let(:planting) { create(:planting, garden: garden) }
let!(:activity) { FactoryBot.create(:activity, garden: garden, planting: planting, owner: member) }
let!(:activity2) { FactoryBot.create(:activity) }
it '#index' do
get('/api/v1/activities', params: {}, headers:)
expect(subject['data'].size).to eq(2)
get('/api/v1/activities', params: {}, headers: headers)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
end
it '#show' do
get("/api/v1/activities/#{activity.id}", params: {}, headers:)
get("/api/v1/activities/#{activity.id}", params: {}, headers: headers)
expect(subject['data']['id']).to eq(activity.id.to_s)
end
context 'filtering' do
it 'filters by owner' do
get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers:)
get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -29,7 +32,7 @@ RSpec.describe 'Activities', type: :request do
end
it 'filters by garden' do
get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:)
get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -37,7 +40,7 @@ RSpec.describe 'Activities', type: :request do
end
it 'filters by planting' do
get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:)
get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -45,12 +48,12 @@ RSpec.describe 'Activities', type: :request do
end
it 'filters by category' do
get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:)
activity2.update!(category: activity.category)
get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(2)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
expect(subject['data'][1]['id']).to eq(activity2.id.to_s)
end
end
end

View File

@@ -3,9 +3,9 @@
require 'rails_helper'
RSpec.describe 'Crops', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:crop) { FactoryBot.create(:crop) }
let(:crop_encoded_as_json_api) do
{ "id" => crop.id.to_s,
@@ -66,13 +66,13 @@ RSpec.describe 'Crops', type: :request do
end
describe '#index' do
before { get '/api/v1/crops', params: {}, headers: }
before { get '/api/v1/crops', params: {}, headers: 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: }
before { get "/api/v1/crops/#{crop.id}", params: {}, headers: headers }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("plantings" => plantings_as_json_api) }
@@ -85,19 +85,19 @@ RSpec.describe 'Crops', type: :request do
it '#create' do
expect do
post '/api/v1/crops', params: { 'crop' => { 'name' => 'can i make this' } }, headers:
post '/api/v1/crops', params: { 'crop' => { 'name' => 'can i make this' } }, headers: 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:
post "/api/v1/crops/#{crop.id}", params: { 'crop' => { 'name' => 'can i modify this' } }, headers: headers
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/crops/#{crop.id}", params: {}, headers:
delete "/api/v1/crops/#{crop.id}", params: {}, headers: headers
end.to raise_error ActionController::RoutingError
end
end

View File

@@ -3,10 +3,10 @@
require 'rails_helper'
RSpec.describe 'Gardens', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:garden) { FactoryBot.create(:garden) }
let!(:garden) { FactoryBot.create(:garden, owner: member) }
let(:garden_encoded_as_json_api) do
{ "id" => garden.id.to_s,
"type" => "gardens",
@@ -41,20 +41,23 @@ RSpec.describe 'Gardens', type: :request do
end
it '#index' do
get('/api/v1/gardens', params: {}, headers:)
get('/api/v1/gardens', params: {}, headers: headers)
expect(subject['data']).to include(garden_encoded_as_json_api)
end
it '#show' do
get("/api/v1/gardens/#{garden.id}", params: {}, headers:)
get("/api/v1/gardens/#{garden.id}", params: {}, headers: 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)) }
let(:garden_type) { create(:garden_type) }
let!(:garden2) { FactoryBot.create(:garden, owner: member, active: false, garden_type: garden_type) }
let!(:other_member_garden) { FactoryBot.create(:garden) }
pending 'filters by active' do
get('/api/v1/gardens?filter[active]=true', params: {}, headers:)
it 'filters by active' do
get('/api/v1/gardens?filter[active]=true', params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -62,7 +65,7 @@ RSpec.describe 'Gardens', type: :request do
end
it 'filters by garden_type' do
get("/api/v1/gardens?filter[garden_type]=#{garden2.garden_type.id}", params: {}, headers:)
get("/api/v1/gardens?filter[garden_type]=#{garden_type.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -70,22 +73,15 @@ RSpec.describe 'Gardens', type: :request do
end
it 'filters by owner' do
get("/api/v1/gardens?filter[owner_id]=#{garden2.owner.id}", params: {}, headers:)
get("/api/v1/gardens?filter[owner_id]=#{member.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(2)
expect(subject['data'][1]['id']).to eq(garden2.id.to_s)
expect(subject['data'].map { |g| g['id'] }).to include(garden.id.to_s, garden2.id.to_s)
end
end
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:garden_params) do
{
data: {
@@ -98,26 +94,19 @@ RSpec.describe 'Gardens', type: :request do
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/gardens', params: garden_params, headers: headers
post '/api/v1/gardens', params: garden_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/gardens', params: garden_params, headers: auth_headers
expect do
post '/api/v1/gardens', params: garden_params, headers: headers
end.to change { member.gardens.count }.by(1)
expect(response).to have_http_status(:created)
expect(member.gardens.count).to eq(2) # 1 from after_create callback, 1 from api
end
end
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:garden) { create(:garden, owner: member) }
let(:other_member_garden) { create(:garden) }
let(:update_params) do
{
@@ -132,12 +121,12 @@ RSpec.describe 'Gardens', type: :request do
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: headers
patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own garden' do
patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: auth_headers
patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: headers
expect(response).to have_http_status(:ok)
expect(garden.reload.name).to eq('An updated garden')
end
@@ -152,35 +141,27 @@ RSpec.describe 'Gardens', type: :request do
}
}
}.to_json
patch "/api/v1/gardens/#{other_member_garden.id}", params: update_params_for_other, headers: auth_headers
patch "/api/v1/gardens/#{other_member_garden.id}", params: update_params_for_other, headers: headers
expect(response).to have_http_status(:forbidden)
end
end
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let!(:garden) { create(:garden, owner: member) }
let(:other_member_garden) { create(:garden) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/gardens/#{garden.id}", headers: headers
delete "/api/v1/gardens/#{garden.id}", headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own garden' do
delete "/api/v1/gardens/#{garden.id}", headers: auth_headers
delete "/api/v1/gardens/#{garden.id}", headers: headers
expect(response).to have_http_status(:no_content)
expect(Garden.find_by(id: garden.id)).to be_nil
end
it 'returns 403 Forbidden for another member\'s garden' do
delete "/api/v1/gardens/#{other_member_garden.id}", headers: auth_headers
delete "/api/v1/gardens/#{other_member_garden.id}", headers: headers
expect(response).to have_http_status(:forbidden)
end
end

View File

@@ -3,10 +3,10 @@
require 'rails_helper'
RSpec.describe 'Harvests', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:harvest) { FactoryBot.create(:harvest) }
let!(:harvest) { FactoryBot.create(:harvest, owner: member) }
let(:harvest_encoded_as_json_api) do
{ "id" => harvest.id.to_s,
"type" => "harvests",
@@ -50,7 +50,7 @@ RSpec.describe 'Harvests', type: :request do
let(:attributes) do
{
"harvested-at" => "2015-09-17",
"harvested-at" => harvest.harvested_at.strftime('%Y-%m-%d'),
"description" => harvest.description,
"unit" => harvest.unit,
"weight-quantity" => harvest.weight_quantity.to_s,
@@ -60,13 +60,13 @@ RSpec.describe 'Harvests', type: :request do
end
describe '#index' do
before { get '/api/v1/harvests', params: {}, headers: }
before { get '/api/v1/harvests', params: {}, headers: 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: }
before { get "/api/v1/harvests/#{harvest.id}", params: {}, headers: headers }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("planting" => planting_as_json_api) }
@@ -77,16 +77,18 @@ RSpec.describe 'Harvests', type: :request do
end
context 'filtering' do
let!(:harvest2) { FactoryBot.create(:harvest, planting: create(:planting)) }
let(:garden) { create(:garden, owner: member) }
let(:planting) { create(:planting, garden: garden) }
let!(:harvest2) { FactoryBot.create(:harvest, planting: planting) }
it 'filters by crop' do
get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers:)
get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers: headers)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
end
it 'filters by planting' do
get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers:)
get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -94,7 +96,7 @@ RSpec.describe 'Harvests', type: :request do
end
it 'filters by plant_part' do
get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:)
get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -102,25 +104,16 @@ RSpec.describe 'Harvests', type: :request do
end
it 'filters by owner' do
get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:)
get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
expect(subject['data'].size).to eq(2)
expect(subject['data'].map { |h| h['id'] }).to include(harvest.id.to_s, harvest2.id.to_s)
end
end
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:planting) { create(:planting, owner: member) }
let(:plant_part) { create(:plant_part) }
let(:harvest_params) do
{
data: {
@@ -130,34 +123,25 @@ RSpec.describe 'Harvests', type: :request do
},
relationships: {
planting: { data: { type: 'plantings', id: planting.id } }
# plant_part: { data: { type: 'plant_parts', id: plant_part.id } }
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/harvests', params: harvest_params, headers: headers
post '/api/v1/harvests', params: harvest_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/harvests', params: harvest_params, headers: auth_headers
expect do
post '/api/v1/harvests', params: harvest_params, headers: headers
end.to change { member.harvests.count }.by(1)
expect(response).to have_http_status(:created)
expect(member.harvests.count).to eq(1)
end
end
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:harvest) { create(:harvest, owner: member) }
let(:other_member_harvest) { create(:harvest) }
let(:update_params) do
{
@@ -172,12 +156,12 @@ RSpec.describe 'Harvests', type: :request do
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: headers
patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own harvest' do
patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: auth_headers
patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: headers
expect(response).to have_http_status(:ok)
expect(harvest.reload.description).to eq('An updated harvest')
@@ -193,35 +177,29 @@ RSpec.describe 'Harvests', type: :request do
}
}
}.to_json
patch "/api/v1/harvests/#{other_member_harvest.id}", params: update_params_for_other, headers: auth_headers
patch "/api/v1/harvests/#{other_member_harvest.id}", params: update_params_for_other, headers: headers
expect(response).to have_http_status(:forbidden)
end
end
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let!(:harvest) { create(:harvest, owner: member) }
let(:other_member_harvest) { create(:harvest) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/harvests/#{harvest.id}", headers: headers
delete "/api/v1/harvests/#{harvest.id}", headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own harvest' do
delete "/api/v1/harvests/#{harvest.id}", headers: auth_headers
garden = harvest.planting.garden
delete "/api/v1/harvests/#{harvest.id}", headers: headers
expect(response).to have_http_status(:no_content)
expect(Garden.find_by(id: harvest.id)).to be_nil
expect(Harvest.find_by(id: harvest.id)).to be_nil
expect(Garden.find_by(id: garden.id)).not_to be_nil
end
it 'returns 403 Forbidden for another member\'s harvest' do
delete "/api/v1/harvests/#{other_member_harvest.id}", headers: auth_headers
delete "/api/v1/harvests/#{other_member_harvest.id}", headers: headers
expect(response).to have_http_status(:forbidden)
end
end

View File

@@ -3,10 +3,9 @@
require 'rails_helper'
RSpec.describe 'Members', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
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",
@@ -17,7 +16,8 @@ RSpec.describe 'Members', type: :request do
"harvests" => harvests_as_json_api,
"photos" => photos_as_json_api,
"plantings" => plantings_as_json_api,
"seeds" => seeds_as_json_api
"seeds" => seeds_as_json_api,
"activities" => activities_as_json_api
} }
end
@@ -41,6 +41,12 @@ RSpec.describe 'Members', type: :request do
"related" => "#{resource_url}/seeds" } }
end
let(:activities_as_json_api) do
{ "links" =>
{ "self" => "#{resource_url}/relationships/activities",
"related" => "#{resource_url}/activities" } }
end
let(:plantings_as_json_api) do
{ "links" =>
{ "self" =>
@@ -61,25 +67,26 @@ RSpec.describe 'Members', type: :request do
end
describe '#index' do
before { get '/api/v1/members', params: {}, headers: }
before { get '/api/v1/members', params: {}, headers: 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: }
before { get "/api/v1/members/#{member.id}", params: {}, headers: 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']['relationships']).to include("activities" => activities_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:
post '/api/v1/members', params: { 'member' => { 'login_name' => 'can i make this' } }, headers: headers
end.to raise_error ActionController::RoutingError
end
@@ -88,13 +95,13 @@ RSpec.describe 'Members', type: :request do
post "/api/v1/members/#{member.id}", params: {
'member' => { 'login_name' => 'can i modify this' }
},
headers:
headers: headers
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/members/#{member.id}", params: {}, headers:
delete "/api/v1/members/#{member.id}", params: {}, headers: headers
end.to raise_error ActionController::RoutingError
end
end

View File

@@ -3,10 +3,10 @@
require 'rails_helper'
RSpec.describe 'Photos', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:photo) { FactoryBot.create(:photo) }
let!(:photo) { FactoryBot.create(:photo, owner: member) }
let(:photo_encoded_as_json_api) do
{ "id" => photo.id.to_s,
"type" => "photos",
@@ -58,13 +58,13 @@ RSpec.describe 'Photos', type: :request do
end
describe '#index' do
before { get '/api/v1/photos', params: {}, headers: }
before { get '/api/v1/photos', params: {}, headers: 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: }
before { get "/api/v1/photos/#{photo.id}", params: {}, headers: headers }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("plantings" => plantings_as_json_api) }
@@ -75,19 +75,19 @@ RSpec.describe 'Photos', type: :request do
it '#create' do
expect do
post '/api/v1/photos', params: { 'photo' => { 'name' => 'can i make this' } }, headers:
post '/api/v1/photos', params: { 'photo' => { 'name' => 'can i make this' } }, headers: 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:
post "/api/v1/photos/#{photo.id}", params: { 'photo' => { 'name' => 'can i modify this' } }, headers: headers
end.to raise_error ActionController::RoutingError
end
it '#delete' do
expect do
delete "/api/v1/photos/#{photo.id}", params: {}, headers:
delete "/api/v1/photos/#{photo.id}", params: {}, headers: headers
end.to raise_error ActionController::RoutingError
end
end

View File

@@ -3,10 +3,10 @@
require 'rails_helper'
RSpec.describe 'Plantings', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:planting) { FactoryBot.create(:planting) }
let!(:planting) { FactoryBot.create(:planting, owner: member) }
let(:planting_encoded_as_json_api) do
{ "id" => planting.id.to_s,
"type" => "plantings",
@@ -56,11 +56,11 @@ RSpec.describe 'Plantings', type: :request do
let(:attributes) do
{
"slug" => planting.slug,
"planted-at" => "2014-07-30",
"planted-at" => planting.planted_at.strftime('%Y-%m-%d'),
"failed" => false,
"finished-at" => nil,
"finished" => false,
"quantity" => 33,
"quantity" => planting.quantity,
"description" => planting.description,
"crop-name" => planting.crop.name,
"crop-slug" => planting.crop.slug,
@@ -79,14 +79,14 @@ RSpec.describe 'Plantings', type: :request do
end
it '#index' do
get('/api/v1/plantings', params: {}, headers:)
get('/api/v1/plantings', params: {}, headers: 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:)
get("/api/v1/plantings/#{planting.id}", params: {}, headers: 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)
@@ -96,13 +96,6 @@ RSpec.describe 'Plantings', type: :request do
end
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:garden) { create(:garden, owner: member) }
let(:planting_params) do
@@ -121,27 +114,19 @@ RSpec.describe 'Plantings', type: :request do
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/plantings', params: planting_params, headers: headers
post '/api/v1/plantings', params: planting_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/plantings', params: planting_params, headers: auth_headers
expect do
post '/api/v1/plantings', params: planting_params, headers: headers
end.to change { member.plantings.count }.by(1)
expect(response).to have_http_status(:created)
expect(member.plantings.count).to eq(1)
end
end
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:planting) { create(:planting, owner: member) }
let(:other_member_planting) { create(:planting) }
let(:update_params) do
{
@@ -156,12 +141,12 @@ RSpec.describe 'Plantings', type: :request do
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: headers
patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own planting' do
patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: auth_headers
patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: headers
expect(response).to have_http_status(:ok)
expect(planting.reload.description).to eq('An updated planting')
@@ -177,83 +162,85 @@ RSpec.describe 'Plantings', type: :request do
}
}
}.to_json
patch "/api/v1/plantings/#{other_member_planting.id}", params: update_params_for_other, headers: auth_headers
patch "/api/v1/plantings/#{other_member_planting.id}", params: update_params_for_other, headers: headers
expect(response).to have_http_status(:forbidden)
end
end
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let!(:planting) { create(:planting, owner: member) }
let(:other_member_planting) { create(:planting) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/plantings/#{planting.id}", headers: headers
delete "/api/v1/plantings/#{planting.id}", headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own planting' do
delete "/api/v1/plantings/#{planting.id}", headers: auth_headers
garden = planting.garden
delete "/api/v1/plantings/#{planting.id}", headers: headers
expect(response).to have_http_status(:no_content)
expect(Garden.find_by(id: planting.id)).to be_nil
expect(Planting.find_by(id: planting.id)).to be_nil
expect(Garden.find_by(id: garden.id)).not_to be_nil
end
it 'returns 403 Forbidden for another member\'s planting' do
delete "/api/v1/plantings/#{other_member_planting.id}", headers: auth_headers
delete "/api/v1/plantings/#{other_member_planting.id}", headers: headers
expect(response).to have_http_status(:forbidden)
end
end
describe "by member/owner" do
before :each do
@member1 = planting.owner
@planting2 = create(:planting, owner: create(:owner))
@member2 = @planting2.owner
let!(:planting2) { create(:planting, owner: create(:owner)) }
let(:member2) { planting2.owner }
describe "on /api/v1/plantings" do
it "filters by owner but respects authorization scope" do
# Filtering by the current member's id should work
get "/api/v1/plantings?filter[owner-id]=#{member.id}", headers: headers
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting.id.to_s)
# Filtering by another member's id should return nothing from the scoped collection
get "/api/v1/plantings?filter[owner-id]=#{member2.id}", headers: headers
expect(response).to have_http_status(:ok)
expect(subject['data']).to be_empty
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)
describe "on /api/v1/members/:id/plantings" do
it "returns plantings for the correct member" do
get "/api/v1/members/#{member.id}/plantings", headers: headers
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting.id.to_s)
end
get "/api/v1/plantings?filter[owner-id]=#{@member2.id}"
expect(JSON.parse(response.body)['data'][0]['id']).to eq(@planting2.id.to_s)
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)
it "returns forbidden when accessing another member's plantings" do
get "/api/v1/members/#{member2.id}/plantings", headers: headers
expect(response).to have_http_status(:forbidden)
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)) }
let!(:planting2) { FactoryBot.create(:planting, owner: member, failed: true, sunniness: 'shade') }
let!(:perennial_planting) { FactoryBot.create(:planting, owner: member, crop: FactoryBot.create(:crop, perennial: true)) }
it 'filters by failed' do
get('/api/v1/plantings?filter[failed]=true', params: {}, headers:)
get('/api/v1/plantings?filter[failed]=true', params: {}, headers: headers)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting2.id.to_s)
end
it 'filters by sunniness' do
get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:)
get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers: headers)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting2.id.to_s)
end
it 'filters by perennial' do
get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:)
get('/api/v1/plantings?filter[perennial]=true', params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -261,11 +248,11 @@ RSpec.describe 'Plantings', type: :request do
end
it 'filters by active' do
get('/api/v1/plantings?filter[active]=true', params: {}, headers:)
get('/api/v1/plantings?filter[active]=true', params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(2)
expect(subject['data'][0]['id']).to eq(planting.id.to_s)
expect(subject['data'].map { |p| p['id'] }).to include(planting.id.to_s, perennial_planting.id.to_s)
end
end
end

View File

@@ -3,10 +3,10 @@
require 'rails_helper'
RSpec.describe 'Seeds', type: :request do
include_context 'with authenticated member'
subject { JSON.parse response.body }
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
let!(:seed) { FactoryBot.create(:seed) }
let!(:seed) { FactoryBot.create(:seed, owner: member) }
let(:seed_encoded_as_json_api) do
{ "id" => seed.id.to_s,
"type" => "seeds",
@@ -36,7 +36,7 @@ RSpec.describe 'Seeds', type: :request do
{
"description" => seed.description,
"quantity" => seed.quantity,
"plant-before" => "2013-07-15",
"plant-before" => seed.plant_before.strftime('%Y-%m-%d'),
"tradable-to" => seed.tradable_to,
"days-until-maturity-min" => seed.days_until_maturity_min,
"days-until-maturity-max" => seed.days_until_maturity_max,
@@ -47,13 +47,13 @@ RSpec.describe 'Seeds', type: :request do
end
describe '#index' do
before { get '/api/v1/seeds', params: {}, headers: }
before { get '/api/v1/seeds', params: {}, headers: 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: }
before { get "/api/v1/seeds/#{seed.id}", params: {}, headers: headers }
it { expect(subject['data']['attributes']).to eq(attributes) }
it { expect(subject['data']['relationships']).to include("owner" => owner_as_json_api) }
@@ -62,13 +62,6 @@ RSpec.describe 'Seeds', type: :request do
end
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:seed_params) do
{
@@ -85,27 +78,19 @@ RSpec.describe 'Seeds', type: :request do
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/seeds', params: seed_params, headers: headers
post '/api/v1/seeds', params: seed_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/seeds', params: seed_params, headers: auth_headers
expect do
post '/api/v1/seeds', params: seed_params, headers: headers
end.to change { member.seeds.count }.by(1)
expect(response).to have_http_status(:created)
expect(member.seeds.count).to eq(1)
end
end
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:seed) { create(:seed, owner: member, crop: crop) }
let(:other_member_seed) { create(:seed) }
let(:update_params) do
{
@@ -120,12 +105,12 @@ RSpec.describe 'Seeds', type: :request do
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: headers
patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own seed' do
patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: auth_headers
patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: headers
expect(response).to have_http_status(:ok)
expect(seed.reload.description).to eq('An updated seed')
end
@@ -140,47 +125,39 @@ RSpec.describe 'Seeds', type: :request do
}
}
}.to_json
patch "/api/v1/seeds/#{other_member_seed.id}", params: update_params_for_other, headers: auth_headers
patch "/api/v1/seeds/#{other_member_seed.id}", params: update_params_for_other, headers: headers
expect(response).to have_http_status(:forbidden)
end
end
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let!(:seed) { create(:seed, owner: member, crop: crop) }
let(:other_member_seed) { create(:seed) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/seeds/#{seed.id}", headers: headers
delete "/api/v1/seeds/#{seed.id}", headers: unauthenticated_headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own seed' do
delete "/api/v1/seeds/#{seed.id}", headers: auth_headers
delete "/api/v1/seeds/#{seed.id}", headers: headers
expect(response).to have_http_status(:no_content)
expect(Seed.find_by(id: seed.id)).to be_nil
end
it 'returns 403 Forbidden for another member\'s seed' do
delete "/api/v1/seeds/#{other_member_seed.id}", headers: auth_headers
delete "/api/v1/seeds/#{other_member_seed.id}", headers: headers
expect(response).to have_http_status(:forbidden)
end
end
context 'filtering' do
let!(:seed2) do
FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom')
FactoryBot.create(:seed, owner: member, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom')
end
let!(:other_member_seed) { create(:seed) }
it 'filters by crop' do
get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers:)
get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -188,7 +165,7 @@ RSpec.describe 'Seeds', type: :request do
end
it 'filters by tradable_to' do
get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers:)
get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -196,7 +173,7 @@ RSpec.describe 'Seeds', type: :request do
end
it 'filters by organic' do
get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers:)
get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -204,7 +181,7 @@ RSpec.describe 'Seeds', type: :request do
end
it 'filters by gmo' do
get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers:)
get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -212,7 +189,7 @@ RSpec.describe 'Seeds', type: :request do
end
it 'filters by heirloom' do
get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers:)
get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
@@ -220,11 +197,14 @@ RSpec.describe 'Seeds', type: :request do
end
it 'filters by owner' do
get("/api/v1/seeds?filter[owner_id]=#{seed2.owner.id}", params: {}, headers:)
get("/api/v1/seeds?filter[owner_id]=#{member.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
expect(subject['data'].size).to eq(2)
expect(subject['data'].map { |s| s['id'] }).to include(seed.id.to_s, seed2.id.to_s)
get("/api/v1/seeds?filter[owner_id]=#{other_member_seed.owner.id}", params: {}, headers: headers)
expect(response).to have_http_status(:ok)
expect(subject['data']).to be_empty
end
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
RSpec.shared_context 'with authenticated member' do
let(:member) { create(:member) }
let(:api_token) { member.regenerate_api_token }
let(:headers) do
{
'Accept' => 'application/vnd.api+json',
'Authorization' => "Token token=#{api_token.token}",
'Content-Type' => 'application/vnd.api+json'
}
end
let(:unauthenticated_headers) do
{
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json'
}
end
end