scenario_import_spec.rb 22 KB

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