scenario_import_spec.rb 19 KB

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