scenario_import_spec.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. require 'spec_helper'
  2. describe ScenarioImport do
  3. let(:user) { users(:bob) }
  4. let(:guid) { "somescenarioguid" }
  5. let(:tag_fg_color) { "#ffffff" }
  6. let(:tag_bg_color) { "#000000" }
  7. let(:description) { "This is a cool Huginn Scenario that does something useful!" }
  8. let(:name) { "A useful Scenario" }
  9. let(:source_url) { "http://example.com/scenarios/2/export.json" }
  10. let(:weather_agent_options) {
  11. {
  12. 'api_key' => 'some-api-key',
  13. 'location' => '12345'
  14. }
  15. }
  16. let(:trigger_agent_options) {
  17. {
  18. 'expected_receive_period_in_days' => 2,
  19. 'rules' => [{
  20. 'type' => "regex",
  21. 'value' => "rain|storm",
  22. 'path' => "conditions",
  23. }],
  24. 'message' => "Looks like rain!"
  25. }
  26. }
  27. let(:valid_parsed_weather_agent_data) do
  28. {
  29. :type => "Agents::WeatherAgent",
  30. :name => "a weather agent",
  31. :schedule => "5pm",
  32. :keep_events_for => 14,
  33. :disabled => true,
  34. :guid => "a-weather-agent",
  35. :options => weather_agent_options
  36. }
  37. end
  38. let(:valid_parsed_trigger_agent_data) do
  39. {
  40. :type => "Agents::TriggerAgent",
  41. :name => "listen for weather",
  42. :keep_events_for => 0,
  43. :propagate_immediately => true,
  44. :disabled => false,
  45. :guid => "a-trigger-agent",
  46. :options => trigger_agent_options
  47. }
  48. end
  49. let(:valid_parsed_basecamp_agent_data) do
  50. {
  51. :type => "Agents::BasecampAgent",
  52. :name => "Basecamp test",
  53. :schedule => "every_2m",
  54. :keep_events_for => 0,
  55. :propagate_immediately => true,
  56. :disabled => false,
  57. :guid => "a-basecamp-agent",
  58. :options => {project_id: 12345}
  59. }
  60. end
  61. let(:valid_parsed_data) do
  62. {
  63. :name => name,
  64. :description => description,
  65. :guid => guid,
  66. :tag_fg_color => tag_fg_color,
  67. :tag_bg_color => tag_bg_color,
  68. :source_url => source_url,
  69. :exported_at => 2.days.ago.utc.iso8601,
  70. :agents => [
  71. valid_parsed_weather_agent_data,
  72. valid_parsed_trigger_agent_data
  73. ],
  74. :links => [
  75. { :source => 0, :receiver => 1 }
  76. ]
  77. }
  78. end
  79. let(:valid_data) { valid_parsed_data.to_json }
  80. let(:invalid_data) { { :name => "some scenario missing a guid" }.to_json }
  81. describe "initialization" do
  82. it "is initialized with an attributes hash" do
  83. ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com"
  84. end
  85. end
  86. describe "validations" do
  87. subject do
  88. _import = ScenarioImport.new
  89. _import.set_user(user)
  90. _import
  91. end
  92. it "is not valid when none of file, url, or data are present" do
  93. subject.should_not be_valid
  94. subject.should have(1).error_on(:base)
  95. subject.errors[:base].should include("Please provide either a Scenario JSON File or a Public Scenario URL.")
  96. end
  97. describe "data" do
  98. it "should be invalid with invalid data" do
  99. subject.data = invalid_data
  100. subject.should_not be_valid
  101. subject.should have(1).error_on(:base)
  102. subject.data = "foo"
  103. subject.should_not be_valid
  104. subject.should have(1).error_on(:base)
  105. # It also clears the data when invalid
  106. subject.data.should be_nil
  107. end
  108. it "should be valid with valid data" do
  109. subject.data = valid_data
  110. subject.should be_valid
  111. end
  112. end
  113. describe "url" do
  114. it "should be invalid with an unreasonable URL" do
  115. subject.url = "foo"
  116. subject.should_not be_valid
  117. subject.should have(1).error_on(:url)
  118. subject.errors[:url].should include("appears to be invalid")
  119. end
  120. it "should be invalid when the referenced url doesn't contain a scenario" do
  121. stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_data)
  122. subject.url = "http://example.com/scenarios/1/export.json"
  123. subject.should_not be_valid
  124. subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.")
  125. end
  126. it "should be valid when the url points to a valid scenario" do
  127. stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_data)
  128. subject.url = "http://example.com/scenarios/1/export.json"
  129. subject.should be_valid
  130. end
  131. end
  132. describe "file" do
  133. it "should be invalid when the uploaded file doesn't contain a scenario" do
  134. subject.file = StringIO.new("foo")
  135. subject.should_not be_valid
  136. subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.")
  137. subject.file = StringIO.new(invalid_data)
  138. subject.should_not be_valid
  139. subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.")
  140. end
  141. it "should be valid with a valid uploaded scenario" do
  142. subject.file = StringIO.new(valid_data)
  143. subject.should be_valid
  144. end
  145. end
  146. end
  147. describe "#dangerous?" do
  148. it "returns false on most Agents" do
  149. ScenarioImport.new(:data => valid_data).should_not be_dangerous
  150. end
  151. it "returns true if a ShellCommandAgent is present" do
  152. valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent"
  153. ScenarioImport.new(:data => valid_parsed_data.to_json).should be_dangerous
  154. end
  155. end
  156. describe "#import and #generate_diff" do
  157. let(:scenario_import) do
  158. _import = ScenarioImport.new(:data => valid_data)
  159. _import.set_user users(:bob)
  160. _import
  161. end
  162. context "when this scenario has never been seen before" do
  163. describe "#import" do
  164. it "makes a new scenario" do
  165. lambda {
  166. scenario_import.import(:skip_agents => true)
  167. }.should change { users(:bob).scenarios.count }.by(1)
  168. scenario_import.scenario.name.should == name
  169. scenario_import.scenario.description.should == description
  170. scenario_import.scenario.guid.should == guid
  171. scenario_import.scenario.tag_fg_color.should == tag_fg_color
  172. scenario_import.scenario.tag_bg_color.should == tag_bg_color
  173. scenario_import.scenario.source_url.should == source_url
  174. scenario_import.scenario.public.should be_falsey
  175. end
  176. it "creates the Agents" do
  177. lambda {
  178. scenario_import.import
  179. }.should change { users(:bob).agents.count }.by(2)
  180. weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent")
  181. trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent")
  182. weather_agent.name.should == "a weather agent"
  183. weather_agent.schedule.should == "5pm"
  184. weather_agent.keep_events_for.should == 14
  185. weather_agent.propagate_immediately.should be_falsey
  186. weather_agent.should be_disabled
  187. weather_agent.memory.should be_empty
  188. weather_agent.options.should == weather_agent_options
  189. trigger_agent.name.should == "listen for weather"
  190. trigger_agent.sources.should == [weather_agent]
  191. trigger_agent.schedule.should be_nil
  192. trigger_agent.keep_events_for.should == 0
  193. trigger_agent.propagate_immediately.should be_truthy
  194. trigger_agent.should_not be_disabled
  195. trigger_agent.memory.should be_empty
  196. trigger_agent.options.should == trigger_agent_options
  197. end
  198. 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
  199. agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
  200. lambda {
  201. scenario_import.import
  202. }.should change { users(:bob).agents.count }.by(2)
  203. end
  204. end
  205. describe "#generate_diff" do
  206. it "returns AgentDiff objects for the incoming Agents" do
  207. scenario_import.should be_valid
  208. agent_diffs = scenario_import.agent_diffs
  209. weather_agent_diff = agent_diffs[0]
  210. trigger_agent_diff = agent_diffs[1]
  211. valid_parsed_weather_agent_data.each do |key, value|
  212. if key == :type
  213. value = value.split("::").last
  214. end
  215. weather_agent_diff.should respond_to(key)
  216. field = weather_agent_diff.send(key)
  217. field.should be_a(ScenarioImport::AgentDiff::FieldDiff)
  218. field.incoming.should == value
  219. field.updated.should == value
  220. field.current.should be_nil
  221. end
  222. weather_agent_diff.should_not respond_to(:propagate_immediately)
  223. valid_parsed_trigger_agent_data.each do |key, value|
  224. if key == :type
  225. value = value.split("::").last
  226. end
  227. trigger_agent_diff.should respond_to(key)
  228. field = trigger_agent_diff.send(key)
  229. field.should be_a(ScenarioImport::AgentDiff::FieldDiff)
  230. field.incoming.should == value
  231. field.updated.should == value
  232. field.current.should be_nil
  233. end
  234. trigger_agent_diff.should_not respond_to(:schedule)
  235. end
  236. end
  237. end
  238. context "when an a scenario already exists with the given guid" do
  239. let!(:existing_scenario) do
  240. _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario", :description => "something")
  241. _existing_scenerio.guid = guid
  242. _existing_scenerio.save!
  243. agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
  244. agents(:bob_weather_agent).scenarios << _existing_scenerio
  245. _existing_scenerio
  246. end
  247. describe "#import" do
  248. it "uses the existing scenario, updating its data" do
  249. lambda {
  250. scenario_import.import(:skip_agents => true)
  251. scenario_import.scenario.should == existing_scenario
  252. }.should_not change { users(:bob).scenarios.count }
  253. existing_scenario.reload
  254. existing_scenario.guid.should == guid
  255. existing_scenario.tag_fg_color.should == tag_fg_color
  256. existing_scenario.tag_bg_color.should == tag_bg_color
  257. existing_scenario.description.should == description
  258. existing_scenario.name.should == name
  259. existing_scenario.source_url.should == source_url
  260. existing_scenario.public.should be_falsey
  261. end
  262. it "updates any existing agents in the scenario, and makes new ones as needed" do
  263. scenario_import.should be_valid
  264. lambda {
  265. scenario_import.import
  266. }.should change { users(:bob).agents.count }.by(1) # One, because the weather agent already existed.
  267. weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent")
  268. trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent")
  269. weather_agent.should == agents(:bob_weather_agent)
  270. weather_agent.name.should == "a weather agent"
  271. weather_agent.schedule.should == "5pm"
  272. weather_agent.keep_events_for.should == 14
  273. weather_agent.propagate_immediately.should be_falsey
  274. weather_agent.should be_disabled
  275. weather_agent.memory.should be_empty
  276. weather_agent.options.should == weather_agent_options
  277. trigger_agent.name.should == "listen for weather"
  278. trigger_agent.sources.should == [weather_agent]
  279. trigger_agent.schedule.should be_nil
  280. trigger_agent.keep_events_for.should == 0
  281. trigger_agent.propagate_immediately.should be_truthy
  282. trigger_agent.should_not be_disabled
  283. trigger_agent.memory.should be_empty
  284. trigger_agent.options.should == trigger_agent_options
  285. end
  286. it "honors updates coming from the UI" do
  287. scenario_import.merges = {
  288. "0" => {
  289. "name" => "updated name",
  290. "schedule" => "6pm",
  291. "keep_events_for" => "2",
  292. "disabled" => "false",
  293. "options" => weather_agent_options.merge("api_key" => "foo").to_json
  294. }
  295. }
  296. scenario_import.should be_valid
  297. scenario_import.import.should be_truthy
  298. weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent")
  299. weather_agent.name.should == "updated name"
  300. weather_agent.schedule.should == "6pm"
  301. weather_agent.keep_events_for.should == 2
  302. weather_agent.should_not be_disabled
  303. weather_agent.options.should == weather_agent_options.merge("api_key" => "foo")
  304. end
  305. it "adds errors when updated agents are invalid" do
  306. scenario_import.merges = {
  307. "0" => {
  308. "name" => "",
  309. "schedule" => "foo",
  310. "keep_events_for" => "2",
  311. "options" => weather_agent_options.merge("api_key" => "").to_json
  312. }
  313. }
  314. scenario_import.import.should be_falsey
  315. errors = scenario_import.errors.full_messages.to_sentence
  316. errors.should =~ /Name can't be blank/
  317. errors.should =~ /api_key is required/
  318. errors.should =~ /Schedule is not a valid schedule/
  319. end
  320. end
  321. describe "#generate_diff" do
  322. it "returns AgentDiff objects that include 'current' values from any agents that already exist" do
  323. agent_diffs = scenario_import.agent_diffs
  324. weather_agent_diff = agent_diffs[0]
  325. trigger_agent_diff = agent_diffs[1]
  326. # Already exists
  327. weather_agent_diff.agent.should == agents(:bob_weather_agent)
  328. valid_parsed_weather_agent_data.each do |key, value|
  329. next if key == :type
  330. weather_agent_diff.send(key).current.should == agents(:bob_weather_agent).send(key)
  331. end
  332. # Doesn't exist yet
  333. valid_parsed_trigger_agent_data.each do |key, value|
  334. trigger_agent_diff.send(key).current.should be_nil
  335. end
  336. end
  337. it "sets the 'updated' FieldDiff values based on any feedback from the user" do
  338. scenario_import.merges = {
  339. "0" => {
  340. "name" => "a new name",
  341. "schedule" => "6pm",
  342. "keep_events_for" => "2",
  343. "disabled" => "true",
  344. "options" => weather_agent_options.merge("api_key" => "foo").to_json
  345. },
  346. "1" => {
  347. "name" => "another new name"
  348. }
  349. }
  350. scenario_import.should be_valid
  351. agent_diffs = scenario_import.agent_diffs
  352. weather_agent_diff = agent_diffs[0]
  353. trigger_agent_diff = agent_diffs[1]
  354. weather_agent_diff.name.current.should == agents(:bob_weather_agent).name
  355. weather_agent_diff.name.incoming.should == valid_parsed_weather_agent_data[:name]
  356. weather_agent_diff.name.updated.should == "a new name"
  357. weather_agent_diff.schedule.updated.should == "6pm"
  358. weather_agent_diff.keep_events_for.updated.should == "2"
  359. weather_agent_diff.disabled.updated.should == "true"
  360. weather_agent_diff.options.updated.should == weather_agent_options.merge("api_key" => "foo")
  361. end
  362. it "adds errors on validation when updated options are unparsable" do
  363. scenario_import.merges = {
  364. "0" => {
  365. "options" => '{'
  366. }
  367. }
  368. scenario_import.should_not be_valid
  369. scenario_import.should have(1).error_on(:base)
  370. end
  371. end
  372. end
  373. context "agents which require a service" do
  374. let(:valid_parsed_services) do
  375. data = valid_parsed_data
  376. data[:agents] = [valid_parsed_basecamp_agent_data,
  377. valid_parsed_trigger_agent_data]
  378. data
  379. end
  380. let(:valid_parsed_services_data) { valid_parsed_services.to_json }
  381. let(:services_scenario_import) {
  382. _import = ScenarioImport.new(:data => valid_parsed_services_data)
  383. _import.set_user users(:bob)
  384. _import
  385. }
  386. describe "#generate_diff" do
  387. it "should check if the agent requires a service" do
  388. agent_diffs = services_scenario_import.agent_diffs
  389. basecamp_agent_diff = agent_diffs[0]
  390. basecamp_agent_diff.requires_service?.should == true
  391. end
  392. it "should add an error when no service is selected" do
  393. services_scenario_import.import.should == false
  394. services_scenario_import.errors[:base].length.should == 1
  395. end
  396. end
  397. describe "#import" do
  398. it "should import" do
  399. services_scenario_import.merges = {
  400. "0" => {
  401. "service_id" => "0",
  402. }
  403. }
  404. lambda {
  405. services_scenario_import.import.should == true
  406. }.should change { users(:bob).agents.count }.by(2)
  407. end
  408. end
  409. end
  410. end
  411. end