From 64564eb120354eef872c98b970c03b56b6e1264a Mon Sep 17 00:00:00 2001 From: Andrew Cantino Date: Wed, 4 Jun 2014 22:26:56 -0700 Subject: [PATCH] Importing of Scenarios now works, creating any needed Agents by global guid --- .../stylesheets/application.css.scss.erb | 1 + app/assets/stylesheets/scenarios.css.scss | 9 + app/concerns/assignable_types.rb | 2 +- app/concerns/has_guid.rb | 13 ++ app/models/agent.rb | 1 + app/models/scenario.rb | 10 +- app/models/scenario_import.rb | 46 +++- app/views/scenario_imports/_step_two.html.erb | 17 +- app/views/scenario_imports/new.html.erb | 2 +- app/views/scenarios/index.html.erb | 2 + ...20140602014917_add_indices_to_scenarios.rb | 2 +- .../20140605032822_add_guid_to_agents.rb | 15 ++ db/schema.rb | 6 +- lib/agents_exporter.rb | 2 +- spec/fixtures/agents.yml | 8 + spec/lib/agents_exporter_spec.rb | 2 +- spec/models/agent_spec.rb | 9 +- spec/models/agents/data_output_agent_spec.rb | 1 - spec/models/agents/hipchat_agent_spec.rb | 1 - spec/models/agents/human_task_agent_spec.rb | 1 - spec/models/agents/jabber_agent_spec.rb | 1 - .../models/agents/peak_detector_agent_spec.rb | 1 - spec/models/agents/pushbullet_agent_spec.rb | 1 - spec/models/agents/slack_agent_spec.rb | 1 - spec/models/agents/translation_agent_spec.rb | 2 - spec/models/agents/trigger_agent_spec.rb | 1 - spec/models/scenario_import_spec.rb | 196 +++++++++++++++++- spec/models/scenario_spec.rb | 42 ++-- spec/support/shared_examples/has_guid.rb | 12 ++ .../shared_examples}/liquid_interpolatable.rb | 0 .../shared_examples}/working_helpers.rb | 6 +- 31 files changed, 342 insertions(+), 71 deletions(-) create mode 100644 app/assets/stylesheets/scenarios.css.scss create mode 100644 app/concerns/has_guid.rb create mode 100644 db/migrate/20140605032822_add_guid_to_agents.rb create mode 100644 spec/support/shared_examples/has_guid.rb rename spec/{models/concerns => support/shared_examples}/liquid_interpolatable.rb (100%) rename spec/{models/concerns => support/shared_examples}/working_helpers.rb (96%) diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index 29e36e11..3f0a6239 100644 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -158,6 +158,7 @@ h2 .scenario, a span.label.scenario { } // Bootstrappy color styles + .color-danger { color: #d9534f; } diff --git a/app/assets/stylesheets/scenarios.css.scss b/app/assets/stylesheets/scenarios.css.scss new file mode 100644 index 00000000..c6ef3bbd --- /dev/null +++ b/app/assets/stylesheets/scenarios.css.scss @@ -0,0 +1,9 @@ +.scenario-import { + .danger { + color: red; + font-weight: strong; + border: 1px solid red; + padding: 10px; + margin: 10px 0; + } +} \ No newline at end of file diff --git a/app/concerns/assignable_types.rb b/app/concerns/assignable_types.rb index 482b12cb..275ab6f6 100644 --- a/app/concerns/assignable_types.rb +++ b/app/concerns/assignable_types.rb @@ -29,7 +29,7 @@ module AssignableTypes const_get(:TYPES).include?(type) end - def build_for_type(type, user, attributes) + def build_for_type(type, user, attributes = {}) attributes.delete(:type) if valid_type?(type) diff --git a/app/concerns/has_guid.rb b/app/concerns/has_guid.rb new file mode 100644 index 00000000..c3957952 --- /dev/null +++ b/app/concerns/has_guid.rb @@ -0,0 +1,13 @@ +module HasGuid + extend ActiveSupport::Concern + + included do + before_save :make_guid + end + + protected + + def make_guid + self.guid = SecureRandom.hex unless guid.present? + end +end \ No newline at end of file diff --git a/app/models/agent.rb b/app/models/agent.rb index 355e46bb..80fab6cf 100644 --- a/app/models/agent.rb +++ b/app/models/agent.rb @@ -12,6 +12,7 @@ class Agent < ActiveRecord::Base include JSONSerializedField include RDBMSFunctions include WorkingHelpers + include HasGuid markdown_class_attributes :description, :event_description diff --git a/app/models/scenario.rb b/app/models/scenario.rb index d3010e97..52448c37 100644 --- a/app/models/scenario.rb +++ b/app/models/scenario.rb @@ -1,22 +1,18 @@ class Scenario < ActiveRecord::Base - attr_accessible :name, :agent_ids, :description, :public + include HasGuid + + attr_accessible :name, :agent_ids, :description, :public, :source_url belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios - before_save :make_guid - validates_presence_of :name, :user validate :agents_are_owned protected - def make_guid - self.guid = SecureRandom.hex unless guid.present? - end - def agents_are_owned errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user } end diff --git a/app/models/scenario_import.rb b/app/models/scenario_import.rb index 3b907f5f..b507568b 100644 --- a/app/models/scenario_import.rb +++ b/app/models/scenario_import.rb @@ -4,6 +4,7 @@ class ScenarioImport include ActiveModel::Callbacks include ActiveModel::Validations::Callbacks + DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent] URL_REGEX = /\Ahttps?:\/\//i attr_accessor :file, :url, :data, :do_import @@ -33,19 +34,54 @@ class ScenarioImport @existing_scenario ||= user.scenarios.find_by_guid(parsed_data["guid"]) end + def dangerous? + (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) } + end + def parsed_data - @parsed_data + @parsed_data ||= data && JSON.parse(data) rescue {} end def do_import? do_import == "1" end - def import! + def import!(options = {}) + guid = parsed_data['guid'] + description = parsed_data['description'] + name = parsed_data['name'] + agents = parsed_data['agents'] + links = parsed_data['links'] + source_url = parsed_data['source_url'].presence || nil + @scenario = user.scenarios.where(:guid => guid).first_or_initialize + @scenario.update_attributes!(:name => name, :description => description, + :source_url => source_url, :public => false) + + unless options[:skip_agents] + created_agents = agents.map do |agent_data| + agent = @scenario.agents.find_by(:guid => agent_data['guid']) || Agent.build_for_type(agent_data['type'], user) + agent.guid = agent_data['guid'] + agent.attributes = { :name => agent_data['name'], + :schedule => agent_data['schedule'], + :keep_events_for => agent_data['keep_events_for'], + :propagate_immediately => agent_data['propagate_immediately'], + :disabled => agent_data['disabled'], + :options => agent_data['options'], + :scenario_ids => [@scenario.id] } + agent.save! + agent + end + + links.each do |link| + receiver = created_agents[link['receiver']] + source = created_agents[link['source']] + receiver.sources << source unless receiver.sources.include?(source) + end + end end def scenario - existing_scenario + @scenario || @existing_scenario end protected @@ -65,10 +101,12 @@ class ScenarioImport def validate_data if data.present? @parsed_data = JSON.parse(data) rescue {} - if (%w[name guid] - @parsed_data.keys).length > 0 + if (%w[name guid agents] - @parsed_data.keys).length > 0 errors.add(:base, "The provided data does not appear to be a valid Scenario.") self.data = nil end + else + @parsed_data = nil end end diff --git a/app/views/scenario_imports/_step_two.html.erb b/app/views/scenario_imports/_step_two.html.erb index 590cfa9b..5d42ba5a 100644 --- a/app/views/scenario_imports/_step_two.html.erb +++ b/app/views/scenario_imports/_step_two.html.erb @@ -32,11 +32,20 @@ }); + <% if @scenario_import.dangerous? %> +
+ This Scenario contains one or more potentially dangerous Agents. + These may be able to run local commands or execute code. + Please be sure that you understand the above Agent configurations before importing! +
+ <% end %> + <% if @scenario_import.existing_scenario.present? %> - - This Scenario already exists on your Huginn. - If you continue, the import will overwrite your existing <%= @scenario_import.existing_scenario.name %> Scenario. - +
+ This Scenario already exists in your system. + If you continue, the import will overwrite your existing + <%= @scenario_import.existing_scenario.name %> Scenario and the Agents in it. +
<% end %>
diff --git a/app/views/scenario_imports/new.html.erb b/app/views/scenario_imports/new.html.erb index c3623468..c3dbcad7 100644 --- a/app/views/scenario_imports/new.html.erb +++ b/app/views/scenario_imports/new.html.erb @@ -1,4 +1,4 @@ -
+
<% if @scenario_import.errors.any? %> diff --git a/app/views/scenarios/index.html.erb b/app/views/scenarios/index.html.erb index 81d826ec..e48c3a05 100644 --- a/app/views/scenarios/index.html.erb +++ b/app/views/scenarios/index.html.erb @@ -13,6 +13,7 @@ Name Agents + Public @@ -22,6 +23,7 @@ <%= link_to(scenario.name, scenario, class: "label label-info") %> <%= link_to pluralize(scenario.agents.count, "agent"), scenario %> + <%= scenario.public? ? "yes" : "no" %>
<%= link_to 'Show', scenario, class: "btn btn-default" %> diff --git a/db/migrate/20140602014917_add_indices_to_scenarios.rb b/db/migrate/20140602014917_add_indices_to_scenarios.rb index 288a57ae..5774cf5b 100644 --- a/db/migrate/20140602014917_add_indices_to_scenarios.rb +++ b/db/migrate/20140602014917_add_indices_to_scenarios.rb @@ -1,6 +1,6 @@ class AddIndicesToScenarios < ActiveRecord::Migration def change - add_index :scenarios, [:user_id, :guid] + add_index :scenarios, [:user_id, :guid], :unique => true add_index :scenario_memberships, :agent_id add_index :scenario_memberships, :scenario_id end diff --git a/db/migrate/20140605032822_add_guid_to_agents.rb b/db/migrate/20140605032822_add_guid_to_agents.rb new file mode 100644 index 00000000..f13be9b1 --- /dev/null +++ b/db/migrate/20140605032822_add_guid_to_agents.rb @@ -0,0 +1,15 @@ +class AddGuidToAgents < ActiveRecord::Migration + class Agent < ActiveRecord::Base; end + + def change + add_column :agents, :guid, :string + + Agent.find_each do |agent| + agent.update_attribute :guid, SecureRandom.hex + end + + change_column_null :agents, :guid, false + + add_index :agents, :guid + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 8cebf1ad..23e69d6e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140602014917) do +ActiveRecord::Schema.define(version: 20140605032822) do create_table "agent_logs", force: true do |t| t.integer "agent_id", null: false @@ -42,8 +42,10 @@ ActiveRecord::Schema.define(version: 20140602014917) do t.integer "keep_events_for", default: 0, null: false t.boolean "propagate_immediately", default: false, null: false t.boolean "disabled", default: false, null: false + t.string "guid", null: false end + add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree add_index "agents", ["type"], name: "index_agents_on_type", using: :btree add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree @@ -111,7 +113,7 @@ ActiveRecord::Schema.define(version: 20140602014917) do t.string "source_url" end - add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", using: :btree + add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree create_table "user_credentials", force: true do |t| t.integer "user_id", null: false diff --git a/lib/agents_exporter.rb b/lib/agents_exporter.rb index 1826cccc..4302c0e6 100644 --- a/lib/agents_exporter.rb +++ b/lib/agents_exporter.rb @@ -46,7 +46,7 @@ class AgentsExporter :keep_events_for => agent.keep_events_for, :propagate_immediately => agent.propagate_immediately, :disabled => agent.disabled, - :source_system_agent_id => agent.id, + :guid => agent.guid, :options => agent.options } end diff --git a/spec/fixtures/agents.yml b/spec/fixtures/agents.yml index d07e0ccb..26e1962b 100644 --- a/spec/fixtures/agents.yml +++ b/spec/fixtures/agents.yml @@ -4,6 +4,7 @@ jane_website_agent: events_count: 1 schedule: "5pm" name: "ZKCD" + guid: <%= SecureRandom.hex %> options: <%= { :url => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss", :expected_update_period_in_days => 2, @@ -20,6 +21,7 @@ bob_website_agent: events_count: 1 schedule: "midnight" name: "ZKCD" + guid: <%= SecureRandom.hex %> options: <%= { :url => "http://xkcd.com", :expected_update_period_in_days => 2, @@ -35,6 +37,7 @@ bob_weather_agent: user: bob schedule: "midnight" name: "SF Weather" + guid: <%= SecureRandom.hex %> keep_events_for: 45 options: <%= { :location => 94102, @@ -48,6 +51,7 @@ jane_weather_agent: user: jane schedule: "midnight" name: "SF Weather" + guid: <%= SecureRandom.hex %> keep_events_for: 30 options: <%= { :location => 94103, @@ -60,6 +64,7 @@ jane_rain_notifier_agent: type: Agents::TriggerAgent user: jane name: "Jane's Rain Watcher" + guid: <%= SecureRandom.hex %> options: <%= { :expected_receive_period_in_days => "2", :rules => [{ @@ -74,6 +79,7 @@ bob_rain_notifier_agent: type: Agents::TriggerAgent user: bob name: "Bob's Rain Watcher" + guid: <%= SecureRandom.hex %> options: <%= { :expected_receive_period_in_days => "2", :rules => [{ @@ -88,6 +94,7 @@ bob_twitter_user_agent: type: Agents::TwitterUserAgent user: bob name: "Bob's Twitter User Watcher" + guid: <%= SecureRandom.hex %> options: <%= { :username => "tectonic", :expected_update_period_in_days => "2", @@ -101,3 +108,4 @@ bob_manual_event_agent: type: Agents::ManualEventAgent user: bob name: "Bob's event testing agent" + guid: <%= SecureRandom.hex %> diff --git a/spec/lib/agents_exporter_spec.rb b/spec/lib/agents_exporter_spec.rb index b028d2d3..1dfb3f2d 100644 --- a/spec/lib/agents_exporter_spec.rb +++ b/spec/lib/agents_exporter_spec.rb @@ -20,7 +20,7 @@ describe AgentsExporter do Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) data[:links].should == [{ :source => 0, :receiver => 1 }] data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } - data[:agents].all? { |agent_json| agent_json[:source_system_agent_id] && agent_json[:type] && agent_json[:name] }.should be_true + data[:agents].all? { |agent_json| agent_json[:guid].present? && agent_json[:type].present? && agent_json[:name].present? }.should be_true end it "does not output links to other agents" do diff --git a/spec/models/agent_spec.rb b/spec/models/agent_spec.rb index a7d352f5..fa4f2370 100644 --- a/spec/models/agent_spec.rb +++ b/spec/models/agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/working_helpers' describe Agent do it_behaves_like WorkingHelpers @@ -122,6 +121,14 @@ describe Agent do stub(Agents::CannotBeScheduled).valid_type?("Agents::CannotBeScheduled") { true } end + let(:new_instance) do + agent = Agents::SomethingSource.new(:name => "some agent") + agent.user = users(:bob) + agent + end + + it_behaves_like HasGuid + describe ".default_schedule" do it "stores the default on the class" do Agents::SomethingSource.default_schedule.should == "2pm" diff --git a/spec/models/agents/data_output_agent_spec.rb b/spec/models/agents/data_output_agent_spec.rb index 6999a156..5e981e71 100644 --- a/spec/models/agents/data_output_agent_spec.rb +++ b/spec/models/agents/data_output_agent_spec.rb @@ -1,7 +1,6 @@ # encoding: utf-8 require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::DataOutputAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/hipchat_agent_spec.rb b/spec/models/agents/hipchat_agent_spec.rb index 5f29b50e..3682db30 100644 --- a/spec/models/agents/hipchat_agent_spec.rb +++ b/spec/models/agents/hipchat_agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::HipchatAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/human_task_agent_spec.rb b/spec/models/agents/human_task_agent_spec.rb index 615827d8..68e2c216 100644 --- a/spec/models/agents/human_task_agent_spec.rb +++ b/spec/models/agents/human_task_agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::HumanTaskAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/jabber_agent_spec.rb b/spec/models/agents/jabber_agent_spec.rb index 9a5cb47a..a4e6e7f5 100644 --- a/spec/models/agents/jabber_agent_spec.rb +++ b/spec/models/agents/jabber_agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::JabberAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/peak_detector_agent_spec.rb b/spec/models/agents/peak_detector_agent_spec.rb index 5b3b8260..83c61e02 100644 --- a/spec/models/agents/peak_detector_agent_spec.rb +++ b/spec/models/agents/peak_detector_agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::PeakDetectorAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/pushbullet_agent_spec.rb b/spec/models/agents/pushbullet_agent_spec.rb index 58356a9a..586b2069 100644 --- a/spec/models/agents/pushbullet_agent_spec.rb +++ b/spec/models/agents/pushbullet_agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::PushbulletAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/slack_agent_spec.rb b/spec/models/agents/slack_agent_spec.rb index 383eacf0..c97c1536 100644 --- a/spec/models/agents/slack_agent_spec.rb +++ b/spec/models/agents/slack_agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::SlackAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/translation_agent_spec.rb b/spec/models/agents/translation_agent_spec.rb index be24f9cb..85996cac 100644 --- a/spec/models/agents/translation_agent_spec.rb +++ b/spec/models/agents/translation_agent_spec.rb @@ -1,6 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' - describe Agents::TranslationAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/agents/trigger_agent_spec.rb b/spec/models/agents/trigger_agent_spec.rb index 8fe3e87d..d99b90f1 100644 --- a/spec/models/agents/trigger_agent_spec.rb +++ b/spec/models/agents/trigger_agent_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require 'models/concerns/liquid_interpolatable' describe Agents::TriggerAgent do it_behaves_like LiquidInterpolatable diff --git a/spec/models/scenario_import_spec.rb b/spec/models/scenario_import_spec.rb index 53ebdb3f..4c77e28a 100644 --- a/spec/models/scenario_import_spec.rb +++ b/spec/models/scenario_import_spec.rb @@ -1,6 +1,64 @@ require 'spec_helper' describe ScenarioImport do + let(:guid) { "somescenarioguid" } + 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_data) do + { + :name => name, + :description => description, + :guid => guid, + :source_url => source_url, + :exported_at => 2.days.ago.utc.iso8601, + :agents => [ + { + :type => "Agents::WeatherAgent", + :name => "a weather agent", + :schedule => "5pm", + :keep_events_for => 14, + :propagate_immediately => false, + :disabled => false, + :guid => "a-weather-agent", + :options => weather_agent_options + }, + { + :type => "Agents::TriggerAgent", + :name => "listen for weather", + :schedule => nil, + :keep_events_for => 0, + :propagate_immediately => true, + :disabled => true, + :guid => "a-trigger-agent", + :options => trigger_agent_options + } + ], + :links => [ + { :source => 0, :receiver => 1 } + ] + } + 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 ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com" @@ -9,8 +67,6 @@ describe ScenarioImport do describe "validations" do subject { ScenarioImport.new } - let(:valid_json) { { :name => "some scenario", :guid => "someguid" }.to_json } - let(:invalid_json) { { :name => "some scenario missing a guid" }.to_json } it "is not valid when none of file, url, or data are present" do subject.should_not be_valid @@ -20,7 +76,7 @@ describe ScenarioImport do describe "data" do it "should be invalid with invalid data" do - subject.data = invalid_json + subject.data = invalid_data subject.should_not be_valid subject.should have(1).error_on(:base) @@ -33,7 +89,7 @@ describe ScenarioImport do end it "should be valid with valid data" do - subject.data = valid_json + subject.data = valid_data subject.should be_valid end end @@ -47,14 +103,14 @@ describe ScenarioImport do 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_json) + 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" subject.should_not be_valid subject.errors[:base].should 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_json) + 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" subject.should be_valid end @@ -66,15 +122,139 @@ describe ScenarioImport do subject.should_not be_valid subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") - subject.file = StringIO.new(invalid_json) + subject.file = StringIO.new(invalid_data) subject.should_not be_valid subject.errors[:base].should 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_json) + subject.file = StringIO.new(valid_data) subject.should be_valid end end end + + describe "#dangerous?" do + it "returns false on most Agents" do + ScenarioImport.new(:data => valid_data).should_not be_dangerous + end + + it "returns true if a ShellCommandAgent is present" do + valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent" + ScenarioImport.new(:data => valid_parsed_data.to_json).should be_dangerous + end + end + + describe "#import!" 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 + it "makes a new scenario" do + lambda { + scenario_import.import!(:skip_agents => true) + }.should change { users(:bob).scenarios.count }.by(1) + + scenario_import.scenario.name.should == name + scenario_import.scenario.description.should == description + scenario_import.scenario.guid.should == guid + scenario_import.scenario.source_url.should == source_url + scenario_import.scenario.public.should be_false + end + + it "creates the Agents" do + lambda { + scenario_import.import! + }.should 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") + + weather_agent.name.should == "a weather agent" + weather_agent.schedule.should == "5pm" + weather_agent.keep_events_for.should == 14 + weather_agent.propagate_immediately.should be_false + weather_agent.should_not be_disabled + weather_agent.memory.should be_empty + weather_agent.options.should == weather_agent_options + + trigger_agent.name.should == "listen for weather" + trigger_agent.sources.should == [weather_agent] + trigger_agent.schedule.should be_nil + trigger_agent.keep_events_for.should == 0 + trigger_agent.propagate_immediately.should be_true + trigger_agent.should be_disabled + trigger_agent.memory.should be_empty + trigger_agent.options.should == 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" + + lambda { + scenario_import.import! + }.should change { users(:bob).agents.count }.by(2) + end + end + + context "when an a scenario already exists with the given guid" do + let!(:existing_scenario) { + _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario") + _existing_scenerio.guid = guid + _existing_scenerio.save! + _existing_scenerio + } + + it "uses the existing scenario, updating it's data" do + lambda { + scenario_import.import!(:skip_agents => true) + scenario_import.scenario.should == existing_scenario + }.should_not change { users(:bob).scenarios.count } + + existing_scenario.reload + existing_scenario.guid.should == guid + existing_scenario.description.should == description + existing_scenario.name.should == name + existing_scenario.source_url.should == source_url + existing_scenario.public.should be_false + end + + it "updates any existing agents in the scenario, and makes new ones as needed" do + agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" + agents(:bob_weather_agent).scenarios << existing_scenario + + lambda { + # Shouldn't matter how many times we do it! + scenario_import.import! + scenario_import.import! + scenario_import.import! + }.should change { users(:bob).agents.count }.by(1) + + weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") + trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent") + + weather_agent.should == agents(:bob_weather_agent) + + weather_agent.name.should == "a weather agent" + weather_agent.schedule.should == "5pm" + weather_agent.keep_events_for.should == 14 + weather_agent.propagate_immediately.should be_false + weather_agent.should_not be_disabled + weather_agent.memory.should be_empty + weather_agent.options.should == weather_agent_options + + trigger_agent.name.should == "listen for weather" + trigger_agent.sources.should == [weather_agent] + trigger_agent.schedule.should be_nil + trigger_agent.keep_events_for.should == 0 + trigger_agent.propagate_immediately.should be_true + trigger_agent.should be_disabled + trigger_agent.memory.should be_empty + trigger_agent.options.should == trigger_agent_options + end + end + end end \ No newline at end of file diff --git a/spec/models/scenario_spec.rb b/spec/models/scenario_spec.rb index 8510b0c0..f3d96c47 100644 --- a/spec/models/scenario_spec.rb +++ b/spec/models/scenario_spec.rb @@ -1,54 +1,42 @@ require 'spec_helper' describe Scenario do + let(:new_instance) { users(:bob).scenarios.build(:name => "some scenario") } + + it_behaves_like HasGuid + describe "validations" do before do - @scenario = users(:bob).scenarios.new(:name => "some scenario") - @scenario.should be_valid + new_instance.should be_valid end it "validates the presence of name" do - @scenario.name = '' - @scenario.should_not be_valid + new_instance.name = '' + new_instance.should_not be_valid end it "validates the presence of user" do - @scenario.user = nil - @scenario.should_not be_valid + new_instance.user = nil + new_instance.should_not be_valid end it "only allows Agents owned by user" do - @scenario.agent_ids = [agents(:bob_website_agent).id] - @scenario.should be_valid + new_instance.agent_ids = [agents(:bob_website_agent).id] + new_instance.should be_valid - @scenario.agent_ids = [agents(:jane_website_agent).id] - @scenario.should_not be_valid - end - end - - describe "guid" do - it "gets created before_save, but only if it's not present" do - scenario = users(:bob).scenarios.new(:name => "some scenario") - scenario.guid.should be_nil - scenario.save! - scenario.guid.should_not be_nil - - lambda { scenario.save! }.should_not change { scenario.reload.guid } + new_instance.agent_ids = [agents(:jane_website_agent).id] + new_instance.should_not be_valid end end describe "counters" do - before do - @scenario = users(:bob).scenarios.new(:name => "some scenario") - end - it "maintains a counter cache on user" do lambda { - @scenario.save! + new_instance.save! }.should change { users(:bob).reload.scenario_count }.by(1) lambda { - @scenario.destroy + new_instance.destroy }.should change { users(:bob).reload.scenario_count }.by(-1) end end diff --git a/spec/support/shared_examples/has_guid.rb b/spec/support/shared_examples/has_guid.rb new file mode 100644 index 00000000..a4120023 --- /dev/null +++ b/spec/support/shared_examples/has_guid.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +shared_examples_for HasGuid do + it "gets created before_save, but only if it's not present" do + instance = new_instance + instance.guid.should be_nil + instance.save! + instance.guid.should_not be_nil + + lambda { instance.save! }.should_not change { instance.reload.guid } + end +end diff --git a/spec/models/concerns/liquid_interpolatable.rb b/spec/support/shared_examples/liquid_interpolatable.rb similarity index 100% rename from spec/models/concerns/liquid_interpolatable.rb rename to spec/support/shared_examples/liquid_interpolatable.rb diff --git a/spec/models/concerns/working_helpers.rb b/spec/support/shared_examples/working_helpers.rb similarity index 96% rename from spec/models/concerns/working_helpers.rb rename to spec/support/shared_examples/working_helpers.rb index c101e495..6da44a6e 100644 --- a/spec/models/concerns/working_helpers.rb +++ b/spec/support/shared_examples/working_helpers.rb @@ -3,7 +3,7 @@ require 'spec_helper' shared_examples_for WorkingHelpers do describe "recent_error_logs?" do it "returns true if last_error_log_at is near last_event_at" do - agent = Agent.new + agent = described_class.new agent.last_error_log_at = 10.minutes.ago agent.last_event_at = 10.minutes.ago @@ -26,9 +26,10 @@ shared_examples_for WorkingHelpers do agent.recent_error_logs?.should be_false end end + describe "received_event_without_error?" do before do - @agent = Agent.new + @agent = described_class.new end it "should return false until the first event was received" do @@ -49,5 +50,4 @@ shared_examples_for WorkingHelpers do @agent.received_event_without_error?.should == true end end - end