mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-10-31 03:09:06 -04:00
250 lines
9.6 KiB
Ruby
250 lines
9.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
# POST /plantings.json and PATCH /plantings/:slug.json are for our own React garden cards only: session cookie
|
|
# and CSRF token, same origin. It is not part of the public API (/api/v1).
|
|
describe 'Creating and updating a planting as JSON' do
|
|
include Devise::Test::IntegrationHelpers
|
|
|
|
let(:member) { create(:member) }
|
|
let(:garden) { create(:garden, owner: member) }
|
|
let(:crop) { create(:annual_crop, name: 'lettuce') }
|
|
let(:json_headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } }
|
|
|
|
def create_planting(garden_id: garden.id, crop_id: crop.id, headers: json_headers, **attributes)
|
|
post '/plantings.json', params: { planting: { garden_id:, crop_id:, quantity: 3, **attributes } }.to_json,
|
|
headers: headers
|
|
end
|
|
|
|
context 'when signed in' do
|
|
before { sign_in member }
|
|
|
|
it "creates the planting and returns the garden's updated card" do
|
|
expect { create_planting }.to change { garden.plantings.count }.by(1)
|
|
|
|
expect(response).to have_http_status(:created)
|
|
card = response.parsed_body['garden']
|
|
expect(card).to include('id' => garden.id, 'name' => garden.name)
|
|
expect(card['annuals'].map { |planting| planting.dig('crop', 'name') }).to eq ['lettuce']
|
|
expect(garden.plantings.last).to have_attributes(owner: member, crop: crop, quantity: 3)
|
|
end
|
|
|
|
it 'does not include the owner in the card, which the page already has' do
|
|
create_planting
|
|
|
|
expect(response.parsed_body['garden']['owner']).to be_nil
|
|
end
|
|
|
|
it 'defaults the planted date to today' do
|
|
create_planting
|
|
|
|
expect(garden.plantings.last.planted_at).to eq Time.zone.today
|
|
end
|
|
|
|
it 'returns the validation errors as 422 and creates nothing' do
|
|
expect { create_planting(crop_id: nil) }.not_to(change(Planting, :count))
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.parsed_body['errors']).to have_key('crop')
|
|
end
|
|
|
|
it "refuses a garden that is not yours, with a JSON 403 and not a redirect" do
|
|
someone_elses = create(:garden)
|
|
|
|
expect { create_planting(garden_id: someone_elses.id) }.not_to(change(Planting, :count))
|
|
|
|
expect(response).to have_http_status(:forbidden)
|
|
expect(response.parsed_body).to have_key('error')
|
|
end
|
|
|
|
it 'reports a missing garden as a validation error' do
|
|
create_planting(garden_id: nil)
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.parsed_body['errors']).to have_key('garden')
|
|
end
|
|
end
|
|
|
|
describe 'Loading a planting to edit as JSON' do
|
|
let!(:planting) do
|
|
create(:planting, garden: garden, owner: member, crop: crop, planted_at: Date.new(2026, 3, 1),
|
|
quantity: 4, sunniness: 'shade', description: 'By the fence')
|
|
end
|
|
|
|
it 'gives the current values and the choices for the selects' do
|
|
sign_in member
|
|
|
|
get "/plantings/#{planting.slug}/edit.json", headers: json_headers
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(response.parsed_body['planting']).to include(
|
|
'url' => "/plantings/#{planting.slug}", 'crop' => { 'id' => crop.id, 'name' => 'lettuce' },
|
|
'planted_at' => '2026-03-01', 'quantity' => 4, 'sunniness' => 'shade',
|
|
'description' => 'By the fence'
|
|
)
|
|
expect(response.parsed_body['planted_from_values']).to include('seed', 'seedling')
|
|
expect(response.parsed_body['sunniness_values']).to eq %w(sun semi-shade shade)
|
|
end
|
|
|
|
it "refuses someone else's planting" do
|
|
sign_in create(:member)
|
|
|
|
get "/plantings/#{planting.slug}/edit.json", headers: json_headers
|
|
|
|
expect(response).to have_http_status(:forbidden)
|
|
end
|
|
end
|
|
|
|
describe 'Updating a planting as JSON' do
|
|
let!(:planting) { create(:planting, garden: garden, owner: member, crop: crop, planted_at: Date.new(2026, 3, 1)) }
|
|
|
|
def update_planting(attributes, target: planting)
|
|
patch "/plantings/#{target.slug}.json", params: { planting: attributes }.to_json, headers: json_headers
|
|
end
|
|
|
|
context 'when signed in' do
|
|
before { sign_in member }
|
|
|
|
it "changes the planted date and returns the garden's updated card" do
|
|
update_planting({ planted_at: '2026-04-15' })
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(planting.reload.planted_at).to eq Date.new(2026, 4, 15)
|
|
annual = response.parsed_body.dig('garden', 'annuals').find { |row| row['id'] == planting.id }
|
|
expect(annual['planted_at']).to eq '2026-04-15'
|
|
end
|
|
|
|
it 'marks it finished on the date given, and the refreshed card no longer lists it' do
|
|
update_planting({ finished: true, finished_at: '2026-04-15' })
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
expect(planting.reload).to have_attributes(finished: true, finished_at: Date.new(2026, 4, 15))
|
|
expect(response.parsed_body.dig('garden', 'annuals').pluck('id')).not_to include(planting.id)
|
|
end
|
|
|
|
it 'does not finish it on or before the day it was planted' do
|
|
update_planting({ finished: true, finished_at: '2026-03-01' })
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.parsed_body['errors']).to have_key('finished_at')
|
|
expect(planting.reload.finished).to be false
|
|
end
|
|
|
|
it 'returns validation errors as 422 and keeps the old date' do
|
|
planting.update!(finished_at: Date.new(2026, 3, 10))
|
|
|
|
update_planting({ planted_at: '2026-05-01' })
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.parsed_body['errors']).to have_key('finished_at')
|
|
expect(planting.reload.planted_at).to eq Date.new(2026, 3, 1)
|
|
end
|
|
|
|
it "refuses someone else's planting" do
|
|
someone_elses = create(:planting, planted_at: Date.new(2026, 3, 1))
|
|
|
|
update_planting({ planted_at: '2026-04-15' }, target: someone_elses)
|
|
|
|
expect(response).to have_http_status(:forbidden)
|
|
expect(someone_elses.reload.planted_at).to eq Date.new(2026, 3, 1)
|
|
end
|
|
end
|
|
|
|
it 'asks a visitor who is not signed in to sign in' do
|
|
update_planting({ planted_at: '2026-04-15' })
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
expect(planting.reload.planted_at).to eq Date.new(2026, 3, 1)
|
|
end
|
|
end
|
|
|
|
it 'asks a visitor who is not signed in to sign in' do
|
|
expect { create_planting }.not_to(change(Planting, :count))
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
|
|
describe 'CSRF protection' do
|
|
# Each controller class copies this setting when it is defined, so it has to
|
|
# be switched on for PlantingsController itself, not just ActionController::Base.
|
|
around do |example|
|
|
original = PlantingsController.allow_forgery_protection
|
|
PlantingsController.allow_forgery_protection = true
|
|
example.run
|
|
ensure
|
|
PlantingsController.allow_forgery_protection = original
|
|
end
|
|
|
|
# A real cookie session, as a browser has. (The sign_in test helper bypasses
|
|
# the session, so it can't show what CSRF protection does.)
|
|
before do
|
|
post member_session_path, params: { member: { login: member.email, password: 'password1' } }
|
|
end
|
|
|
|
it 'treats a request without the token as signed out, and creates nothing' do
|
|
expect { create_planting }.not_to(change(Planting, :count))
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
|
|
it 'accepts a request carrying the token from the page' do
|
|
get new_planting_path
|
|
token = Capybara.string(response.body).find('meta[name=csrf-token]', visible: :all)['content']
|
|
|
|
expect { create_planting(headers: json_headers.merge('X-CSRF-Token' => token)) }
|
|
.to change { garden.plantings.count }.by(1)
|
|
expect(response).to have_http_status(:created)
|
|
end
|
|
|
|
it 'rejects a wrong token' do
|
|
expect { create_planting(headers: json_headers.merge('X-CSRF-Token' => 'not-the-token')) }
|
|
.not_to(change(Planting, :count))
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
describe 'cross-origin requests' do
|
|
before { sign_in member }
|
|
|
|
it 'get no CORS headers, so other sites cannot call it from a browser' do
|
|
create_planting(headers: json_headers.merge('Origin' => 'https://example.com'))
|
|
|
|
expect(response.headers.keys.map(&:downcase)).not_to include('access-control-allow-origin')
|
|
end
|
|
|
|
it 'get no CORS headers on a preflight either' do
|
|
process :options, '/plantings.json', headers: { 'Origin' => 'https://example.com',
|
|
'Access-Control-Request-Method' => 'POST' }
|
|
|
|
expect(response.headers.keys.map(&:downcase)).not_to include('access-control-allow-origin')
|
|
end
|
|
end
|
|
|
|
describe 'the new planting form, when arriving from a garden' do
|
|
before { sign_in member }
|
|
|
|
it 'does not ask which garden, and posts the one you came from' do
|
|
get new_planting_path(garden_id: garden.id)
|
|
|
|
page = Capybara.string(response.body)
|
|
expect(page).to have_css("input[type=hidden][name='planting[garden_id]'][value='#{garden.id}']", visible: :all)
|
|
expect(page).to have_text "Planting in #{garden.name}"
|
|
expect(page).to have_no_text 'Where did you plant it?'
|
|
end
|
|
|
|
it 'still asks when it is not from one of your gardens' do
|
|
get new_planting_path(garden_id: create(:garden).id)
|
|
|
|
expect(Capybara.string(response.body)).to have_text 'Where did you plant it?'
|
|
end
|
|
|
|
it 'still asks when no garden was given' do
|
|
get new_planting_path
|
|
|
|
expect(Capybara.string(response.body)).to have_text 'Where did you plant it?'
|
|
end
|
|
end
|
|
end
|