| @@ -158,6 +158,7 @@ h2 .scenario, a span.label.scenario { | ||
| 158 | 158 | } | 
| 159 | 159 |  | 
| 160 | 160 | // Bootstrappy color styles | 
| 161 | + | |
| 161 | 162 |  .color-danger { | 
| 162 | 163 | color: #d9534f; | 
| 163 | 164 | } | 
| @@ -0,0 +1,9 @@ | ||
| 1 | +.scenario-import { | |
| 2 | +  .danger { | |
| 3 | + color: red; | |
| 4 | + font-weight: strong; | |
| 5 | + border: 1px solid red; | |
| 6 | + padding: 10px; | |
| 7 | + margin: 10px 0; | |
| 8 | + } | |
| 9 | +} | 
| @@ -29,7 +29,7 @@ module AssignableTypes | ||
| 29 | 29 | const_get(:TYPES).include?(type) | 
| 30 | 30 | end | 
| 31 | 31 |  | 
| 32 | - def build_for_type(type, user, attributes) | |
| 32 | +    def build_for_type(type, user, attributes = {}) | |
| 33 | 33 | attributes.delete(:type) | 
| 34 | 34 |  | 
| 35 | 35 | if valid_type?(type) | 
| @@ -0,0 +1,13 @@ | ||
| 1 | +module HasGuid | |
| 2 | + extend ActiveSupport::Concern | |
| 3 | + | |
| 4 | + included do | |
| 5 | + before_save :make_guid | |
| 6 | + end | |
| 7 | + | |
| 8 | + protected | |
| 9 | + | |
| 10 | + def make_guid | |
| 11 | + self.guid = SecureRandom.hex unless guid.present? | |
| 12 | + end | |
| 13 | +end | 
| @@ -12,6 +12,7 @@ class Agent < ActiveRecord::Base | ||
| 12 | 12 | include JSONSerializedField | 
| 13 | 13 | include RDBMSFunctions | 
| 14 | 14 | include WorkingHelpers | 
| 15 | + include HasGuid | |
| 15 | 16 |  | 
| 16 | 17 | markdown_class_attributes :description, :event_description | 
| 17 | 18 |  | 
| @@ -1,22 +1,18 @@ | ||
| 1 | 1 | class Scenario < ActiveRecord::Base | 
| 2 | - attr_accessible :name, :agent_ids, :description, :public | |
| 2 | + include HasGuid | |
| 3 | + | |
| 4 | + attr_accessible :name, :agent_ids, :description, :public, :source_url | |
| 3 | 5 |  | 
| 4 | 6 | belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios | 
| 5 | 7 | has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario | 
| 6 | 8 | has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios | 
| 7 | 9 |  | 
| 8 | - before_save :make_guid | |
| 9 | - | |
| 10 | 10 | validates_presence_of :name, :user | 
| 11 | 11 |  | 
| 12 | 12 | validate :agents_are_owned | 
| 13 | 13 |  | 
| 14 | 14 | protected | 
| 15 | 15 |  | 
| 16 | - def make_guid | |
| 17 | - self.guid = SecureRandom.hex unless guid.present? | |
| 18 | - end | |
| 19 | - | |
| 20 | 16 | def agents_are_owned | 
| 21 | 17 |      errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user } | 
| 22 | 18 | end | 
| @@ -4,6 +4,7 @@ class ScenarioImport | ||
| 4 | 4 | include ActiveModel::Callbacks | 
| 5 | 5 | include ActiveModel::Validations::Callbacks | 
| 6 | 6 |  | 
| 7 | + DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent] | |
| 7 | 8 | URL_REGEX = /\Ahttps?:\/\//i | 
| 8 | 9 |  | 
| 9 | 10 | attr_accessor :file, :url, :data, :do_import | 
| @@ -33,19 +34,54 @@ class ScenarioImport | ||
| 33 | 34 | @existing_scenario ||= user.scenarios.find_by_guid(parsed_data["guid"]) | 
| 34 | 35 | end | 
| 35 | 36 |  | 
| 37 | + def dangerous? | |
| 38 | +    (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) } | |
| 39 | + end | |
| 40 | + | |
| 36 | 41 | def parsed_data | 
| 37 | - @parsed_data | |
| 42 | +    @parsed_data ||= data && JSON.parse(data) rescue {} | |
| 38 | 43 | end | 
| 39 | 44 |  | 
| 40 | 45 | def do_import? | 
| 41 | 46 | do_import == "1" | 
| 42 | 47 | end | 
| 43 | 48 |  | 
| 44 | - def import! | |
| 49 | +  def import!(options = {}) | |
| 50 | + guid = parsed_data['guid'] | |
| 51 | + description = parsed_data['description'] | |
| 52 | + name = parsed_data['name'] | |
| 53 | + agents = parsed_data['agents'] | |
| 54 | + links = parsed_data['links'] | |
| 55 | + source_url = parsed_data['source_url'].presence || nil | |
| 56 | + @scenario = user.scenarios.where(:guid => guid).first_or_initialize | |
| 57 | + @scenario.update_attributes!(:name => name, :description => description, | |
| 58 | + :source_url => source_url, :public => false) | |
| 59 | + | |
| 60 | + unless options[:skip_agents] | |
| 61 | + created_agents = agents.map do |agent_data| | |
| 62 | + agent = @scenario.agents.find_by(:guid => agent_data['guid']) || Agent.build_for_type(agent_data['type'], user) | |
| 63 | + agent.guid = agent_data['guid'] | |
| 64 | +        agent.attributes = { :name => agent_data['name'], | |
| 65 | + :schedule => agent_data['schedule'], | |
| 66 | + :keep_events_for => agent_data['keep_events_for'], | |
| 67 | + :propagate_immediately => agent_data['propagate_immediately'], | |
| 68 | + :disabled => agent_data['disabled'], | |
| 69 | + :options => agent_data['options'], | |
| 70 | + :scenario_ids => [@scenario.id] } | |
| 71 | + agent.save! | |
| 72 | + agent | |
| 73 | + end | |
| 74 | + | |
| 75 | + links.each do |link| | |
| 76 | + receiver = created_agents[link['receiver']] | |
| 77 | + source = created_agents[link['source']] | |
| 78 | + receiver.sources << source unless receiver.sources.include?(source) | |
| 79 | + end | |
| 80 | + end | |
| 45 | 81 | end | 
| 46 | 82 |  | 
| 47 | 83 | def scenario | 
| 48 | - existing_scenario | |
| 84 | + @scenario || @existing_scenario | |
| 49 | 85 | end | 
| 50 | 86 |  | 
| 51 | 87 | protected | 
| @@ -65,10 +101,12 @@ class ScenarioImport | ||
| 65 | 101 | def validate_data | 
| 66 | 102 | if data.present? | 
| 67 | 103 |        @parsed_data = JSON.parse(data) rescue {} | 
| 68 | - if (%w[name guid] - @parsed_data.keys).length > 0 | |
| 104 | + if (%w[name guid agents] - @parsed_data.keys).length > 0 | |
| 69 | 105 | errors.add(:base, "The provided data does not appear to be a valid Scenario.") | 
| 70 | 106 | self.data = nil | 
| 71 | 107 | end | 
| 108 | + else | |
| 109 | + @parsed_data = nil | |
| 72 | 110 | end | 
| 73 | 111 | end | 
| 74 | 112 |  | 
| @@ -32,11 +32,20 @@ | ||
| 32 | 32 | }); | 
| 33 | 33 | </script> | 
| 34 | 34 |  | 
| 35 | + <% if @scenario_import.dangerous? %> | |
| 36 | + <div class="danger"> | |
| 37 | + This Scenario contains one or more potentially dangerous Agents. | |
| 38 | + These may be able to run local commands or execute code. | |
| 39 | + Please be sure that you understand the above Agent configurations before importing! | |
| 40 | + </div> | |
| 41 | + <% end %> | |
| 42 | + | |
| 35 | 43 | <% if @scenario_import.existing_scenario.present? %> | 
| 36 | - <strong> | |
| 37 | - This Scenario already exists on your Huginn. | |
| 38 | - If you continue, the import will overwrite your existing <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario. | |
| 39 | - </strong> | |
| 44 | + <div class="danger"> | |
| 45 | + This Scenario already exists in your system. | |
| 46 | + If you continue, the import will overwrite your existing | |
| 47 | + <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario and the Agents in it. | |
| 48 | + </div> | |
| 40 | 49 | <% end %> | 
| 41 | 50 |  | 
| 42 | 51 | <div class="checkbox"> | 
| @@ -1,4 +1,4 @@ | ||
| 1 | -<div class='container'> | |
| 1 | +<div class='container scenario-import'> | |
| 2 | 2 | <div class='row'> | 
| 3 | 3 | <div class='col-md-12'> | 
| 4 | 4 | <% if @scenario_import.errors.any? %> | 
| @@ -13,6 +13,7 @@ | ||
| 13 | 13 | <tr> | 
| 14 | 14 | <th>Name</th> | 
| 15 | 15 | <th>Agents</th> | 
| 16 | + <th>Public</th> | |
| 16 | 17 | <th></th> | 
| 17 | 18 | </tr> | 
| 18 | 19 |  | 
| @@ -22,6 +23,7 @@ | ||
| 22 | 23 | <%= link_to(scenario.name, scenario, class: "label label-info") %> | 
| 23 | 24 | </td> | 
| 24 | 25 | <td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td> | 
| 26 | + <td><%= scenario.public? ? "yes" : "no" %></td> | |
| 25 | 27 | <td> | 
| 26 | 28 | <div class="btn-group btn-group-xs" style="float: right"> | 
| 27 | 29 | <%= link_to 'Show', scenario, class: "btn btn-default" %> | 
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | class AddIndicesToScenarios < ActiveRecord::Migration | 
| 2 | 2 | def change | 
| 3 | - add_index :scenarios, [:user_id, :guid] | |
| 3 | + add_index :scenarios, [:user_id, :guid], :unique => true | |
| 4 | 4 | add_index :scenario_memberships, :agent_id | 
| 5 | 5 | add_index :scenario_memberships, :scenario_id | 
| 6 | 6 | end | 
| @@ -0,0 +1,15 @@ | ||
| 1 | +class AddGuidToAgents < ActiveRecord::Migration | |
| 2 | + class Agent < ActiveRecord::Base; end | |
| 3 | + | |
| 4 | + def change | |
| 5 | + add_column :agents, :guid, :string | |
| 6 | + | |
| 7 | + Agent.find_each do |agent| | |
| 8 | + agent.update_attribute :guid, SecureRandom.hex | |
| 9 | + end | |
| 10 | + | |
| 11 | + change_column_null :agents, :guid, false | |
| 12 | + | |
| 13 | + add_index :agents, :guid | |
| 14 | + end | |
| 15 | +end | 
| @@ -11,7 +11,7 @@ | ||
| 11 | 11 | # | 
| 12 | 12 | # It's strongly recommended that you check this file into your version control system. | 
| 13 | 13 |  | 
| 14 | -ActiveRecord::Schema.define(version: 20140602014917) do | |
| 14 | +ActiveRecord::Schema.define(version: 20140605032822) do | |
| 15 | 15 |  | 
| 16 | 16 | create_table "agent_logs", force: true do |t| | 
| 17 | 17 | t.integer "agent_id", null: false | 
| @@ -42,8 +42,10 @@ ActiveRecord::Schema.define(version: 20140602014917) do | ||
| 42 | 42 | t.integer "keep_events_for", default: 0, null: false | 
| 43 | 43 | t.boolean "propagate_immediately", default: false, null: false | 
| 44 | 44 | t.boolean "disabled", default: false, null: false | 
| 45 | + t.string "guid", null: false | |
| 45 | 46 | end | 
| 46 | 47 |  | 
| 48 | + add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree | |
| 47 | 49 | add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree | 
| 48 | 50 | add_index "agents", ["type"], name: "index_agents_on_type", using: :btree | 
| 49 | 51 | 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 | ||
| 111 | 113 | t.string "source_url" | 
| 112 | 114 | end | 
| 113 | 115 |  | 
| 114 | - add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", using: :btree | |
| 116 | + add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree | |
| 115 | 117 |  | 
| 116 | 118 | create_table "user_credentials", force: true do |t| | 
| 117 | 119 | t.integer "user_id", null: false | 
| @@ -46,7 +46,7 @@ class AgentsExporter | ||
| 46 | 46 | :keep_events_for => agent.keep_events_for, | 
| 47 | 47 | :propagate_immediately => agent.propagate_immediately, | 
| 48 | 48 | :disabled => agent.disabled, | 
| 49 | - :source_system_agent_id => agent.id, | |
| 49 | + :guid => agent.guid, | |
| 50 | 50 | :options => agent.options | 
| 51 | 51 | } | 
| 52 | 52 | end | 
| @@ -4,6 +4,7 @@ jane_website_agent: | ||
| 4 | 4 | events_count: 1 | 
| 5 | 5 | schedule: "5pm" | 
| 6 | 6 | name: "ZKCD" | 
| 7 | + guid: <%= SecureRandom.hex %> | |
| 7 | 8 |    options: <%= { | 
| 8 | 9 | :url => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss", | 
| 9 | 10 | :expected_update_period_in_days => 2, | 
| @@ -20,6 +21,7 @@ bob_website_agent: | ||
| 20 | 21 | events_count: 1 | 
| 21 | 22 | schedule: "midnight" | 
| 22 | 23 | name: "ZKCD" | 
| 24 | + guid: <%= SecureRandom.hex %> | |
| 23 | 25 |    options: <%= { | 
| 24 | 26 | :url => "http://xkcd.com", | 
| 25 | 27 | :expected_update_period_in_days => 2, | 
| @@ -35,6 +37,7 @@ bob_weather_agent: | ||
| 35 | 37 | user: bob | 
| 36 | 38 | schedule: "midnight" | 
| 37 | 39 | name: "SF Weather" | 
| 40 | + guid: <%= SecureRandom.hex %> | |
| 38 | 41 | keep_events_for: 45 | 
| 39 | 42 |    options: <%= { | 
| 40 | 43 | :location => 94102, | 
| @@ -48,6 +51,7 @@ jane_weather_agent: | ||
| 48 | 51 | user: jane | 
| 49 | 52 | schedule: "midnight" | 
| 50 | 53 | name: "SF Weather" | 
| 54 | + guid: <%= SecureRandom.hex %> | |
| 51 | 55 | keep_events_for: 30 | 
| 52 | 56 |    options: <%= { | 
| 53 | 57 | :location => 94103, | 
| @@ -60,6 +64,7 @@ jane_rain_notifier_agent: | ||
| 60 | 64 | type: Agents::TriggerAgent | 
| 61 | 65 | user: jane | 
| 62 | 66 | name: "Jane's Rain Watcher" | 
| 67 | + guid: <%= SecureRandom.hex %> | |
| 63 | 68 |    options: <%= { | 
| 64 | 69 | :expected_receive_period_in_days => "2", | 
| 65 | 70 |                   :rules => [{ | 
| @@ -74,6 +79,7 @@ bob_rain_notifier_agent: | ||
| 74 | 79 | type: Agents::TriggerAgent | 
| 75 | 80 | user: bob | 
| 76 | 81 | name: "Bob's Rain Watcher" | 
| 82 | + guid: <%= SecureRandom.hex %> | |
| 77 | 83 |    options: <%= { | 
| 78 | 84 | :expected_receive_period_in_days => "2", | 
| 79 | 85 |                   :rules => [{ | 
| @@ -88,6 +94,7 @@ bob_twitter_user_agent: | ||
| 88 | 94 | type: Agents::TwitterUserAgent | 
| 89 | 95 | user: bob | 
| 90 | 96 | name: "Bob's Twitter User Watcher" | 
| 97 | + guid: <%= SecureRandom.hex %> | |
| 91 | 98 |    options: <%= { | 
| 92 | 99 | :username => "tectonic", | 
| 93 | 100 | :expected_update_period_in_days => "2", | 
| @@ -101,3 +108,4 @@ bob_manual_event_agent: | ||
| 101 | 108 | type: Agents::ManualEventAgent | 
| 102 | 109 | user: bob | 
| 103 | 110 | name: "Bob's event testing agent" | 
| 111 | + guid: <%= SecureRandom.hex %> | 
| @@ -20,7 +20,7 @@ describe AgentsExporter do | ||
| 20 | 20 | Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) | 
| 21 | 21 |        data[:links].should == [{ :source => 0, :receiver => 1 }] | 
| 22 | 22 |        data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } | 
| 23 | -      data[:agents].all? { |agent_json| agent_json[:source_system_agent_id] && agent_json[:type] && agent_json[:name] }.should be_true | |
| 23 | +      data[:agents].all? { |agent_json| agent_json[:guid].present? && agent_json[:type].present? && agent_json[:name].present? }.should be_true | |
| 24 | 24 | end | 
| 25 | 25 |  | 
| 26 | 26 | it "does not output links to other agents" do | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/working_helpers' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agent do | 
| 5 | 4 | it_behaves_like WorkingHelpers | 
| @@ -122,6 +121,14 @@ describe Agent do | ||
| 122 | 121 |        stub(Agents::CannotBeScheduled).valid_type?("Agents::CannotBeScheduled") { true } | 
| 123 | 122 | end | 
| 124 | 123 |  | 
| 124 | + let(:new_instance) do | |
| 125 | + agent = Agents::SomethingSource.new(:name => "some agent") | |
| 126 | + agent.user = users(:bob) | |
| 127 | + agent | |
| 128 | + end | |
| 129 | + | |
| 130 | + it_behaves_like HasGuid | |
| 131 | + | |
| 125 | 132 | describe ".default_schedule" do | 
| 126 | 133 | it "stores the default on the class" do | 
| 127 | 134 | Agents::SomethingSource.default_schedule.should == "2pm" | 
| @@ -1,7 +1,6 @@ | ||
| 1 | 1 | # encoding: utf-8 | 
| 2 | 2 |  | 
| 3 | 3 | require 'spec_helper' | 
| 4 | -require 'models/concerns/liquid_interpolatable' | |
| 5 | 4 |  | 
| 6 | 5 | describe Agents::DataOutputAgent do | 
| 7 | 6 | it_behaves_like LiquidInterpolatable | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agents::HipchatAgent do | 
| 5 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agents::HumanTaskAgent do | 
| 5 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agents::JabberAgent do | 
| 5 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agents::PeakDetectorAgent do | 
| 5 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agents::PushbulletAgent do | 
| 5 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agents::SlackAgent do | 
| 5 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,6 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | - | |
| 4 | 2 |  | 
| 5 | 3 | describe Agents::TranslationAgent do | 
| 6 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | -require 'models/concerns/liquid_interpolatable' | |
| 3 | 2 |  | 
| 4 | 3 | describe Agents::TriggerAgent do | 
| 5 | 4 | it_behaves_like LiquidInterpolatable | 
| @@ -1,6 +1,64 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | 2 |  | 
| 3 | 3 | describe ScenarioImport do | 
| 4 | +  let(:guid) { "somescenarioguid" } | |
| 5 | +  let(:description) { "This is a cool Huginn Scenario that does something useful!" } | |
| 6 | +  let(:name) { "A useful Scenario" } | |
| 7 | +  let(:source_url) { "http://example.com/scenarios/2/export.json" } | |
| 8 | +  let(:weather_agent_options) { | |
| 9 | +    { | |
| 10 | + 'api_key' => 'some-api-key', | |
| 11 | + 'location' => '12345' | |
| 12 | + } | |
| 13 | + } | |
| 14 | +  let(:trigger_agent_options) { | |
| 15 | +    { | |
| 16 | + 'expected_receive_period_in_days' => 2, | |
| 17 | +      'rules' => [{ | |
| 18 | + 'type' => "regex", | |
| 19 | + 'value' => "rain|storm", | |
| 20 | + 'path' => "conditions", | |
| 21 | + }], | |
| 22 | + 'message' => "Looks like rain!" | |
| 23 | + } | |
| 24 | + } | |
| 25 | + let(:valid_parsed_data) do | |
| 26 | +    {  | |
| 27 | + :name => name, | |
| 28 | + :description => description, | |
| 29 | + :guid => guid, | |
| 30 | + :source_url => source_url, | |
| 31 | + :exported_at => 2.days.ago.utc.iso8601, | |
| 32 | + :agents => [ | |
| 33 | +        { | |
| 34 | + :type => "Agents::WeatherAgent", | |
| 35 | + :name => "a weather agent", | |
| 36 | + :schedule => "5pm", | |
| 37 | + :keep_events_for => 14, | |
| 38 | + :propagate_immediately => false, | |
| 39 | + :disabled => false, | |
| 40 | + :guid => "a-weather-agent", | |
| 41 | + :options => weather_agent_options | |
| 42 | + }, | |
| 43 | +        { | |
| 44 | + :type => "Agents::TriggerAgent", | |
| 45 | + :name => "listen for weather", | |
| 46 | + :schedule => nil, | |
| 47 | + :keep_events_for => 0, | |
| 48 | + :propagate_immediately => true, | |
| 49 | + :disabled => true, | |
| 50 | + :guid => "a-trigger-agent", | |
| 51 | + :options => trigger_agent_options | |
| 52 | + } | |
| 53 | + ], | |
| 54 | + :links => [ | |
| 55 | +        { :source => 0, :receiver => 1 } | |
| 56 | + ] | |
| 57 | + } | |
| 58 | + end | |
| 59 | +  let(:valid_data) { valid_parsed_data.to_json } | |
| 60 | +  let(:invalid_data) { { :name => "some scenario missing a guid" }.to_json } | |
| 61 | + | |
| 4 | 62 | describe "initialization" do | 
| 5 | 63 | it "is initialized with an attributes hash" do | 
| 6 | 64 | ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com" | 
| @@ -9,8 +67,6 @@ describe ScenarioImport do | ||
| 9 | 67 |  | 
| 10 | 68 | describe "validations" do | 
| 11 | 69 |      subject { ScenarioImport.new } | 
| 12 | -    let(:valid_json) { { :name => "some scenario", :guid => "someguid" }.to_json } | |
| 13 | -    let(:invalid_json) { { :name => "some scenario missing a guid" }.to_json } | |
| 14 | 70 |  | 
| 15 | 71 | it "is not valid when none of file, url, or data are present" do | 
| 16 | 72 | subject.should_not be_valid | 
| @@ -20,7 +76,7 @@ describe ScenarioImport do | ||
| 20 | 76 |  | 
| 21 | 77 | describe "data" do | 
| 22 | 78 | it "should be invalid with invalid data" do | 
| 23 | - subject.data = invalid_json | |
| 79 | + subject.data = invalid_data | |
| 24 | 80 | subject.should_not be_valid | 
| 25 | 81 | subject.should have(1).error_on(:base) | 
| 26 | 82 |  | 
| @@ -33,7 +89,7 @@ describe ScenarioImport do | ||
| 33 | 89 | end | 
| 34 | 90 |  | 
| 35 | 91 | it "should be valid with valid data" do | 
| 36 | - subject.data = valid_json | |
| 92 | + subject.data = valid_data | |
| 37 | 93 | subject.should be_valid | 
| 38 | 94 | end | 
| 39 | 95 | end | 
| @@ -47,14 +103,14 @@ describe ScenarioImport do | ||
| 47 | 103 | end | 
| 48 | 104 |  | 
| 49 | 105 | it "should be invalid when the referenced url doesn't contain a scenario" do | 
| 50 | - stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_json) | |
| 106 | + stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_data) | |
| 51 | 107 | subject.url = "http://example.com/scenarios/1/export.json" | 
| 52 | 108 | subject.should_not be_valid | 
| 53 | 109 |          subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") | 
| 54 | 110 | end | 
| 55 | 111 |  | 
| 56 | 112 | it "should be valid when the url points to a valid scenario" do | 
| 57 | - stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_json) | |
| 113 | + stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_data) | |
| 58 | 114 | subject.url = "http://example.com/scenarios/1/export.json" | 
| 59 | 115 | subject.should be_valid | 
| 60 | 116 | end | 
| @@ -66,15 +122,139 @@ describe ScenarioImport do | ||
| 66 | 122 | subject.should_not be_valid | 
| 67 | 123 |          subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") | 
| 68 | 124 |  | 
| 69 | - subject.file = StringIO.new(invalid_json) | |
| 125 | + subject.file = StringIO.new(invalid_data) | |
| 70 | 126 | subject.should_not be_valid | 
| 71 | 127 |          subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") | 
| 72 | 128 | end | 
| 73 | 129 |  | 
| 74 | 130 | it "should be valid with a valid uploaded scenario" do | 
| 75 | - subject.file = StringIO.new(valid_json) | |
| 131 | + subject.file = StringIO.new(valid_data) | |
| 76 | 132 | subject.should be_valid | 
| 77 | 133 | end | 
| 78 | 134 | end | 
| 79 | 135 | end | 
| 136 | + | |
| 137 | + describe "#dangerous?" do | |
| 138 | + it "returns false on most Agents" do | |
| 139 | + ScenarioImport.new(:data => valid_data).should_not be_dangerous | |
| 140 | + end | |
| 141 | + | |
| 142 | + it "returns true if a ShellCommandAgent is present" do | |
| 143 | + valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent" | |
| 144 | + ScenarioImport.new(:data => valid_parsed_data.to_json).should be_dangerous | |
| 145 | + end | |
| 146 | + end | |
| 147 | + | |
| 148 | + describe "#import!" do | |
| 149 | + let(:scenario_import) do | |
| 150 | + _import = ScenarioImport.new(:data => valid_data) | |
| 151 | + _import.set_user users(:bob) | |
| 152 | + _import | |
| 153 | + end | |
| 154 | + | |
| 155 | + context "when this scenario has never been seen before" do | |
| 156 | + it "makes a new scenario" do | |
| 157 | +        lambda { | |
| 158 | + scenario_import.import!(:skip_agents => true) | |
| 159 | +        }.should change { users(:bob).scenarios.count }.by(1) | |
| 160 | + | |
| 161 | + scenario_import.scenario.name.should == name | |
| 162 | + scenario_import.scenario.description.should == description | |
| 163 | + scenario_import.scenario.guid.should == guid | |
| 164 | + scenario_import.scenario.source_url.should == source_url | |
| 165 | + scenario_import.scenario.public.should be_false | |
| 166 | + end | |
| 167 | + | |
| 168 | + it "creates the Agents" do | |
| 169 | +        lambda { | |
| 170 | + scenario_import.import! | |
| 171 | +        }.should change { users(:bob).agents.count }.by(2) | |
| 172 | + | |
| 173 | + weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") | |
| 174 | + trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") | |
| 175 | + | |
| 176 | + weather_agent.name.should == "a weather agent" | |
| 177 | + weather_agent.schedule.should == "5pm" | |
| 178 | + weather_agent.keep_events_for.should == 14 | |
| 179 | + weather_agent.propagate_immediately.should be_false | |
| 180 | + weather_agent.should_not be_disabled | |
| 181 | + weather_agent.memory.should be_empty | |
| 182 | + weather_agent.options.should == weather_agent_options | |
| 183 | + | |
| 184 | + trigger_agent.name.should == "listen for weather" | |
| 185 | + trigger_agent.sources.should == [weather_agent] | |
| 186 | + trigger_agent.schedule.should be_nil | |
| 187 | + trigger_agent.keep_events_for.should == 0 | |
| 188 | + trigger_agent.propagate_immediately.should be_true | |
| 189 | + trigger_agent.should be_disabled | |
| 190 | + trigger_agent.memory.should be_empty | |
| 191 | + trigger_agent.options.should == trigger_agent_options | |
| 192 | + end | |
| 193 | + | |
| 194 | + 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 | |
| 195 | + agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" | |
| 196 | + | |
| 197 | +        lambda { | |
| 198 | + scenario_import.import! | |
| 199 | +        }.should change { users(:bob).agents.count }.by(2) | |
| 200 | + end | |
| 201 | + end | |
| 202 | + | |
| 203 | + context "when an a scenario already exists with the given guid" do | |
| 204 | +      let!(:existing_scenario) { | |
| 205 | + _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario") | |
| 206 | + _existing_scenerio.guid = guid | |
| 207 | + _existing_scenerio.save! | |
| 208 | + _existing_scenerio | |
| 209 | + } | |
| 210 | + | |
| 211 | + it "uses the existing scenario, updating it's data" do | |
| 212 | +        lambda { | |
| 213 | + scenario_import.import!(:skip_agents => true) | |
| 214 | + scenario_import.scenario.should == existing_scenario | |
| 215 | +        }.should_not change { users(:bob).scenarios.count } | |
| 216 | + | |
| 217 | + existing_scenario.reload | |
| 218 | + existing_scenario.guid.should == guid | |
| 219 | + existing_scenario.description.should == description | |
| 220 | + existing_scenario.name.should == name | |
| 221 | + existing_scenario.source_url.should == source_url | |
| 222 | + existing_scenario.public.should be_false | |
| 223 | + end | |
| 224 | + | |
| 225 | + it "updates any existing agents in the scenario, and makes new ones as needed" do | |
| 226 | + agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" | |
| 227 | + agents(:bob_weather_agent).scenarios << existing_scenario | |
| 228 | + | |
| 229 | +        lambda { | |
| 230 | + # Shouldn't matter how many times we do it! | |
| 231 | + scenario_import.import! | |
| 232 | + scenario_import.import! | |
| 233 | + scenario_import.import! | |
| 234 | +        }.should change { users(:bob).agents.count }.by(1) | |
| 235 | + | |
| 236 | + weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") | |
| 237 | + trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent") | |
| 238 | + | |
| 239 | + weather_agent.should == agents(:bob_weather_agent) | |
| 240 | + | |
| 241 | + weather_agent.name.should == "a weather agent" | |
| 242 | + weather_agent.schedule.should == "5pm" | |
| 243 | + weather_agent.keep_events_for.should == 14 | |
| 244 | + weather_agent.propagate_immediately.should be_false | |
| 245 | + weather_agent.should_not be_disabled | |
| 246 | + weather_agent.memory.should be_empty | |
| 247 | + weather_agent.options.should == weather_agent_options | |
| 248 | + | |
| 249 | + trigger_agent.name.should == "listen for weather" | |
| 250 | + trigger_agent.sources.should == [weather_agent] | |
| 251 | + trigger_agent.schedule.should be_nil | |
| 252 | + trigger_agent.keep_events_for.should == 0 | |
| 253 | + trigger_agent.propagate_immediately.should be_true | |
| 254 | + trigger_agent.should be_disabled | |
| 255 | + trigger_agent.memory.should be_empty | |
| 256 | + trigger_agent.options.should == trigger_agent_options | |
| 257 | + end | |
| 258 | + end | |
| 259 | + end | |
| 80 | 260 | end | 
| @@ -1,54 +1,42 @@ | ||
| 1 | 1 | require 'spec_helper' | 
| 2 | 2 |  | 
| 3 | 3 | describe Scenario do | 
| 4 | +  let(:new_instance) { users(:bob).scenarios.build(:name => "some scenario") } | |
| 5 | + | |
| 6 | + it_behaves_like HasGuid | |
| 7 | + | |
| 4 | 8 | describe "validations" do | 
| 5 | 9 | before do | 
| 6 | - @scenario = users(:bob).scenarios.new(:name => "some scenario") | |
| 7 | - @scenario.should be_valid | |
| 10 | + new_instance.should be_valid | |
| 8 | 11 | end | 
| 9 | 12 |  | 
| 10 | 13 | it "validates the presence of name" do | 
| 11 | - @scenario.name = '' | |
| 12 | - @scenario.should_not be_valid | |
| 14 | + new_instance.name = '' | |
| 15 | + new_instance.should_not be_valid | |
| 13 | 16 | end | 
| 14 | 17 |  | 
| 15 | 18 | it "validates the presence of user" do | 
| 16 | - @scenario.user = nil | |
| 17 | - @scenario.should_not be_valid | |
| 19 | + new_instance.user = nil | |
| 20 | + new_instance.should_not be_valid | |
| 18 | 21 | end | 
| 19 | 22 |  | 
| 20 | 23 | it "only allows Agents owned by user" do | 
| 21 | - @scenario.agent_ids = [agents(:bob_website_agent).id] | |
| 22 | - @scenario.should be_valid | |
| 24 | + new_instance.agent_ids = [agents(:bob_website_agent).id] | |
| 25 | + new_instance.should be_valid | |
| 23 | 26 |  | 
| 24 | - @scenario.agent_ids = [agents(:jane_website_agent).id] | |
| 25 | - @scenario.should_not be_valid | |
| 26 | - end | |
| 27 | - end | |
| 28 | - | |
| 29 | - describe "guid" do | |
| 30 | - it "gets created before_save, but only if it's not present" do | |
| 31 | - scenario = users(:bob).scenarios.new(:name => "some scenario") | |
| 32 | - scenario.guid.should be_nil | |
| 33 | - scenario.save! | |
| 34 | - scenario.guid.should_not be_nil | |
| 35 | - | |
| 36 | -      lambda { scenario.save! }.should_not change { scenario.reload.guid } | |
| 27 | + new_instance.agent_ids = [agents(:jane_website_agent).id] | |
| 28 | + new_instance.should_not be_valid | |
| 37 | 29 | end | 
| 38 | 30 | end | 
| 39 | 31 |  | 
| 40 | 32 | describe "counters" do | 
| 41 | - before do | |
| 42 | - @scenario = users(:bob).scenarios.new(:name => "some scenario") | |
| 43 | - end | |
| 44 | - | |
| 45 | 33 | it "maintains a counter cache on user" do | 
| 46 | 34 |        lambda { | 
| 47 | - @scenario.save! | |
| 35 | + new_instance.save! | |
| 48 | 36 |        }.should change { users(:bob).reload.scenario_count }.by(1) | 
| 49 | 37 |  | 
| 50 | 38 |        lambda { | 
| 51 | - @scenario.destroy | |
| 39 | + new_instance.destroy | |
| 52 | 40 |        }.should change { users(:bob).reload.scenario_count }.by(-1) | 
| 53 | 41 | end | 
| 54 | 42 | end | 
| @@ -0,0 +1,12 @@ | ||
| 1 | +require 'spec_helper' | |
| 2 | + | |
| 3 | +shared_examples_for HasGuid do | |
| 4 | + it "gets created before_save, but only if it's not present" do | |
| 5 | + instance = new_instance | |
| 6 | + instance.guid.should be_nil | |
| 7 | + instance.save! | |
| 8 | + instance.guid.should_not be_nil | |
| 9 | + | |
| 10 | +    lambda { instance.save! }.should_not change { instance.reload.guid } | |
| 11 | + end | |
| 12 | +end | 
| @@ -3,7 +3,7 @@ require 'spec_helper' | ||
| 3 | 3 | shared_examples_for WorkingHelpers do | 
| 4 | 4 | describe "recent_error_logs?" do | 
| 5 | 5 | it "returns true if last_error_log_at is near last_event_at" do | 
| 6 | - agent = Agent.new | |
| 6 | + agent = described_class.new | |
| 7 | 7 |  | 
| 8 | 8 | agent.last_error_log_at = 10.minutes.ago | 
| 9 | 9 | agent.last_event_at = 10.minutes.ago | 
| @@ -26,9 +26,10 @@ shared_examples_for WorkingHelpers do | ||
| 26 | 26 | agent.recent_error_logs?.should be_false | 
| 27 | 27 | end | 
| 28 | 28 | end | 
| 29 | + | |
| 29 | 30 | describe "received_event_without_error?" do | 
| 30 | 31 | before do | 
| 31 | - @agent = Agent.new | |
| 32 | + @agent = described_class.new | |
| 32 | 33 | end | 
| 33 | 34 |  | 
| 34 | 35 | it "should return false until the first event was received" do | 
| @@ -49,5 +50,4 @@ shared_examples_for WorkingHelpers do | ||
| 49 | 50 | @agent.received_event_without_error?.should == true | 
| 50 | 51 | end | 
| 51 | 52 | end | 
| 52 | - | |
| 53 | 53 | end |