require 'rails_helper' describe ScenarioImport do let(:user) { users(:bob) } let(:guid) { "somescenarioguid" } let(:tag_fg_color) { "#ffffff" } let(:tag_bg_color) { "#000000" } let(:icon) { 'Star' } let(:description) { "This is a cool Huginn Scenario that does something useful!" } let(:name) { "A useful Scenario" } let(:source_url) { "http://example.com/scenarios/2/export.json" } let(:weather_agent_options) { { 'api_key' => 'some-api-key', 'location' => '12345' } } let(:trigger_agent_options) { { 'expected_receive_period_in_days' => 2, 'rules' => [{ 'type' => "regex", 'value' => "rain|storm", 'path' => "conditions", }], 'message' => "Looks like rain!" } } let(:valid_parsed_weather_agent_data) do { :type => "Agents::WeatherAgent", :name => "a weather agent", :schedule => "5pm", :keep_events_for => 14.days, :disabled => true, :guid => "a-weather-agent", :options => weather_agent_options } end let(:valid_parsed_trigger_agent_data) do { :type => "Agents::TriggerAgent", :name => "listen for weather", :keep_events_for => 0, :propagate_immediately => true, :disabled => false, :guid => "a-trigger-agent", :options => trigger_agent_options } end let(:valid_parsed_basecamp_agent_data) do { :type => "Agents::BasecampAgent", :name => "Basecamp test", :schedule => "every_2m", :keep_events_for => 0, :propagate_immediately => true, :disabled => false, :guid => "a-basecamp-agent", :options => {project_id: 12345} } end let(:valid_parsed_data) do { schema_version: 1, name: name, description: description, guid: guid, tag_fg_color: tag_fg_color, tag_bg_color: tag_bg_color, icon: icon, source_url: source_url, exported_at: 2.days.ago.utc.iso8601, agents: [ valid_parsed_weather_agent_data, valid_parsed_trigger_agent_data ], links: [ { :source => 0, :receiver => 1 } ], control_links: [] } end let(:valid_data) { valid_parsed_data.to_json } let(:invalid_data) { { :name => "some scenario missing a guid" }.to_json } describe "initialization" do it "is initialized with an attributes hash" do expect(ScenarioImport.new(:url => "http://google.com").url).to eq("http://google.com") end end describe "validations" do subject do _import = ScenarioImport.new _import.set_user(user) _import end it "is not valid when none of file, url, or data are present" do expect(subject).not_to be_valid expect(subject).to have(1).error_on(:base) expect(subject.errors[:base]).to include("Please provide either a Scenario JSON File or a Public Scenario URL.") end describe "data" do it "should be invalid with invalid data" do subject.data = invalid_data expect(subject).not_to be_valid expect(subject).to have(1).error_on(:base) subject.data = "foo" expect(subject).not_to be_valid expect(subject).to have(1).error_on(:base) # It also clears the data when invalid expect(subject.data).to be_nil end it "should be valid with valid data" do subject.data = valid_data expect(subject).to be_valid end end describe "url" do it "should be invalid with an unreasonable URL" do subject.url = "foo" expect(subject).not_to be_valid expect(subject).to have(1).error_on(:url) expect(subject.errors[:url]).to include("appears to be invalid") end it "should be invalid when the referenced url doesn't contain a scenario" do stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_data) subject.url = "http://example.com/scenarios/1/export.json" expect(subject).not_to be_valid expect(subject.errors[:base]).to include("The provided data does not appear to be a valid Scenario.") end it "should be valid when the url points to a valid scenario" do stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_data) subject.url = "http://example.com/scenarios/1/export.json" expect(subject).to be_valid end end describe "file" do it "should be invalid when the uploaded file doesn't contain a scenario" do subject.file = StringIO.new("foo") expect(subject).not_to be_valid expect(subject.errors[:base]).to include("The provided data does not appear to be a valid Scenario.") subject.file = StringIO.new(invalid_data) expect(subject).not_to be_valid expect(subject.errors[:base]).to include("The provided data does not appear to be a valid Scenario.") end it "should be valid with a valid uploaded scenario" do subject.file = StringIO.new(valid_data) expect(subject).to be_valid end end end describe "#dangerous?" do it "returns false on most Agents" do expect(ScenarioImport.new(:data => valid_data)).not_to be_dangerous end it "returns true if a ShellCommandAgent is present" do valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent" expect(ScenarioImport.new(:data => valid_parsed_data.to_json)).to be_dangerous end end describe "#import and #generate_diff" do let(:scenario_import) do _import = ScenarioImport.new(:data => valid_data) _import.set_user users(:bob) _import end context "when this scenario has never been seen before" do describe "#import" do it "makes a new scenario" do expect { scenario_import.import(:skip_agents => true) }.to change { users(:bob).scenarios.count }.by(1) expect(scenario_import.scenario.name).to eq(name) expect(scenario_import.scenario.description).to eq(description) expect(scenario_import.scenario.guid).to eq(guid) expect(scenario_import.scenario.tag_fg_color).to eq(tag_fg_color) expect(scenario_import.scenario.tag_bg_color).to eq(tag_bg_color) expect(scenario_import.scenario.icon).to eq(icon) expect(scenario_import.scenario.source_url).to eq(source_url) expect(scenario_import.scenario.public).to be_falsey end it "creates the Agents" do expect { scenario_import.import }.to change { users(:bob).agents.count }.by(2) weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") expect(weather_agent.name).to eq("a weather agent") expect(weather_agent.schedule).to eq("5pm") expect(weather_agent.keep_events_for).to eq(14.days) expect(weather_agent.propagate_immediately).to be_falsey expect(weather_agent).to be_disabled expect(weather_agent.memory).to be_empty expect(weather_agent.options).to eq(weather_agent_options) expect(trigger_agent.name).to eq("listen for weather") expect(trigger_agent.sources).to eq([weather_agent]) expect(trigger_agent.schedule).to be_nil expect(trigger_agent.keep_events_for).to eq(0) expect(trigger_agent.propagate_immediately).to be_truthy expect(trigger_agent).not_to be_disabled expect(trigger_agent.memory).to be_empty expect(trigger_agent.options).to eq(trigger_agent_options) end it "creates new Agents, even if one already exists with the given guid (so that we don't overwrite a user's work outside of the scenario)" do agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" expect { scenario_import.import }.to change { users(:bob).agents.count }.by(2) end context "when the schema_version is less than 1" do before do valid_parsed_weather_agent_data[:keep_events_for] = 2 valid_parsed_data.delete(:schema_version) end it "translates keep_events_for from days to seconds" do scenario_import.import expect(scenario_import.errors).to be_empty weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") expect(weather_agent.keep_events_for).to eq(2.days) expect(trigger_agent.keep_events_for).to eq(0) end end describe "with control links" do it 'creates the links' do valid_parsed_data[:control_links] = [ { :controller => 1, :control_target => 0 } ] expect { scenario_import.import }.to change { users(:bob).agents.count }.by(2) weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") expect(trigger_agent.sources).to eq([weather_agent]) expect(weather_agent.controllers.to_a).to eq([trigger_agent]) expect(trigger_agent.control_targets.to_a).to eq([weather_agent]) end it "doesn't crash without any control links" do valid_parsed_data.delete(:control_links) expect { scenario_import.import }.to change { users(:bob).agents.count }.by(2) weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") expect(trigger_agent.sources).to eq([weather_agent]) end end end describe "#generate_diff" do it "returns AgentDiff objects for the incoming Agents" do expect(scenario_import).to be_valid agent_diffs = scenario_import.agent_diffs weather_agent_diff = agent_diffs[0] trigger_agent_diff = agent_diffs[1] valid_parsed_weather_agent_data.each do |key, value| if key == :type value = value.split("::").last end expect(weather_agent_diff).to respond_to(key) field = weather_agent_diff.send(key) expect(field).to be_a(ScenarioImport::AgentDiff::FieldDiff) expect(field.incoming).to eq(value) expect(field.updated).to eq(value) expect(field.current).to be_nil end expect(weather_agent_diff).not_to respond_to(:propagate_immediately) valid_parsed_trigger_agent_data.each do |key, value| if key == :type value = value.split("::").last end expect(trigger_agent_diff).to respond_to(key) field = trigger_agent_diff.send(key) expect(field).to be_a(ScenarioImport::AgentDiff::FieldDiff) expect(field.incoming).to eq(value) expect(field.updated).to eq(value) expect(field.current).to be_nil end expect(trigger_agent_diff).not_to respond_to(:schedule) end end end context "when an a scenario already exists with the given guid for the importing user" do let!(:existing_scenario) do _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario", :description => "something") _existing_scenerio.guid = guid _existing_scenerio.save! agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" agents(:bob_weather_agent).scenarios << _existing_scenerio _existing_scenerio end describe "#import" do it "uses the existing scenario, updating its data" do expect { scenario_import.import(:skip_agents => true) expect(scenario_import.scenario).to eq(existing_scenario) }.not_to change { users(:bob).scenarios.count } existing_scenario.reload expect(existing_scenario.guid).to eq(guid) expect(existing_scenario.tag_fg_color).to eq(tag_fg_color) expect(existing_scenario.tag_bg_color).to eq(tag_bg_color) expect(existing_scenario.icon).to eq(icon) expect(existing_scenario.description).to eq(description) expect(existing_scenario.name).to eq(name) expect(existing_scenario.source_url).to eq(source_url) expect(existing_scenario.public).to be_falsey end it "updates any existing agents in the scenario, and makes new ones as needed" do expect(scenario_import).to be_valid expect { scenario_import.import }.to change { users(:bob).agents.count }.by(1) # One, because the weather agent already existed. weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent") expect(weather_agent).to eq(agents(:bob_weather_agent)) expect(weather_agent.name).to eq("a weather agent") expect(weather_agent.schedule).to eq("5pm") expect(weather_agent.keep_events_for).to eq(14.days) expect(weather_agent.propagate_immediately).to be_falsey expect(weather_agent).to be_disabled expect(weather_agent.memory).to be_empty expect(weather_agent.options).to eq(weather_agent_options) expect(trigger_agent.name).to eq("listen for weather") expect(trigger_agent.sources).to eq([weather_agent]) expect(trigger_agent.schedule).to be_nil expect(trigger_agent.keep_events_for).to eq(0) expect(trigger_agent.propagate_immediately).to be_truthy expect(trigger_agent).not_to be_disabled expect(trigger_agent.memory).to be_empty expect(trigger_agent.options).to eq(trigger_agent_options) end it "honors updates coming from the UI" do scenario_import.merges = { "0" => { "name" => "updated name", "schedule" => "6pm", "keep_events_for" => 2.days.to_i.to_s, "disabled" => "false", "options" => weather_agent_options.merge("api_key" => "foo").to_json } } expect(scenario_import).to be_valid expect(scenario_import.import).to be_truthy weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") expect(weather_agent.name).to eq("updated name") expect(weather_agent.schedule).to eq("6pm") expect(weather_agent.keep_events_for).to eq(2.days.to_i) expect(weather_agent).not_to be_disabled expect(weather_agent.options).to eq(weather_agent_options.merge("api_key" => "foo")) end it "adds errors when updated agents are invalid" do scenario_import.merges = { "0" => { "name" => "", "schedule" => "foo", "keep_events_for" => 2.days.to_i.to_s, "options" => weather_agent_options.merge("api_key" => "").to_json } } expect(scenario_import.import).to be_falsey errors = scenario_import.errors.full_messages.to_sentence expect(errors).to match(/Name can't be blank/) expect(errors).to match(/api_key is required/) expect(errors).to match(/Schedule is not a valid schedule/) end end describe "#generate_diff" do it "returns AgentDiff objects that include 'current' values from any agents that already exist" do agent_diffs = scenario_import.agent_diffs weather_agent_diff = agent_diffs[0] trigger_agent_diff = agent_diffs[1] # Already exists expect(weather_agent_diff.agent).to eq(agents(:bob_weather_agent)) valid_parsed_weather_agent_data.each do |key, value| next if key == :type expect(weather_agent_diff.send(key).current).to eq(agents(:bob_weather_agent).send(key)) end # Doesn't exist yet valid_parsed_trigger_agent_data.each do |key, value| expect(trigger_agent_diff.send(key).current).to be_nil end end context "when the schema_version is less than 1" do it "translates keep_events_for from days to seconds" do valid_parsed_data.delete(:schema_version) valid_parsed_data[:agents] = [valid_parsed_weather_agent_data.merge(keep_events_for: 5)] scenario_import.merges = { "0" => { "name" => "a new name", "schedule" => "6pm", "keep_events_for" => 2.days.to_i.to_s, "disabled" => "true", "options" => weather_agent_options.merge("api_key" => "foo").to_json } } expect(scenario_import).to be_valid weather_agent_diff = scenario_import.agent_diffs[0] expect(weather_agent_diff.name.current).to eq(agents(:bob_weather_agent).name) expect(weather_agent_diff.name.incoming).to eq('a weather agent') expect(weather_agent_diff.name.updated).to eq('a new name') expect(weather_agent_diff.keep_events_for.current).to eq(45.days.to_i) expect(weather_agent_diff.keep_events_for.incoming).to eq(5.days.to_i) expect(weather_agent_diff.keep_events_for.updated).to eq(2.days.to_i.to_s) end end it "sets the 'updated' FieldDiff values based on any feedback from the user" do scenario_import.merges = { "0" => { "name" => "a new name", "schedule" => "6pm", "keep_events_for" => 2.days.to_s, "disabled" => "true", "options" => weather_agent_options.merge("api_key" => "foo").to_json }, "1" => { "name" => "another new name" } } expect(scenario_import).to be_valid agent_diffs = scenario_import.agent_diffs weather_agent_diff = agent_diffs[0] trigger_agent_diff = agent_diffs[1] expect(weather_agent_diff.name.current).to eq(agents(:bob_weather_agent).name) expect(weather_agent_diff.name.incoming).to eq(valid_parsed_weather_agent_data[:name]) expect(weather_agent_diff.name.updated).to eq("a new name") expect(weather_agent_diff.schedule.updated).to eq("6pm") expect(weather_agent_diff.keep_events_for.current).to eq(45.days) expect(weather_agent_diff.keep_events_for.updated).to eq(2.days.to_s) expect(weather_agent_diff.disabled.updated).to eq("true") expect(weather_agent_diff.options.updated).to eq(weather_agent_options.merge("api_key" => "foo")) end it "adds errors on validation when updated options are unparsable" do scenario_import.merges = { "0" => { "options" => '{' } } expect(scenario_import).not_to be_valid expect(scenario_import).to have(1).error_on(:base) end end end context "when Bob imports Jane's scenario" do let!(:existing_scenario) do _existing_scenerio = users(:jane).scenarios.build(:name => "an existing scenario", :description => "something") _existing_scenerio.guid = guid _existing_scenerio.save! _existing_scenerio end describe "#import" do it "makes a new scenario for Bob" do expect { scenario_import.import(:skip_agents => true) }.to change { users(:bob).scenarios.count }.by(1) expect(Scenario.where(guid: guid).count).to eq(2) expect(scenario_import.scenario.name).to eq(name) expect(scenario_import.scenario.description).to eq(description) expect(scenario_import.scenario.guid).to eq(guid) expect(scenario_import.scenario.tag_fg_color).to eq(tag_fg_color) expect(scenario_import.scenario.tag_bg_color).to eq(tag_bg_color) expect(scenario_import.scenario.icon).to eq(icon) expect(scenario_import.scenario.source_url).to eq(source_url) expect(scenario_import.scenario.public).to be_falsey end it "does not change Jane's scenario" do expect { scenario_import.import(:skip_agents => true) }.not_to change { users(:jane).scenarios } expect(users(:jane).scenarios.find_by(guid: guid)).to eq(existing_scenario) end end end context "agents which require a service" do let(:valid_parsed_services) do data = valid_parsed_data data[:agents] = [valid_parsed_basecamp_agent_data, valid_parsed_trigger_agent_data] data end let(:valid_parsed_services_data) { valid_parsed_services.to_json } let(:services_scenario_import) { _import = ScenarioImport.new(:data => valid_parsed_services_data) _import.set_user users(:bob) _import } describe "#generate_diff" do it "should check if the agent requires a service" do agent_diffs = services_scenario_import.agent_diffs basecamp_agent_diff = agent_diffs[0] expect(basecamp_agent_diff.requires_service?).to eq(true) end it "should add an error when no service is selected" do expect(services_scenario_import.import).to eq(false) expect(services_scenario_import.errors[:base].length).to eq(1) end end describe "#import" do it "should import" do services_scenario_import.merges = { "0" => { "service_id" => "0", } } expect { expect(services_scenario_import.import).to eq(true) }.to change { users(:bob).agents.count }.by(2) end end end end end