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. expect(ScenarioImport.new(:url => "http://google.com").url).to eq("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. expect(subject).not_to be_valid
  94. expect(subject).to have(1).error_on(:base)
  95. expect(subject.errors[:base]).to 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. expect(subject).not_to be_valid
  101. expect(subject).to have(1).error_on(:base)
  102. subject.data = "foo"
  103. expect(subject).not_to be_valid
  104. expect(subject).to have(1).error_on(:base)
  105. # It also clears the data when invalid
  106. expect(subject.data).to be_nil
  107. end
  108. it "should be valid with valid data" do
  109. subject.data = valid_data
  110. expect(subject).to 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. expect(subject).not_to be_valid
  117. expect(subject).to have(1).error_on(:url)
  118. expect(subject.errors[:url]).to 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. expect(subject).not_to be_valid
  124. expect(subject.errors[:base]).to 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. expect(subject).to 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. expect(subject).not_to be_valid
  136. expect(subject.errors[:base]).to include("The provided data does not appear to be a valid Scenario.")
  137. subject.file = StringIO.new(invalid_data)
  138. expect(subject).not_to be_valid
  139. expect(subject.errors[:base]).to 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. expect(subject).to be_valid
  144. end
  145. end
  146. end
  147. describe "#dangerous?" do
  148. it "returns false on most Agents" do
  149. expect(ScenarioImport.new(:data => valid_data)).not_to be_dangerous
  150. end
  151. it "returns true if a ShellCommandAgent is present" do
  152. valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent"
  153. expect(ScenarioImport.new(:data => valid_parsed_data.to_json)).to 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. expect {
  166. scenario_import.import(:skip_agents => true)
  167. }.to change { users(:bob).scenarios.count }.by(1)
  168. expect(scenario_import.scenario.name).to eq(name)
  169. expect(scenario_import.scenario.description).to eq(description)
  170. expect(scenario_import.scenario.guid).to eq(guid)
  171. expect(scenario_import.scenario.tag_fg_color).to eq(tag_fg_color)
  172. expect(scenario_import.scenario.tag_bg_color).to eq(tag_bg_color)
  173. expect(scenario_import.scenario.source_url).to eq(source_url)
  174. expect(scenario_import.scenario.public).to be_falsey
  175. end
  176. it "creates the Agents" do
  177. expect {
  178. scenario_import.import
  179. }.to 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. expect(weather_agent.name).to eq("a weather agent")
  183. expect(weather_agent.schedule).to eq("5pm")
  184. expect(weather_agent.keep_events_for).to eq(14)
  185. expect(weather_agent.propagate_immediately).to be_falsey
  186. expect(weather_agent).to be_disabled
  187. expect(weather_agent.memory).to be_empty
  188. expect(weather_agent.options).to eq(weather_agent_options)
  189. expect(trigger_agent.name).to eq("listen for weather")
  190. expect(trigger_agent.sources).to eq([weather_agent])
  191. expect(trigger_agent.schedule).to be_nil
  192. expect(trigger_agent.keep_events_for).to eq(0)
  193. expect(trigger_agent.propagate_immediately).to be_truthy
  194. expect(trigger_agent).not_to be_disabled
  195. expect(trigger_agent.memory).to be_empty
  196. expect(trigger_agent.options).to eq(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. expect {
  201. scenario_import.import
  202. }.to 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. expect(scenario_import).to 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. expect(weather_agent_diff).to respond_to(key)
  216. field = weather_agent_diff.send(key)
  217. expect(field).to be_a(ScenarioImport::AgentDiff::FieldDiff)
  218. expect(field.incoming).to eq(value)
  219. expect(field.updated).to eq(value)
  220. expect(field.current).to be_nil
  221. end
  222. expect(weather_agent_diff).not_to 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. expect(trigger_agent_diff).to respond_to(key)
  228. field = trigger_agent_diff.send(key)
  229. expect(field).to be_a(ScenarioImport::AgentDiff::FieldDiff)
  230. expect(field.incoming).to eq(value)
  231. expect(field.updated).to eq(value)
  232. expect(field.current).to be_nil
  233. end
  234. expect(trigger_agent_diff).not_to 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. expect {
  250. scenario_import.import(:skip_agents => true)
  251. expect(scenario_import.scenario).to eq(existing_scenario)
  252. }.not_to change { users(:bob).scenarios.count }
  253. existing_scenario.reload
  254. expect(existing_scenario.guid).to eq(guid)
  255. expect(existing_scenario.tag_fg_color).to eq(tag_fg_color)
  256. expect(existing_scenario.tag_bg_color).to eq(tag_bg_color)
  257. expect(existing_scenario.description).to eq(description)
  258. expect(existing_scenario.name).to eq(name)
  259. expect(existing_scenario.source_url).to eq(source_url)
  260. expect(existing_scenario.public).to be_falsey
  261. end
  262. it "updates any existing agents in the scenario, and makes new ones as needed" do
  263. expect(scenario_import).to be_valid
  264. expect {
  265. scenario_import.import
  266. }.to 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. expect(weather_agent).to eq(agents(:bob_weather_agent))
  270. expect(weather_agent.name).to eq("a weather agent")
  271. expect(weather_agent.schedule).to eq("5pm")
  272. expect(weather_agent.keep_events_for).to eq(14)
  273. expect(weather_agent.propagate_immediately).to be_falsey
  274. expect(weather_agent).to be_disabled
  275. expect(weather_agent.memory).to be_empty
  276. expect(weather_agent.options).to eq(weather_agent_options)
  277. expect(trigger_agent.name).to eq("listen for weather")
  278. expect(trigger_agent.sources).to eq([weather_agent])
  279. expect(trigger_agent.schedule).to be_nil
  280. expect(trigger_agent.keep_events_for).to eq(0)
  281. expect(trigger_agent.propagate_immediately).to be_truthy
  282. expect(trigger_agent).not_to be_disabled
  283. expect(trigger_agent.memory).to be_empty
  284. expect(trigger_agent.options).to eq(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. expect(scenario_import).to be_valid
  297. expect(scenario_import.import).to be_truthy
  298. weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent")
  299. expect(weather_agent.name).to eq("updated name")
  300. expect(weather_agent.schedule).to eq("6pm")
  301. expect(weather_agent.keep_events_for).to eq(2)
  302. expect(weather_agent).not_to be_disabled
  303. expect(weather_agent.options).to eq(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. expect(scenario_import.import).to be_falsey
  315. errors = scenario_import.errors.full_messages.to_sentence
  316. expect(errors).to match(/Name can't be blank/)
  317. expect(errors).to match(/api_key is required/)
  318. expect(errors).to match(/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. expect(weather_agent_diff.agent).to eq(agents(:bob_weather_agent))
  328. valid_parsed_weather_agent_data.each do |key, value|
  329. next if key == :type
  330. expect(weather_agent_diff.send(key).current).to eq(agents(:bob_weather_agent).send(key))
  331. end
  332. # Doesn't exist yet
  333. valid_parsed_trigger_agent_data.each do |key, value|
  334. expect(trigger_agent_diff.send(key).current).to 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. expect(scenario_import).to be_valid
  351. agent_diffs = scenario_import.agent_diffs
  352. weather_agent_diff = agent_diffs[0]
  353. trigger_agent_diff = agent_diffs[1]
  354. expect(weather_agent_diff.name.current).to eq(agents(:bob_weather_agent).name)
  355. expect(weather_agent_diff.name.incoming).to eq(valid_parsed_weather_agent_data[:name])
  356. expect(weather_agent_diff.name.updated).to eq("a new name")
  357. expect(weather_agent_diff.schedule.updated).to eq("6pm")
  358. expect(weather_agent_diff.keep_events_for.updated).to eq("2")
  359. expect(weather_agent_diff.disabled.updated).to eq("true")
  360. expect(weather_agent_diff.options.updated).to eq(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. expect(scenario_import).not_to be_valid
  369. expect(scenario_import).to 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. expect(basecamp_agent_diff.requires_service?).to eq(true)
  391. end
  392. it "should add an error when no service is selected" do
  393. expect(services_scenario_import.import).to eq(false)
  394. expect(services_scenario_import.errors[:base].length).to eq(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. expect {
  405. expect(services_scenario_import.import).to eq(true)
  406. }.to change { users(:bob).agents.count }.by(2)
  407. end
  408. end
  409. end
  410. end
  411. end