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.
This commit is contained in:
google-labs-jules[bot]
2025-11-29 03:41:08 +00:00
parent e9a187b3df
commit 1b087bd161
9 changed files with 183 additions and 236 deletions

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",
@@ -68,13 +67,13 @@ 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) }
@@ -87,7 +86,7 @@ RSpec.describe 'Members', type: :request do
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
@@ -96,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