Nessuna descrizione http://j1x-huginn.herokuapp.com

scenario_import_spec.rb 15KB

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