scenario_import_spec.rb 20 KB

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