12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094 |
- require 'rails_helper'
- describe Agent do
- it_behaves_like WorkingHelpers
- describe '.active/inactive' do
- let(:agent) { agents(:jane_website_agent) }
- it 'is active per default' do
- expect(Agent.active).to include(agent)
- expect(Agent.inactive).not_to include(agent)
- end
- it 'is not active when disabled' do
- agent.update_attribute(:disabled, true)
- expect(Agent.active).not_to include(agent)
- expect(Agent.inactive).to include(agent)
- end
- it 'is not active when deactivated' do
- agent.update_attribute(:deactivated, true)
- expect(Agent.active).not_to include(agent)
- expect(Agent.inactive).to include(agent)
- end
- it 'is not active when disabled and deactivated' do
- agent.update_attribute(:disabled, true)
- agent.update_attribute(:deactivated, true)
- expect(Agent.active).not_to include(agent)
- expect(Agent.inactive).to include(agent)
- end
- end
- describe ".bulk_check" do
- before do
- @weather_agent_count = Agents::WeatherAgent.where(schedule: "midnight", disabled: false).count
- end
- it "should run all Agents with the given schedule" do
- expect(Agents::WeatherAgent).to receive(:async_check).with(anything).exactly(@weather_agent_count).times
- Agents::WeatherAgent.bulk_check("midnight")
- end
- it "should skip disabled Agents" do
- agents(:bob_weather_agent).update_attribute :disabled, true
- expect(Agents::WeatherAgent).to receive(:async_check).with(anything).exactly(@weather_agent_count - 1).times
- Agents::WeatherAgent.bulk_check("midnight")
- end
- it "should skip agents of deactivated accounts" do
- agents(:bob_weather_agent).user.deactivate!
- expect(Agents::WeatherAgent).to receive(:async_check).with(anything).exactly(@weather_agent_count - 1).times
- Agents::WeatherAgent.bulk_check("midnight")
- end
- end
- describe ".run_schedule" do
- before do
- expect(Agents::WeatherAgent.count).to be > 0
- expect(Agents::WebsiteAgent.count).to be > 0
- end
- it "runs agents with the given schedule" do
- weather_agent_ids = [agents(:bob_weather_agent), agents(:jane_weather_agent)].map(&:id)
- expect(Agents::WeatherAgent).to receive(:async_check) { |agent_id| weather_agent_ids.delete(agent_id) }.twice
- expect(Agents::WebsiteAgent).to receive(:async_check).with(agents(:bob_website_agent).id)
- Agent.run_schedule("midnight")
- expect(weather_agent_ids).to be_empty
- end
- it "groups agents by type" do
- expect(Agents::WeatherAgent).to receive(:bulk_check).with("midnight").once
- expect(Agents::WebsiteAgent).to receive(:bulk_check).with("midnight").once
- Agent.run_schedule("midnight")
- end
- it "ignores unknown types" do
- Agent.where(id: agents(:bob_weather_agent).id).update_all type: 'UnknownTypeAgent'
- expect(Agents::WeatherAgent).to receive(:bulk_check).with("midnight").once
- expect(Agents::WebsiteAgent).to receive(:bulk_check).with("midnight").once
- Agent.run_schedule("midnight")
- end
- it "only runs agents with the given schedule" do
- expect(Agents::WebsiteAgent).not_to receive(:async_check)
- Agent.run_schedule("blah")
- end
- it "will not run the 'never' schedule" do
- agents(:bob_weather_agent).update_attribute 'schedule', 'never'
- expect(Agents::WebsiteAgent).not_to receive(:async_check)
- Agent.run_schedule("never")
- end
- end
- describe "credential" do
- let(:agent) { agents(:bob_weather_agent) }
- it "should return the value of the credential when credential is present" do
- expect(agent.credential("aws_secret")).to eq(user_credentials(:bob_aws_secret).credential_value)
- end
- it "should return nil when credential is not present" do
- expect(agent.credential("non_existing_credential")).to eq(nil)
- end
- it "should memoize the load" do
- count = 0
- allow_any_instance_of(UserCredential).to receive(:credential_value) { count += 1 }.and_return("foo")
- expect { expect(agent.credential("aws_secret")).to eq("foo") }.to change { count }.by(1)
- expect { expect(agent.credential("aws_secret")).to eq("foo") }.not_to change { count }
- agent.reload
- expect { expect(agent.credential("aws_secret")).to eq("foo") }.to change { count }.by(1)
- expect { expect(agent.credential("aws_secret")).to eq("foo") }.not_to change { count }
- end
- end
- describe "changes to type" do
- it "validates types" do
- source = Agent.new
- source.type = "Agents::WeatherAgent"
- expect(source).to have(0).errors_on(:type)
- source.type = "Agents::WebsiteAgent"
- expect(source).to have(0).errors_on(:type)
- source.type = "Agents::Fake"
- expect(source).to have(1).error_on(:type)
- end
- it "disallows changes to type once a record has been saved" do
- source = agents(:bob_website_agent)
- source.type = "Agents::WeatherAgent"
- expect(source).to have(1).error_on(:type)
- end
- it "should know about available types" do
- expect(Agent.types).to include(Agents::WeatherAgent, Agents::WebsiteAgent)
- end
- end
- describe "with an example Agent" do
- class Agents::SomethingSource < Agent
- default_schedule "2pm"
- def check
- create_event payload: {}
- end
- def validate_options
- errors.add(:base, "bad is bad") if options[:bad]
- end
- end
- class Agents::CannotBeScheduled < Agent
- cannot_be_scheduled!
- def receive(events)
- events.each do |_event|
- create_event payload: { events_received: 1 }
- end
- end
- end
- before do
- allow(Agents::SomethingSource).to receive(:valid_type?).with("Agents::SomethingSource") { true }
- allow(Agents::CannotBeScheduled).to receive(:valid_type?).with("Agents::CannotBeScheduled") { true }
- end
- describe Agents::SomethingSource do
- let(:new_instance) do
- agent = Agents::SomethingSource.new(name: "some agent")
- agent.user = users(:bob)
- agent
- end
- it_behaves_like LiquidInterpolatable
- it_behaves_like HasGuid
- end
- describe ".short_type" do
- it "returns a short name without 'Agents::'" do
- expect(Agents::SomethingSource.new.short_type).to eq("SomethingSource")
- expect(Agents::CannotBeScheduled.new.short_type).to eq("CannotBeScheduled")
- end
- end
- describe ".default_schedule" do
- it "stores the default on the class" do
- expect(Agents::SomethingSource.default_schedule).to eq("2pm")
- expect(Agents::SomethingSource.new.default_schedule).to eq("2pm")
- end
- it "sets the default on new instances, allows setting new schedules, and prevents invalid schedules" do
- @checker = Agents::SomethingSource.new(name: "something")
- @checker.user = users(:bob)
- expect(@checker.schedule).to eq("2pm")
- @checker.save!
- expect(@checker.reload.schedule).to eq("2pm")
- @checker.update_attribute :schedule, "5pm"
- expect(@checker.reload.schedule).to eq("5pm")
- expect(@checker.reload.schedule).to eq("5pm")
- @checker.schedule = "this_is_not_real"
- expect(@checker).to have(1).errors_on(:schedule)
- end
- it "should have an empty schedule if it cannot_be_scheduled" do
- @checker = Agents::CannotBeScheduled.new(name: "something")
- @checker.user = users(:bob)
- expect(@checker.schedule).to be_nil
- expect(@checker).to be_valid
- @checker.schedule = "5pm"
- @checker.save!
- expect(@checker.schedule).to be_nil
- @checker.schedule = "5pm"
- expect(@checker).to have(0).errors_on(:schedule)
- expect(@checker.schedule).to be_nil
- end
- end
- describe "#create_event" do
- before do
- @checker = Agents::SomethingSource.new(name: "something")
- @checker.user = users(:bob)
- @checker.save!
- end
- it "should use the checker's user" do
- @checker.check
- expect(Event.last.user).to eq(@checker.user)
- end
- it "should log an error if the Agent has been marked with 'cannot_create_events!'" do
- expect(@checker).to receive(:can_create_events?) { false }
- expect {
- @checker.check
- }.not_to change { Event.count }
- expect(@checker.logs.first.message).to match(/cannot create events/i)
- end
- end
- describe ".async_check" do
- before do
- @checker = Agents::SomethingSource.new(name: "something")
- @checker.user = users(:bob)
- @checker.save!
- end
- it "records last_check_at and calls check on the given Agent" do
- expect(@checker).to receive(:check).once {
- @checker.options[:new] = true
- }
- allow(Agent).to receive(:find).with(@checker.id) { @checker }
- expect(@checker.last_check_at).to be_nil
- Agents::SomethingSource.async_check(@checker.id)
- expect(@checker.reload.last_check_at).to be_within(2).of(Time.now)
- expect(@checker.reload.options[:new]).to be_truthy # Show that we save options
- end
- it "should log exceptions" do
- expect(@checker).to receive(:check).once {
- raise "foo"
- }
- expect(Agent).to receive(:find).with(@checker.id) { @checker }
- expect {
- Agents::SomethingSource.async_check(@checker.id)
- }.to raise_error(RuntimeError)
- log = @checker.logs.first
- expect(log.message).to match(/Exception/)
- expect(log.level).to eq(4)
- end
- it "should not run disabled Agents" do
- expect(Agent).to receive(:find).with(agents(:bob_weather_agent).id) { agents(:bob_weather_agent) }
- expect(agents(:bob_weather_agent)).not_to receive(:check)
- agents(:bob_weather_agent).update_attribute :disabled, true
- Agent.async_check(agents(:bob_weather_agent).id)
- end
- end
- describe ".receive!" do
- before do
- stub_request(:any, /pirateweather/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/weather.json")),
- status: 200)
- end
- it "should use available events" do
- Agent.async_check(agents(:bob_weather_agent).id)
- expect(Agent).to receive(:async_receive).with(agents(:bob_rain_notifier_agent).id, anything).once
- Agent.receive!
- end
- it "should not propagate to disabled Agents" do
- Agent.async_check(agents(:bob_weather_agent).id)
- agents(:bob_rain_notifier_agent).update_attribute :disabled, true
- expect(Agent).not_to receive(:async_receive).with(agents(:bob_rain_notifier_agent).id, anything)
- Agent.receive!
- end
- it "should not propagate to Agents with unknown types" do
- Agent.async_check(agents(:jane_weather_agent).id)
- Agent.async_check(agents(:bob_weather_agent).id)
- Agent.where(id: agents(:bob_rain_notifier_agent).id).update_all type: 'UnknownTypeAgent'
- expect(Agent).not_to receive(:async_receive).with(agents(:bob_rain_notifier_agent).id, anything)
- expect(Agent).to receive(:async_receive).with(agents(:jane_rain_notifier_agent).id, anything).once
- Agent.receive!
- end
- it "should not propagate from Agents with unknown types" do
- Agent.async_check(agents(:jane_weather_agent).id)
- Agent.async_check(agents(:bob_weather_agent).id)
- Agent.where(id: agents(:bob_weather_agent).id).update_all type: 'UnknownTypeAgent'
- expect(Agent).not_to receive(:async_receive).with(agents(:bob_rain_notifier_agent).id, anything)
- expect(Agent).to receive(:async_receive).with(agents(:jane_rain_notifier_agent).id, anything).once
- Agent.receive!
- end
- it "should log exceptions" do
- count = 0
- allow_any_instance_of(Agents::TriggerAgent).to receive(:receive) {
- count += 1
- raise "foo"
- }
- Agent.async_check(agents(:bob_weather_agent).id)
- expect {
- Agent.async_receive(agents(:bob_rain_notifier_agent).id, [agents(:bob_weather_agent).events.last.id])
- }.to raise_error(RuntimeError)
- log = agents(:bob_rain_notifier_agent).logs.first
- expect(log.message).to match(/Exception/)
- expect(log.level).to eq(4)
- expect(count).to eq 1
- end
- it "should track when events have been seen and not received them again" do
- count = 0
- allow_any_instance_of(Agents::TriggerAgent).to receive(:receive) { count += 1 }
- Agent.async_check(agents(:bob_weather_agent).id)
- expect {
- Agent.receive!
- }.to change { agents(:bob_rain_notifier_agent).reload.last_checked_event_id }
- expect {
- Agent.receive!
- }.not_to change { agents(:bob_rain_notifier_agent).reload.last_checked_event_id }
- expect(count).to eq 1
- end
- it "should not run consumers that have nothing to do" do
- anything
- Agent.receive!
- end
- it "should group events" do
- count = 0
- allow_any_instance_of(Agents::TriggerAgent).to receive(:receive) { |_agent, events|
- count += 1
- expect(events.map(&:user).map(&:username).uniq.length).to eq(1)
- }
- Agent.async_check(agents(:bob_weather_agent).id)
- Agent.async_check(agents(:jane_weather_agent).id)
- Agent.receive!
- expect(count).to eq 2
- end
- it "should call receive for each event when no_bulk_receive! is used" do
- count = 0
- allow_any_instance_of(Agents::TriggerAgent).to receive(:receive).with(anything) { count += 1 }
- allow(Agents::TriggerAgent).to receive(:no_bulk_receive?) { true }
- Agent.async_check(agents(:bob_weather_agent).id)
- Agent.async_check(agents(:bob_weather_agent).id)
- Agent.receive!
- expect(count).to eq 2
- end
- it "should ignore events that were created before a particular Link" do
- agent2 = Agents::SomethingSource.new(name: "something")
- agent2.user = users(:bob)
- agent2.save!
- agent2.check
- count = 0
- allow_any_instance_of(Agents::TriggerAgent).to receive(:receive) { count += 1 }
- agents(:bob_weather_agent).check # bob_weather_agent makes an event
- expect {
- Agent.receive! # event gets propagated
- }.to change { agents(:bob_rain_notifier_agent).reload.last_checked_event_id }
- # This agent creates a few events before we link to it, but after our last check.
- agent2.check
- agent2.check
- # Now we link to it.
- agents(:bob_rain_notifier_agent).sources << agent2
- expect(agent2.links_as_source.first.event_id_at_creation).to eq(agent2.events.reorder("events.id desc").first.id)
- expect {
- Agent.receive! # but we don't receive those events because they're too old
- }.not_to change { agents(:bob_rain_notifier_agent).reload.last_checked_event_id }
- # Now a new event is created by agent2
- agent2.check
- expect {
- Agent.receive! # and we receive it
- }.to change { agents(:bob_rain_notifier_agent).reload.last_checked_event_id }
- expect(count).to eq 2
- end
- it "should not run agents of deactivated accounts" do
- agents(:bob_weather_agent).user.deactivate!
- Agent.async_check(agents(:bob_weather_agent).id)
- expect(Agent).not_to receive(:async_receive).with(agents(:bob_rain_notifier_agent).id, anything)
- Agent.receive!
- end
- end
- describe ".async_receive" do
- it "should not run disabled Agents" do
- expect(Agent).to receive(:find).with(agents(:bob_rain_notifier_agent).id) { agents(:bob_rain_notifier_agent) }
- expect(agents(:bob_rain_notifier_agent)).not_to receive(:receive)
- agents(:bob_rain_notifier_agent).update_attribute :disabled, true
- Agent.async_receive(agents(:bob_rain_notifier_agent).id, [1, 2, 3])
- end
- end
- describe "creating a new agent and then calling .receive!" do
- it "should not backfill events for a newly created agent" do
- Event.delete_all
- sender = Agents::SomethingSource.new(name: "Sending Agent")
- sender.user = users(:bob)
- sender.save!
- sender.create_event payload: {}
- sender.create_event payload: {}
- expect(sender.events.count).to eq(2)
- receiver = Agents::CannotBeScheduled.new(name: "Receiving Agent")
- receiver.user = users(:bob)
- receiver.sources << sender
- receiver.save!
- expect(receiver.events.count).to eq(0)
- Agent.receive!
- expect(receiver.events.count).to eq(0)
- sender.create_event payload: {}
- Agent.receive!
- expect(receiver.events.count).to eq(1)
- end
- end
- describe "creating agents with propagate_immediately = true" do
- it "should schedule subagent events immediately" do
- Event.delete_all
- sender = Agents::SomethingSource.new(name: "Sending Agent")
- sender.user = users(:bob)
- sender.save!
- receiver = Agents::CannotBeScheduled.new(
- name: "Receiving Agent",
- )
- receiver.propagate_immediately = true
- receiver.user = users(:bob)
- receiver.sources << sender
- receiver.save!
- sender.create_event payload: { "message" => "new payload" }
- expect(sender.events.count).to eq(1)
- expect(receiver.events.count).to eq(1)
- # should be true without calling Agent.receive!
- end
- it "should only schedule receiving agents that are set to propagate_immediately" do
- Event.delete_all
- sender = Agents::SomethingSource.new(name: "Sending Agent")
- sender.user = users(:bob)
- sender.save!
- im_receiver = Agents::CannotBeScheduled.new(
- name: "Immediate Receiving Agent",
- )
- im_receiver.propagate_immediately = true
- im_receiver.user = users(:bob)
- im_receiver.sources << sender
- im_receiver.save!
- slow_receiver = Agents::CannotBeScheduled.new(
- name: "Slow Receiving Agent",
- )
- slow_receiver.user = users(:bob)
- slow_receiver.sources << sender
- slow_receiver.save!
- sender.create_event payload: { "message" => "new payload" }
- expect(sender.events.count).to eq(1)
- expect(im_receiver.events.count).to eq(1)
- # we should get the quick one
- # but not the slow one
- expect(slow_receiver.events.count).to eq(0)
- Agent.receive!
- # now we should have one in both
- expect(im_receiver.events.count).to eq(1)
- expect(slow_receiver.events.count).to eq(1)
- end
- end
- describe "validations" do
- it "calls validate_options" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- agent.options[:bad] = true
- expect(agent).to have(1).error_on(:base)
- agent.options[:bad] = false
- expect(agent).to have(0).errors_on(:base)
- end
- it "makes options symbol-indifferent before validating" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- agent.options["bad"] = true
- expect(agent).to have(1).error_on(:base)
- agent.options["bad"] = false
- expect(agent).to have(0).errors_on(:base)
- end
- it "makes memory symbol-indifferent before validating" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- agent.memory["bad"] = 2
- agent.save
- expect(agent.memory[:bad]).to eq(2)
- end
- it "should work when assigned a hash or JSON string" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.memory = {}
- expect(agent.memory).to eq({})
- expect(agent.memory["foo"]).to be_nil
- agent.memory = ""
- expect(agent.memory["foo"]).to be_nil
- expect(agent.memory).to eq({})
- agent.memory = '{"hi": "there"}'
- expect(agent.memory).to eq({ "hi" => "there" })
- agent.memory = '{invalid}'
- expect(agent.memory).to eq({ "hi" => "there" })
- expect(agent).to have(1).errors_on(:memory)
- agent.memory = "{}"
- expect(agent.memory["foo"]).to be_nil
- expect(agent.memory).to eq({})
- expect(agent).to have(0).errors_on(:memory)
- agent.options = "{}"
- expect(agent.options["foo"]).to be_nil
- expect(agent.options).to eq({})
- expect(agent).to have(0).errors_on(:options)
- agent.options = '{"hi": 2}'
- expect(agent.options["hi"]).to eq(2)
- expect(agent).to have(0).errors_on(:options)
- agent.options = '{"hi": wut}'
- expect(agent.options["hi"]).to eq(2)
- expect(agent).to have(1).errors_on(:options)
- expect(agent.errors_on(:options)).to include("was assigned invalid JSON")
- agent.options = 5
- expect(agent.options["hi"]).to eq(2)
- expect(agent).to have(1).errors_on(:options)
- expect(agent.errors_on(:options)).to include("cannot be set to an instance of #{2.class}") # Integer (ruby >=2.4) or Fixnum (ruby <2.4)
- end
- it "should not allow source agents owned by other people" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- agent.source_ids = [agents(:bob_weather_agent).id]
- expect(agent).to have(0).errors_on(:sources)
- agent.source_ids = [agents(:jane_weather_agent).id]
- expect(agent).to have(1).errors_on(:sources)
- agent.user = users(:jane)
- expect(agent).to have(0).errors_on(:sources)
- end
- it "should not allow target agents owned by other people" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- agent.receiver_ids = [agents(:bob_weather_agent).id]
- expect(agent).to have(0).errors_on(:receivers)
- agent.receiver_ids = [agents(:jane_weather_agent).id]
- expect(agent).to have(1).errors_on(:receivers)
- agent.user = users(:jane)
- expect(agent).to have(0).errors_on(:receivers)
- end
- it "should not allow controller agents owned by other people" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- agent.controller_ids = [agents(:bob_weather_agent).id]
- expect(agent).to have(0).errors_on(:controllers)
- agent.controller_ids = [agents(:jane_weather_agent).id]
- expect(agent).to have(1).errors_on(:controllers)
- agent.user = users(:jane)
- expect(agent).to have(0).errors_on(:controllers)
- end
- it "should not allow control target agents owned by other people" do
- agent = Agents::CannotBeScheduled.new(name: "something")
- agent.user = users(:bob)
- agent.control_target_ids = [agents(:bob_weather_agent).id]
- expect(agent).to have(0).errors_on(:control_targets)
- agent.control_target_ids = [agents(:jane_weather_agent).id]
- expect(agent).to have(1).errors_on(:control_targets)
- agent.user = users(:jane)
- expect(agent).to have(0).errors_on(:control_targets)
- end
- it "should not allow scenarios owned by other people" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- agent.scenario_ids = [scenarios(:bob_weather).id]
- expect(agent).to have(0).errors_on(:scenarios)
- agent.scenario_ids = [scenarios(:bob_weather).id, scenarios(:jane_weather).id]
- expect(agent).to have(1).errors_on(:scenarios)
- agent.scenario_ids = [scenarios(:jane_weather).id]
- expect(agent).to have(1).errors_on(:scenarios)
- agent.user = users(:jane)
- expect(agent).to have(0).errors_on(:scenarios)
- end
- it "validates keep_events_for" do
- agent = Agents::SomethingSource.new(name: "something")
- agent.user = users(:bob)
- expect(agent).to be_valid
- agent.keep_events_for = nil
- expect(agent).to have(1).errors_on(:keep_events_for)
- agent.keep_events_for = 1000
- expect(agent).to have(1).errors_on(:keep_events_for)
- agent.keep_events_for = ""
- expect(agent).to have(1).errors_on(:keep_events_for)
- agent.keep_events_for = 5.days.to_i
- expect(agent).to be_valid
- agent.keep_events_for = 0
- expect(agent).to be_valid
- agent.keep_events_for = 365.days.to_i
- expect(agent).to be_valid
- # Rails seems to call to_i on the input. This guards against future changes to that behavior.
- agent.keep_events_for = "drop table;"
- expect(agent.keep_events_for).to eq(0)
- end
- end
- describe "cleaning up now-expired events" do
- before do
- @time = "2014-01-01 01:00:00 +00:00"
- travel_to @time do
- @agent = Agents::SomethingSource.new(name: "something")
- @agent.keep_events_for = 5.days
- @agent.user = users(:bob)
- @agent.save!
- @event = @agent.create_event payload: { "hello" => "world" }
- expect(@event.expires_at.to_i).to be_within(2).of(5.days.from_now.to_i)
- end
- end
- describe "when keep_events_for has not changed" do
- it "does nothing" do
- expect(@agent).not_to receive(:update_event_expirations!)
- @agent.options[:foo] = "bar1"
- @agent.save!
- @agent.options[:foo] = "bar1"
- @agent.keep_events_for = 5.days
- @agent.save!
- end
- end
- describe "when keep_events_for is changed" do
- it "updates events' expires_at" do
- travel_to @time do
- expect {
- @agent.options[:foo] = "bar1"
- @agent.keep_events_for = 3.days
- @agent.save!
- }.to change { @event.reload.expires_at }
- expect(@event.expires_at.to_i).to be_within(2).of(3.days.from_now.to_i)
- end
- end
- it "updates events relative to their created_at" do
- @event.update_attribute :created_at, 2.days.ago
- expect(@event.reload.created_at.to_i).to be_within(2).of(2.days.ago.to_i)
- expect {
- @agent.options[:foo] = "bar2"
- @agent.keep_events_for = 3.days
- @agent.save!
- }.to change { @event.reload.expires_at }
- expect(@event.expires_at.to_i).to be_within(60 * 61).of(1.days.from_now.to_i) # The larger time is to deal with daylight savings
- end
- it "nulls out expires_at when keep_events_for is set to 0" do
- expect {
- @agent.options[:foo] = "bar"
- @agent.keep_events_for = 0
- @agent.save!
- }.to change { @event.reload.expires_at }.to(nil)
- end
- end
- end
- describe "Agent.build_clone" do
- before do
- Event.delete_all
- @sender = Agents::SomethingSource.new(
- name: 'Agent (2)',
- options: { foo: 'bar2' },
- schedule: '5pm'
- )
- @sender.user = users(:bob)
- @sender.save!
- @sender.create_event payload: {}
- @sender.create_event payload: {}
- expect(@sender.events.count).to eq(2)
- @receiver = Agents::CannotBeScheduled.new(
- name: 'Agent',
- options: { foo: 'bar3' },
- keep_events_for: 3.days,
- propagate_immediately: true
- )
- @receiver.user = users(:bob)
- @receiver.sources << @sender
- @receiver.memory[:test] = 1
- @receiver.save!
- end
- it "should create a clone of a given agent for editing" do
- sender_clone = users(:bob).agents.build_clone(@sender)
- expect(sender_clone.attributes).to eq(Agent.new.attributes
- .update(@sender.slice(:user_id, :type,
- :options, :schedule, :keep_events_for, :propagate_immediately))
- .update('name' => 'Agent (2) (2)', 'options' => { 'foo' => 'bar2' }))
- expect(sender_clone.source_ids).to eq([])
- receiver_clone = users(:bob).agents.build_clone(@receiver)
- expect(receiver_clone.attributes).to eq(Agent.new.attributes
- .update(@receiver.slice(:user_id, :type,
- :options, :schedule, :keep_events_for, :propagate_immediately))
- .update('name' => 'Agent (3)', 'options' => { 'foo' => 'bar3' }))
- expect(receiver_clone.source_ids).to eq([@sender.id])
- end
- end
- end
- describe ".trigger_web_request" do
- class Agents::WebRequestReceiver < Agent
- cannot_be_scheduled!
- end
- before do
- allow(Agents::WebRequestReceiver).to receive(:valid_type?).with("Agents::WebRequestReceiver") { true }
- end
- context "when .receive_web_request is defined" do
- before do
- @agent = Agents::WebRequestReceiver.new(name: "something")
- @agent.user = users(:bob)
- @agent.save!
- def @agent.receive_web_request(params, method, format)
- memory['last_request'] = [params, method, format]
- ['Ok!', 200]
- end
- end
- it "calls the .receive_web_request hook, updates last_web_request_at, and saves" do
- request = ActionDispatch::Request.new({
- 'action_dispatch.request.request_parameters' => { some_param: "some_value" },
- 'REQUEST_METHOD' => "POST",
- 'HTTP_ACCEPT' => 'text/html'
- })
- @agent.trigger_web_request(request)
- expect(@agent.reload.memory['last_request']).to eq([{ "some_param" => "some_value" }, "post", "text/html"])
- expect(@agent.last_web_request_at.to_i).to be_within(1).of(Time.now.to_i)
- end
- end
- context "when .receive_web_request is defined with just request" do
- before do
- @agent = Agents::WebRequestReceiver.new(name: "something")
- @agent.user = users(:bob)
- @agent.save!
- def @agent.receive_web_request(request)
- memory['last_request'] =
- [request.params, request.method_symbol.to_s, request.format,
- { 'HTTP_X_CUSTOM_HEADER' => request.headers['HTTP_X_CUSTOM_HEADER'] }]
- ['Ok!', 200]
- end
- end
- it "calls the .trigger_web_request with headers, and they get passed to .receive_web_request" do
- request = ActionDispatch::Request.new({
- 'action_dispatch.request.request_parameters' => { some_param: "some_value" },
- 'REQUEST_METHOD' => "POST",
- 'HTTP_ACCEPT' => 'text/html',
- 'HTTP_X_CUSTOM_HEADER' => "foo"
- })
- @agent.trigger_web_request(request)
- expect(@agent.reload.memory['last_request']).to eq([{ "some_param" => "some_value" }, "post", "text/html",
- { 'HTTP_X_CUSTOM_HEADER' => "foo" }])
- expect(@agent.last_web_request_at.to_i).to be_within(1).of(Time.now.to_i)
- end
- end
- context "when .receive_webhook is defined" do
- before do
- @agent = Agents::WebRequestReceiver.new(name: "something")
- @agent.user = users(:bob)
- @agent.save!
- def @agent.receive_webhook(params)
- memory['last_webhook_request'] = params
- ['Ok!', 200]
- end
- end
- it "outputs a deprecation warning and calls .receive_webhook with the params" do
- request = ActionDispatch::Request.new({
- 'action_dispatch.request.request_parameters' => { some_param: "some_value" },
- 'REQUEST_METHOD' => "POST",
- 'HTTP_ACCEPT' => 'text/html'
- })
- expect(Rails.logger).to receive(:warn).with("DEPRECATED: The .receive_webhook method is deprecated, please switch your Agent to use .receive_web_request.")
- @agent.trigger_web_request(request)
- expect(@agent.reload.memory['last_webhook_request']).to eq({ "some_param" => "some_value" })
- expect(@agent.last_web_request_at.to_i).to be_within(1).of(Time.now.to_i)
- end
- end
- end
- describe "scopes" do
- describe "of_type" do
- it "should accept classes" do
- agents = Agent.of_type(Agents::WebsiteAgent)
- expect(agents).to include(agents(:bob_website_agent))
- expect(agents).to include(agents(:jane_website_agent))
- expect(agents).not_to include(agents(:bob_weather_agent))
- end
- it "should accept strings" do
- agents = Agent.of_type("Agents::WebsiteAgent")
- expect(agents).to include(agents(:bob_website_agent))
- expect(agents).to include(agents(:jane_website_agent))
- expect(agents).not_to include(agents(:bob_weather_agent))
- end
- it "should accept instances of an Agent" do
- agents = Agent.of_type(agents(:bob_website_agent))
- expect(agents).to include(agents(:bob_website_agent))
- expect(agents).to include(agents(:jane_website_agent))
- expect(agents).not_to include(agents(:bob_weather_agent))
- end
- end
- end
- describe "#create_event" do
- describe "when the agent has keep_events_for set" do
- before do
- expect(agents(:jane_weather_agent).keep_events_for).to be > 0
- end
- it "sets expires_at on created events" do
- event = agents(:jane_weather_agent).create_event payload: { 'hi' => 'there' }
- expect(event.expires_at.to_i).to be_within(5).of(agents(:jane_weather_agent).keep_events_for.seconds.from_now.to_i)
- end
- end
- describe "when the agent does not have keep_events_for set" do
- before do
- expect(agents(:jane_website_agent).keep_events_for).to eq(0)
- end
- it "does not set expires_at on created events" do
- event = agents(:jane_website_agent).create_event payload: { 'hi' => 'there' }
- expect(event.expires_at).to be_nil
- end
- end
- end
- describe '.last_checked_event_id' do
- it "should be updated by setting drop_pending_events to true" do
- agent = agents(:bob_rain_notifier_agent)
- agent.last_checked_event_id = nil
- agent.save!
- agent.update!(drop_pending_events: true)
- expect(agent.reload.last_checked_event_id).to eq(Event.maximum(:id))
- end
- it "should not affect a virtual attribute drop_pending_events" do
- agent = agents(:bob_rain_notifier_agent)
- agent.update!(drop_pending_events: true)
- expect(agent.reload.drop_pending_events).to eq(false)
- end
- end
- describe ".drop_pending_events" do
- before do
- stub_request(:any, /pirateweather/).to_return(body: File.read(Rails.root.join("spec/data_fixtures/weather.json")),
- status: 200)
- end
- it "should drop pending events while the agent was disabled when set to true" do
- agent1 = agents(:bob_weather_agent)
- agent2 = agents(:bob_rain_notifier_agent)
- expect {
- expect {
- Agent.async_check(agent1.id)
- Agent.receive!
- }.to change { agent1.events.count }.by(1)
- }.to change { agent2.events.count }.by(0)
- agent2.disabled = true
- agent2.save!
- expect {
- expect {
- Agent.async_check(agent1.id)
- Agent.receive!
- }.to change { agent1.events.count }.by(1)
- }.not_to change { agent2.events.count }
- agent2.disabled = false
- agent2.drop_pending_events = true
- agent2.save!
- expect {
- Agent.receive!
- }.not_to change { agent2.events.count }
- end
- end
- end
- describe Agent::Drop do
- def interpolate(string, agent)
- agent.interpolate_string(string, "agent" => agent)
- end
- before do
- @wsa1 = Agents::WebsiteAgent.new(
- name: 'XKCD',
- options: {
- expected_update_period_in_days: 2,
- type: 'html',
- url: 'http://xkcd.com/',
- mode: 'on_change',
- extract: {
- url: { css: '#comic img', value: '@src' },
- title: { css: '#comic img', value: '@alt' },
- },
- },
- schedule: 'every_1h',
- keep_events_for: 2.days
- )
- @wsa1.user = users(:bob)
- @wsa1.save!
- @wsa2 = Agents::WebsiteAgent.new(
- name: 'Dilbert',
- options: {
- expected_update_period_in_days: 2,
- type: 'html',
- url: 'http://dilbert.com/',
- mode: 'on_change',
- extract: {
- url: { css: '[id^=strip_enlarged_] img', value: '@src' },
- title: { css: '.STR_DateStrip', value: 'string(.)' },
- },
- },
- schedule: 'every_12h',
- keep_events_for: 2.days
- )
- @wsa2.user = users(:bob)
- @wsa2.save!
- @efa = Agents::EventFormattingAgent.new(
- name: 'Formatter',
- options: {
- instructions: {
- message: '{{agent.name}}: {{title}} {{url}}',
- agent: '{{agent.type}}',
- },
- mode: 'clean',
- matchers: [],
- skip_created_at: 'false',
- },
- keep_events_for: 2.days,
- propagate_immediately: true
- )
- @efa.user = users(:bob)
- @efa.sources << @wsa1 << @wsa2
- @efa.memory[:test] = 1
- @efa.save!
- @wsa1.reload
- @wsa2.reload
- end
- it 'should be created via Agent#to_liquid' do
- expect(@wsa1.to_liquid.class).to be(Agent::Drop)
- expect(@wsa2.to_liquid.class).to be(Agent::Drop)
- expect(@efa.to_liquid.class).to be(Agent::Drop)
- end
- it 'should have .id, .type and .name' do
- t = '[{{agent.id}}]{{agent.type}}: {{agent.name}}'
- expect(interpolate(t, @wsa1)).to eq("[#{@wsa1.id}]WebsiteAgent: XKCD")
- expect(interpolate(t, @wsa2)).to eq("[#{@wsa2.id}]WebsiteAgent: Dilbert")
- expect(interpolate(t, @efa)).to eq("[#{@efa.id}]EventFormattingAgent: Formatter")
- end
- it 'should have .options' do
- t = '{{agent.options.url}}'
- expect(interpolate(t, @wsa1)).to eq('http://xkcd.com/')
- expect(interpolate(t, @wsa2)).to eq('http://dilbert.com/')
- expect(interpolate('{{agent.options.instructions.message}}',
- @efa)).to eq('{{agent.name}}: {{title}} {{url}}')
- end
- it 'should have .sources' do
- t = '{{agent.sources.size}}: {{agent.sources | map:"name" | join:", "}}'
- expect(interpolate(t, @wsa1)).to eq('0: ')
- expect(interpolate(t, @wsa2)).to eq('0: ')
- expect(interpolate(t, @efa)).to eq('2: XKCD, Dilbert')
- t = '{{agent.sources.first.name}}..{{agent.sources.last.name}}'
- expect(interpolate(t, @wsa1)).to eq('..')
- expect(interpolate(t, @wsa2)).to eq('..')
- expect(interpolate(t, @efa)).to eq('XKCD..Dilbert')
- t = '{{agent.sources[1].name}}'
- expect(interpolate(t, @efa)).to eq('Dilbert')
- end
- it 'should have .receivers' do
- t = '{{agent.receivers.size}}: {{agent.receivers | map:"name" | join:", "}}'
- expect(interpolate(t, @wsa1)).to eq('1: Formatter')
- expect(interpolate(t, @wsa2)).to eq('1: Formatter')
- expect(interpolate(t, @efa)).to eq('0: ')
- end
- it 'should have .working' do
- allow(@wsa1).to receive(:working?) { false }
- allow(@wsa2).to receive(:working?) { true }
- allow(@efa).to receive(:working?) { false }
- t = '{% if agent.working %}healthy{% else %}unhealthy{% endif %}'
- expect(interpolate(t, @wsa1)).to eq('unhealthy')
- expect(interpolate(t, @wsa2)).to eq('healthy')
- expect(interpolate(t, @efa)).to eq('unhealthy')
- end
- it 'should have .url' do
- t = '{{ agent.url }}'
- expect(interpolate(t, @wsa1)).to match(/http:\/\/localhost(?::\d+)?\/agents\/#{@wsa1.id}/)
- expect(interpolate(t, @wsa2)).to match(/http:\/\/localhost(?::\d+)?\/agents\/#{@wsa2.id}/)
- expect(interpolate(t, @efa)).to match(/http:\/\/localhost(?::\d+)?\/agents\/#{@efa.id}/)
- end
- end
|