Files
growstuff/spec/models/planting_spec.rb
Daniel O'Connor ae639b6e74 Adjust expectation
2025-12-13 02:12:20 +00:00

612 lines
23 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe Planting do
let(:crop) { FactoryBot.create(:tomato) }
let(:garden_owner) { FactoryBot.create(:member, login_name: 'hatupatu') }
let(:garden) { FactoryBot.create(:garden, owner: garden_owner, name: 'Springfield Community Garden') }
let(:planting) { FactoryBot.create(:planting, crop:, garden:, owner: garden.owner) }
describe 'planting lifespan predictions' do
context 'no predications data yet' do
describe 'planting planted, not finished' do
let(:planting) { FactoryBot.create(:planting, planted_at: 30.days.ago, finished_at: nil, finished: false) }
it { expect(planting.crop.median_lifespan).to be_nil }
it { expect(planting.expected_lifespan).to be_nil }
it { expect(planting.age_in_days).to eq(30) }
it { expect(planting.percentage_grown).to be_nil }
end
describe 'planting not planted yet' do
let(:planting) { FactoryBot.create(:planting, planted_at: nil, finished_at: nil, finished: false) }
it { expect(planting.crop.median_lifespan).to be_nil }
it { expect(planting.expected_lifespan).to be_nil }
it { expect(planting.age_in_days).to be_nil }
it { expect(planting.percentage_grown).to eq(0) }
end
describe 'planting finished, no planted_at' do
let(:planting) { FactoryBot.create(:planting, planted_at: nil, finished_at: 1.day.ago, finished: true) }
it { expect(planting.crop.median_lifespan).to be_nil }
it { expect(planting.expected_lifespan).to be_nil }
it { expect(planting.age_in_days).to be_nil }
it { expect(planting.percentage_grown).to eq(100) }
end
describe 'planting all finished' do
let(:planting) { FactoryBot.create(:planting, planted_at: 30.days.ago, finished_at: 10.days.ago, finished: true) }
it { expect(planting.crop.median_lifespan).to be_nil }
it { expect(planting.expected_lifespan).to eq(20) }
it { expect(planting.age_in_days).to eq(20) }
it { expect(planting.percentage_grown).to eq(100) }
end
describe 'planting finishing in the future' do
let(:planting) { FactoryBot.create(:planting, planted_at: 30.days.ago, finished_at: 10.days.from_now, finished: false) }
it { expect(planting.expected_lifespan).to eq(40) }
it { expect(planting.age_in_days).to eq(30) }
it { expect(planting.percentage_grown).to eq(75) }
end
end
context 'lots of data' do
before do
FactoryBot.create(:planting, crop: planting.crop, planted_at: 10.days.ago)
FactoryBot.create(:planting, crop: planting.crop, planted_at: 100.days.ago, finished_at: 50.days.ago)
FactoryBot.create(:planting, crop: planting.crop, planted_at: 100.days.ago, finished_at: 51.days.ago)
FactoryBot.create(:planting, crop: planting.crop, planted_at: 2.years.ago, finished_at: 50.days.ago)
FactoryBot.create(:planting, crop: planting.crop, planted_at: 150.days.ago, finished_at: 100.days.ago)
planting.crop.update_lifespan_medians
end
it { expect(planting.crop.median_lifespan).to eq 50 }
describe 'planting 30 days ago, not finished' do
let(:planting) { FactoryBot.create(:planting, planted_at: 30.days.ago) }
# 30 / 50 = 60%
it { expect(planting.percentage_grown).to eq 60.0 }
# planted 30 days ago
it { expect(planting.age_in_days).to eq 30 }
# means 20 days to go
it { expect(planting.finish_predicted_at).to eq Time.zone.today + 20.days }
end
describe 'child crop uses parent data' do
let(:child_crop) { FactoryBot.create(:crop, parent: crop, name: 'child') }
let(:child_planting) { FactoryBot.create(:planting, crop: child_crop, planted_at: 30.days.ago) }
# not data for this crop
it { expect(child_crop.median_lifespan).to be_nil }
# 30 / 50 = 60%
it { expect(child_planting.percentage_grown).to eq 60.0 }
# planted 30 days ago
it { expect(child_planting.age_in_days).to eq 30 }
# means 20 days to go
it { expect(child_planting.finish_predicted_at).to eq Time.zone.today + 20.days }
end
describe 'planting not planted yet' do
let(:planting) { FactoryBot.create(:planting, planted_at: nil, finished_at: nil) }
it { expect(planting.percentage_grown).to eq 0 }
end
describe 'planting finished 10 days, but was never planted' do
let(:planting) { FactoryBot.create(:planting, planted_at: nil, finished_at: 10.days.ago) }
it { expect(planting.percentage_grown).to eq 100 }
end
describe 'planted 30 days ago, finished 10 days ago' do
let(:planting) { FactoryBot.create(:planting, planted_at: 30.days.ago, finished_at: 10.days.ago) }
it { expect(planting.age_in_days).to eq 20 }
it { expect(planting.percentage_grown).to eq 100 }
end
end
end
describe 'planting first harvest preductions' do
context 'no data' do
let(:planting) { FactoryBot.create(:planting) }
it { expect(planting.crop.median_days_to_first_harvest).to be_nil }
it { expect(planting.crop.median_days_to_last_harvest).to be_nil }
it { expect(planting.days_to_first_harvest).to be_nil }
it { expect(planting.days_to_last_harvest).to be_nil }
it { expect(planting.expected_lifespan).to be_nil }
end
context 'lots of data' do
let(:crop) { FactoryBot.create(:crop) }
# this is a method so it creates a new one each time
def one_hundred_day_old_planting
FactoryBot.create(:planting, crop:, planted_at: 100.days.ago)
end
before do
# 50 days to harvest
FactoryBot.create(:harvest, harvested_at: 50.days.ago, crop: planting.crop,
planting: one_hundred_day_old_planting)
# 20 days to harvest
FactoryBot.create(:harvest, harvested_at: 80.days.ago, crop: planting.crop,
planting: one_hundred_day_old_planting)
# 10 days to harvest
FactoryBot.create(:harvest, harvested_at: 90.days.ago, crop: planting.crop,
planting: one_hundred_day_old_planting)
planting.crop.plantings.each(&:update_harvest_days!)
planting.crop.update_lifespan_medians
planting.crop.update_harvest_medians
end
it { expect(crop.median_days_to_first_harvest).to eq(20) }
describe 'sets median time to harvest' do
let(:planting) { FactoryBot.create(:planting, crop:, planted_at: Time.zone.today) }
it { expect(planting.first_harvest_predicted_at).to eq(Time.zone.today + 20.days) }
end
describe 'harvest still growing' do
let(:planting) { FactoryBot.create(:planting, crop:, planted_at: Time.zone.today) }
it { expect(planting.before_harvest_time?).to be true }
it { expect(planting.harvest_time?).to be false }
end
describe 'harvesting ready now' do
let(:planting) { FactoryBot.create(:planting, crop:, planted_at: 21.days.ago) }
it { expect(planting.first_harvest_predicted_at).to eq(1.day.ago.to_date) }
it { expect(planting.before_harvest_time?).to be false }
it { expect(planting.harvest_time?).to be true }
end
end
describe 'planting has no harvests' do
let(:planting) { FactoryBot.create(:planting) }
before do
planting.update_harvest_days!
planting.crop.update_harvest_medians
end
it { expect(planting.days_to_first_harvest).to be_nil }
it { expect(planting.days_to_last_harvest).to be_nil }
end
describe 'planting has first harvest' do
let(:planting) { FactoryBot.create(:planting, planted_at: 100.days.ago) }
before do
FactoryBot.create(:harvest,
planting:,
crop: planting.crop,
harvested_at: 10.days.ago)
planting.update_harvest_days!
planting.crop.update_harvest_medians
end
it { expect(planting.days_to_first_harvest).to eq(90) }
it { expect(planting.days_to_last_harvest).to be_nil }
it { expect(planting.crop.median_days_to_first_harvest).to eq(90) }
it { expect(planting.crop.median_days_to_last_harvest).to be_nil }
end
describe 'planting has last harvest' do
let(:planting) { FactoryBot.create(:planting, planted_at: 100.days.ago, finished_at: 1.day.ago, finished: true) }
before do
FactoryBot.create(:harvest, planting:, crop: planting.crop, harvested_at: 90.days.ago)
FactoryBot.create(:harvest, planting:, crop: planting.crop, harvested_at: 10.days.ago)
planting.update_harvest_days!
planting.crop.update_harvest_medians
end
it { expect(planting.days_to_first_harvest).to eq(10) }
it { expect(planting.days_to_last_harvest).to eq(90) }
it { expect(planting.crop.median_days_to_first_harvest).to eq(10) }
it { expect(planting.crop.median_days_to_last_harvest).to eq(90) }
end
end
describe 'planting perennial' do
let(:crop) { FactoryBot.create(:crop, name: 'feijoa', perennial: true) }
it { expect(planting.perennial?).to be true }
describe 'no harvest to predict from' do
it { expect(planting.harvest_months).to eq({}) }
end
describe 'harvests used to predict' do
before do
FactoryBot.create(:harvest, planting:, crop:, harvested_at: '1 May 2019')
FactoryBot.create(:harvest, planting:, crop:, harvested_at: '18 June 2019')
FactoryBot.create_list(:harvest, 4, planting:, crop:, harvested_at: '18 August 2019')
end
it { expect(planting.harvest_months).to eq(5 => 1, 6 => 1, 8 => 4) }
end
describe 'nearby plantings used to predict' do
# Note the locations used need to be stubbed in geocoder
let(:garden) { FactoryBot.create(:garden, location: 'Edinburgh', owner: garden_owner) }
before do
# Near by planting with harvests
nearby_garden = FactoryBot.create(:garden, location: 'Greenwich, UK')
nearby_planting = FactoryBot.create(:planting,
crop:,
garden: nearby_garden,
owner: nearby_garden.owner,
planted_at: '1 January 2000')
FactoryBot.create(:harvest, planting: nearby_planting, crop:,
harvested_at: '1 May 2019')
FactoryBot.create(:harvest, planting: nearby_planting, crop:,
harvested_at: '18 June 2019')
FactoryBot.create_list(:harvest, 4, planting: nearby_planting, crop:,
harvested_at: '18 August 2008')
# far away planting harvests
faraway_garden = FactoryBot.create(:garden, location: 'Amundsen-Scott Base, Antarctica')
faraway_planting = FactoryBot.create(:planting, garden: faraway_garden, crop:,
owner: faraway_garden.owner, planted_at: '16 May 2001')
FactoryBot.create_list(:harvest, 4, planting: faraway_planting, crop:,
harvested_at: '18 December 2006')
end
it { expect(planting.harvest_months).to eq(5 => 1, 6 => 1, 8 => 4) }
end
end
it 'has an owner' do
planting.owner.should be_an_instance_of Member
end
it "generates a location" do
planting.location.should eq garden.location
end
it "has a slug" do
planting.slug.should match(/^hatupatu-springfield-community-garden-tomato$/)
end
it 'sorts in reverse creation order' do
@planting2 = FactoryBot.create(:planting)
described_class.first.should eq @planting2
end
describe '#planted?' do
it "is false for future plantings" do
planting = FactoryBot.create(:planting, planted_at: Time.zone.today + 1)
expect(planting.planted?).to be(false)
end
it "is false for never planted" do
planting = FactoryBot.create(:planting, planted_at: nil)
expect(planting.planted?).to be(false)
end
it "is true for past plantings" do
planting = FactoryBot.create(:planting, planted_at: Time.zone.today - 1)
expect(planting.planted?).to be(true)
end
end
context 'delegation' do
it 'system name' do
planting.crop_name.should eq planting.crop.name
end
it 'wikipedia url' do
planting.crop_en_wikipedia_url.should eq planting.crop.en_wikipedia_url
end
it 'default scientific name' do
planting.crop_default_scientific_name.should eq planting.crop.default_scientific_name
end
it 'plantings count' do
planting.crop_plantings_count.should eq planting.crop.plantings_count
end
end
context 'quantity' do
it 'allows integer quantities' do
@planting = FactoryBot.build(:planting, quantity: 99)
@planting.should be_valid
end
it "doesn't allow decimal quantities" do
@planting = FactoryBot.build(:planting, quantity: 99.9)
@planting.should_not be_valid
end
it "doesn't allow non-numeric quantities" do
@planting = FactoryBot.build(:planting, quantity: 'foo')
@planting.should_not be_valid
end
it "allows blank quantities" do
@planting = FactoryBot.build(:planting, quantity: nil)
@planting.should be_valid
@planting = FactoryBot.build(:planting, quantity: '')
@planting.should be_valid
end
end
context 'sunniness' do
let(:planting) { FactoryBot.create(:sunny_planting) }
it 'has a sunniness value' do
planting.sunniness.should eq 'sun'
end
it 'all three valid sunniness values should work' do
['sun', 'shade', 'semi-shade', nil, ''].each do |s|
@planting = FactoryBot.build(:planting, sunniness: s)
@planting.should be_valid
end
end
it 'refuses invalid sunniness values' do
@planting = FactoryBot.build(:planting, sunniness: 'not valid')
@planting.should_not be_valid
@planting.errors[:sunniness].should include("not valid is not a valid sunniness value")
end
end
context 'planted from' do
it 'has a planted_from value' do
@planting = FactoryBot.create(:seed_planting)
@planting.planted_from.should eq 'seed'
end
it 'all valid planted_from values should work' do
[
'seed', 'seedling', 'cutting', 'root division',
'runner', 'bare root plant', 'advanced plant',
'graft', 'layering', 'bulb', 'root/tuber', nil, ''
].each do |p|
@planting = FactoryBot.build(:planting, planted_from: p)
@planting.should be_valid
end
end
it 'refuses invalid planted_from values' do
@planting = FactoryBot.build(:planting, planted_from: 'not valid')
@planting.should_not be_valid
@planting.errors[:planted_from].should include("not valid is not a valid planting method")
end
end
# we decided that all the tests for the planting/photo association would
# be done on this side, not on the photos side
context 'photos' do
let(:planting) { FactoryBot.create(:planting) }
let(:photo) { FactoryBot.create(:photo, owner_id: planting.owner_id) }
before { planting.photos << photo }
it 'has a photo' do
expect(planting.photos.first).to eq photo
end
it 'is found in has_photos scope' do
expect(described_class.has_photos).to include(planting)
end
it 'deletes association with photos when photo is deleted' do
photo.destroy
planting.reload
expect(planting.photos).to be_empty
end
it 'has a default photo' do
expect(planting.default_photo).to eq photo
end
it 'chooses the most recent photo' do
@photo2 = FactoryBot.create(:photo, owner: planting.owner)
planting.photos << @photo2
expect(planting.default_photo).to eq @photo2
end
end
context 'interesting plantings' do
describe 'picks up interesting plantings' do
before do
# plantings have members created implicitly for them
# each member is different, hence these are all interesting
@planting1 = FactoryBot.create(:planting, :with_photo, planted_at: 5.days.ago)
@planting2 = FactoryBot.create(:planting, :with_photo, planted_at: 4.days.ago)
@planting3 = FactoryBot.create(:planting, :with_photo, planted_at: 3.days.ago)
@planting4 = FactoryBot.create(:planting, :with_photo, planted_at: 2.days.ago)
end
it { expect(described_class.interesting).to eq([@planting4, @planting3, @planting2, @planting1]) }
end
context "default arguments" do
it 'ignores plantings without photos' do
# first, an interesting planting
@planting = FactoryBot.create(:planting)
@planting.photos << FactoryBot.create(:photo, owner: @planting.owner)
@planting.save
# this one doesn't have a photo
@no_photo_planting = FactoryBot.create(:planting)
expect(described_class.interesting).to include @planting
expect(described_class.interesting).not_to include @no_photo_planting
end
it 'ignores plantings with the same owner' do
# this planting is older
@planting1 = FactoryBot.create(:planting, created_at: 1.day.ago)
@planting1.photos << FactoryBot.create(:photo, owner_id: @planting1.owner_id)
@planting1.save
# this one is newer, and has the same owner, through the garden
@planting2 = FactoryBot.create(:planting,
created_at: 1.minute.ago,
garden: @planting1.garden,
owner: @planting1.owner)
@planting2.photos << FactoryBot.create(:photo, owner: @planting2.owner)
@planting2.save
# result: the newer one is interesting, the older one isn't
expect(described_class.interesting).to include @planting2
expect(described_class.interesting).not_to include @planting1
end
end
context "with howmany argument" do
it "only returns the number asked for" do
@plantings = FactoryBot.create_list(:planting, 10)
@plantings.each do |p|
p.photos << FactoryBot.create(:photo, owner: p.owner)
end
expect(described_class.interesting.limit(3).count).to eq 3
end
end
end # interesting plantings
context "finished" do
it 'has finished fields' do
@planting = FactoryBot.create(:finished_planting)
@planting.finished.should be true
@planting.finished_at.should be_an_instance_of Date
end
it 'has finished scope' do
@p = FactoryBot.create(:planting)
@f = FactoryBot.create(:finished_planting)
described_class.finished.should include @f
described_class.finished.should_not include @p
end
it 'has current scope' do
@p = FactoryBot.create(:planting)
@f = FactoryBot.create(:finished_planting)
described_class.current.should include @p
described_class.current.should_not include @f
end
context "finished date validation" do
it 'requires finished date after planting date' do
@f = FactoryBot.build(:finished_planting, planted_at: '2014-01-01', finished_at: '2013-01-01')
@f.should_not be_valid
end
it 'allows just the planted date' do
@f = FactoryBot.build(:planting, planted_at: '2013-01-01', finished_at: nil)
@f.should be_valid
end
it 'allows just the finished date' do
@f = FactoryBot.build(:planting, finished_at: '2013-01-01', planted_at: nil)
@f.should be_valid
end
end
end
context "failed" do
let(:failed_planting) { FactoryBot.create(:planting, failed: true) }
it 'has a failed field' do
expect(failed_planting.failed).to be true
end
it 'has a failed scope' do
@p = FactoryBot.create(:planting)
@f = FactoryBot.create(:planting, failed: true)
described_class.failed.should include @f
described_class.failed.should_not include @p
end
it 'is not included in the active scope' do
@p = FactoryBot.create(:planting)
@f = FactoryBot.create(:planting, failed: true)
described_class.active.should include @p
described_class.active.should_not include @f
end
it 'cannot be finished and failed' do
@f = FactoryBot.build(:planting, finished: true, failed: true)
@f.should_not be_valid
end
it 'is not finished' do
@f = FactoryBot.build(:planting, finished: true, failed: true)
expect(@f.finished?).to be false
end
end
it 'excludes deleted members' do
expect(described_class.joins(:owner).all).to include(planting)
planting.owner.destroy
expect(described_class.joins(:owner).all).not_to include(planting)
end
context 'ancestry' do
let(:parent_seed) { FactoryBot.create(:seed) }
let(:planting) { FactoryBot.create(:planting, parent_seed:) }
it "planting has a parent seed" do
expect(planting.parent_seed).to eq(parent_seed)
end
it "seed has a child planting" do
expect(parent_seed.child_plantings).to eq [planting]
end
describe 'grandchildren' do
let(:grandchild_seed) { FactoryBot.create(:seed, parent_planting: planting) }
it { expect(grandchild_seed.parent_planting).to eq planting }
it { expect(grandchild_seed.parent_planting.parent_seed).to eq parent_seed }
end
end
describe 'active scope' do
let(:member) { FactoryBot.create(:member) }
let!(:planting) do
FactoryBot.create(:planting, owner: member, garden: member.gardens.first)
end
let!(:finished_planting) do
FactoryBot.create(:finished_planting, owner: member, garden: member.gardens.first)
end
let!(:failed_planting) do
FactoryBot.create(:planting, failed: true, owner: member, garden: member.gardens.first)
end
it { expect(member.plantings.active).to include(planting) }
it { expect(member.plantings.active).not_to include(finished_planting) }
it { expect(member.plantings.active).not_to include(failed_planting) }
end
describe 'homepage', :search do
subject { described_class.homepage_records(100) }
let!(:interesting_planting) { FactoryBot.create(:planting, :reindex, :with_photo) }
let!(:finished_interesting_planting) { FactoryBot.create(:finished_planting, :reindex, :with_photo) }
let!(:planting) { FactoryBot.create(:planting, :reindex) }
before { described_class.reindex }
it { expect(subject.count).to eq 3 }
it { expect(subject.map(&:id)).to eq([interesting_planting.id.to_s, finished_interesting_planting.id.to_s, planting.id.to_s]) }
end
end