java_script_agent_spec.rb 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. require 'rails_helper'
  2. describe Agents::JavaScriptAgent do
  3. before do
  4. @valid_params = {
  5. name: "somename",
  6. options: {
  7. code: "Agent.check = function() { this.createEvent({ 'message': 'hi' }); };",
  8. }
  9. }
  10. @agent = Agents::JavaScriptAgent.new(@valid_params)
  11. @agent.user = users(:jane)
  12. @agent.save!
  13. end
  14. describe "validations" do
  15. it "requires 'code'" do
  16. expect(@agent).to be_valid
  17. @agent.options['code'] = ''
  18. expect(@agent).not_to be_valid
  19. @agent.options.delete('code')
  20. expect(@agent).not_to be_valid
  21. end
  22. it "checks for a valid 'language', but allows nil" do
  23. expect(@agent).to be_valid
  24. @agent.options['language'] = ''
  25. expect(@agent).to be_valid
  26. @agent.options.delete('language')
  27. expect(@agent).to be_valid
  28. @agent.options['language'] = 'foo'
  29. expect(@agent).not_to be_valid
  30. %w[javascript JavaScript coffeescript CoffeeScript].each do |valid_language|
  31. @agent.options['language'] = valid_language
  32. expect(@agent).to be_valid
  33. end
  34. end
  35. it "accepts a credential, but it must exist" do
  36. expect(@agent).to be_valid
  37. @agent.options['code'] = 'credential:foo'
  38. expect(@agent).not_to be_valid
  39. users(:jane).user_credentials.create! credential_name: "foo", credential_value: "bar"
  40. expect(@agent.reload).to be_valid
  41. end
  42. end
  43. describe "#working?" do
  44. describe "when expected_update_period_in_days is set" do
  45. it "returns false when more than expected_update_period_in_days have passed since the last event creation" do
  46. @agent.options['expected_update_period_in_days'] = 1
  47. @agent.save!
  48. expect(@agent).not_to be_working
  49. @agent.check
  50. expect(@agent.reload).to be_working
  51. three_days_from_now = 3.days.from_now
  52. allow(Time).to receive(:now) { three_days_from_now }
  53. expect(@agent).not_to be_working
  54. end
  55. end
  56. describe "when expected_receive_period_in_days is set" do
  57. it "returns false when more than expected_receive_period_in_days have passed since the last event was received" do
  58. @agent.options['expected_receive_period_in_days'] = 1
  59. @agent.save!
  60. expect(@agent).not_to be_working
  61. Agents::JavaScriptAgent.async_receive @agent.id, [events(:bob_website_agent_event).id]
  62. expect(@agent.reload).to be_working
  63. two_days_from_now = 2.days.from_now
  64. allow(Time).to receive(:now) { two_days_from_now }
  65. expect(@agent.reload).not_to be_working
  66. end
  67. end
  68. end
  69. describe "executing code" do
  70. it "works by default" do
  71. @agent.options = @agent.default_options
  72. @agent.options['make_event'] = true
  73. @agent.save!
  74. expect {
  75. expect {
  76. @agent.receive([events(:bob_website_agent_event)])
  77. @agent.check
  78. }.not_to(change { AgentLog.count })
  79. }.to change { Event.count }.by(2)
  80. end
  81. describe "using credentials as code" do
  82. before do
  83. @agent.user.user_credentials.create credential_name: 'code-foo',
  84. credential_value: 'Agent.check = function() { this.log("ran it"); };'
  85. @agent.options['code'] = "credential:code-foo\n\n"
  86. @agent.save!
  87. end
  88. it "accepts credentials" do
  89. @agent.check
  90. expect(AgentLog.last.message).to eq("ran it")
  91. end
  92. it "logs an error when the credential goes away" do
  93. @agent.user.user_credentials.delete_all
  94. @agent.reload.check
  95. expect(AgentLog.last.message).to eq("Unable to find credential")
  96. end
  97. end
  98. describe "error handling" do
  99. it "should log an error when V8 has issues" do
  100. @agent.options['code'] = 'syntax error!'
  101. @agent.save!
  102. expect {
  103. expect {
  104. @agent.check
  105. }.not_to raise_error
  106. }.to change { AgentLog.count }.by(1)
  107. expect(AgentLog.last.message).to match(/Unexpected identifier/)
  108. expect(AgentLog.last.level).to eq(4)
  109. end
  110. it "should log an error when JavaScript throws" do
  111. @agent.options['code'] = 'Agent.check = function() { throw "oh no"; };'
  112. @agent.save!
  113. expect {
  114. expect {
  115. @agent.check
  116. }.not_to raise_error
  117. }.to change { AgentLog.count }.by(1)
  118. expect(AgentLog.last.message).to match(/oh no/)
  119. expect(AgentLog.last.level).to eq(4)
  120. end
  121. end
  122. describe "getMemory" do
  123. it "won't store NaNs" do
  124. @agent.options['code'] = 'Agent.check = function() { this.memory("foo", NaN); };'
  125. @agent.save!
  126. @agent.check
  127. expect(@agent.memory['foo']).to eq('NaN') # string
  128. @agent.save!
  129. expect { @agent.reload.memory }.not_to raise_error
  130. end
  131. it "it stores an Array" do
  132. @agent.options['code'] = 'Agent.check = function() {
  133. var arr = [1,2];
  134. this.memory("foo", arr);
  135. };'
  136. @agent.save!
  137. @agent.check
  138. expect(@agent.memory['foo']).to eq([1, 2])
  139. @agent.save!
  140. expect { @agent.reload.memory }.not_to raise_error
  141. end
  142. it "it stores a Hash" do
  143. @agent.options['code'] = 'Agent.check = function() {
  144. var obj = {};
  145. obj["one"] = 1;
  146. obj["two"] = [1,2];
  147. this.memory("foo", obj);
  148. };'
  149. @agent.save!
  150. @agent.check
  151. expect(@agent.memory['foo']).to eq({ "one" => 1, "two" => [1, 2] })
  152. @agent.save!
  153. expect { @agent.reload.memory }.not_to raise_error
  154. end
  155. it "it stores a nested Hash" do
  156. @agent.options['code'] = 'Agent.check = function() {
  157. var u = {};
  158. u["one"] = 1;
  159. u["two"] = 2;
  160. var obj = {};
  161. obj["three"] = 3;
  162. obj["four"] = u;
  163. this.memory("foo", obj);
  164. };'
  165. @agent.save!
  166. @agent.check
  167. expect(@agent.memory['foo']).to eq({ "three" => 3, "four" => { "one" => 1, "two" => 2 } })
  168. @agent.save!
  169. expect { @agent.reload.memory }.not_to raise_error
  170. end
  171. it "it stores null" do
  172. @agent.options['code'] = 'Agent.check = function() {
  173. this.memory("foo", "test");
  174. this.memory("foo", null);
  175. };'
  176. @agent.save!
  177. @agent.check
  178. expect(@agent.memory['foo']).to eq(nil)
  179. @agent.save!
  180. expect { @agent.reload.memory }.not_to raise_error
  181. end
  182. it "it stores false" do
  183. @agent.options['code'] = 'Agent.check = function() {
  184. this.memory("foo", "test");
  185. this.memory("foo", false);
  186. };'
  187. @agent.save!
  188. @agent.check
  189. expect(@agent.memory['foo']).to eq(false)
  190. @agent.save!
  191. expect { @agent.reload.memory }.not_to raise_error
  192. end
  193. end
  194. describe "setMemory" do
  195. it "stores an object" do
  196. @agent.options['code'] = 'Agent.check = function() {
  197. var u = {};
  198. u["one"] = 1;
  199. u["two"] = 2;
  200. this.setMemory(u);
  201. };'
  202. @agent.save!
  203. @agent.check
  204. expect(@agent.memory).to eq({ "one" => 1, "two" => 2 })
  205. @agent.save!
  206. expect { @agent.reload.memory }.not_to raise_error
  207. end
  208. end
  209. describe "deleteKey" do
  210. it "deletes a memory key" do
  211. @agent.memory = { foo: "baz" }
  212. @agent.options['code'] = 'Agent.check = function() {
  213. this.deleteKey("foo");
  214. };'
  215. @agent.save!
  216. @agent.check
  217. expect(@agent.memory['foo']).to be_nil
  218. expect { @agent.reload.memory }.not_to raise_error
  219. end
  220. it "returns the string value of the deleted key" do
  221. @agent.memory = { foo: "baz" }
  222. @agent.options['code'] = 'Agent.check = function() {
  223. this.createEvent({ message: this.deleteKey("foo")});
  224. };'
  225. @agent.save!
  226. @agent.check
  227. created_event = @agent.events.last
  228. expect(created_event.payload).to eq('message' => "baz")
  229. end
  230. it "returns the hash value of the deleted key" do
  231. @agent.memory = { foo: { baz: 'test' } }
  232. @agent.options['code'] = 'Agent.check = function() {
  233. this.createEvent({ message: this.deleteKey("foo")});
  234. };'
  235. @agent.save!
  236. @agent.check
  237. created_event = @agent.events.last
  238. expect(created_event.payload).to eq('message' => { 'baz' => 'test' })
  239. end
  240. end
  241. describe "creating events" do
  242. it "creates events with this.createEvent in the JavaScript environment" do
  243. @agent.options['code'] =
  244. 'Agent.check = function() { this.createEvent({ message: "This is an event!", stuff: { foo: 5 } }); };'
  245. @agent.save!
  246. expect {
  247. expect {
  248. @agent.check
  249. }.not_to(change { AgentLog.count })
  250. }.to change { Event.count }.by(1)
  251. created_event = @agent.events.last
  252. expect(created_event.payload).to eq({ 'message' => "This is an event!", 'stuff' => { 'foo' => 5 } })
  253. end
  254. end
  255. describe "logging" do
  256. it "can output AgentLogs with this.log and this.error in the JavaScript environment" do
  257. @agent.options['code'] = 'Agent.check = function() { this.log("woah"); this.error("WOAH!"); };'
  258. @agent.save!
  259. expect {
  260. expect {
  261. @agent.check
  262. }.not_to raise_error
  263. }.to change { AgentLog.count }.by(2)
  264. log1, log2 = AgentLog.last(2)
  265. expect(log1.message).to eq("woah")
  266. expect(log1.level).to eq(3)
  267. expect(log2.message).to eq("WOAH!")
  268. expect(log2.level).to eq(4)
  269. end
  270. end
  271. describe "escaping and unescaping HTML" do
  272. it "can escape and unescape html with this.escapeHtml and this.unescapeHtml in the javascript environment" do
  273. @agent.options['code'] =
  274. 'Agent.check = function() { this.createEvent({ escaped: this.escapeHtml(\'test \"escaping\" <characters>\'), unescaped: this.unescapeHtml(\'test &quot;unescaping&quot; &lt;characters&gt;\')}); };'
  275. @agent.save!
  276. expect {
  277. expect {
  278. @agent.check
  279. }.not_to(change { AgentLog.count })
  280. }.to change { Event.count }.by(1)
  281. created_event = @agent.events.last
  282. expect(created_event.payload).to eq({ 'escaped' => 'test &quot;escaping&quot; &lt;characters&gt;',
  283. 'unescaped' => 'test "unescaping" <characters>' })
  284. end
  285. end
  286. describe "getting incoming events" do
  287. it "can access incoming events in the JavaScript enviroment via this.incomingEvents" do
  288. event = Event.new
  289. event.agent = agents(:bob_rain_notifier_agent)
  290. event.payload = { data: "Something you should know about" }
  291. event.save!
  292. event.reload
  293. @agent.options['code'] = <<-JS
  294. Agent.receive = function() {
  295. var events = this.incomingEvents();
  296. for(var i = 0; i < events.length; i++) {
  297. this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload });
  298. }
  299. }
  300. JS
  301. @agent.save!
  302. expect {
  303. expect {
  304. @agent.receive([events(:bob_website_agent_event), event])
  305. }.not_to(change { AgentLog.count })
  306. }.to change { Event.count }.by(2)
  307. created_event = @agent.events.first
  308. expect(created_event.payload).to eq({ 'message' => "I got an event!",
  309. 'event_was' => { 'data' => "Something you should know about" } })
  310. end
  311. end
  312. describe "getting and setting memory, getting options" do
  313. it "can access options via this.options and work with memory via this.memory" do
  314. @agent.options['code'] = <<-JS
  315. Agent.check = function() {
  316. if (this.options('make_event')) {
  317. var callCount = this.memory('callCount') || 0;
  318. this.memory('callCount', callCount + 1);
  319. }
  320. };
  321. JS
  322. @agent.save!
  323. expect {
  324. expect {
  325. @agent.check
  326. expect(@agent.memory['callCount']).not_to be_present
  327. @agent.options['make_event'] = true
  328. @agent.check
  329. expect(@agent.memory['callCount']).to eq(1)
  330. @agent.check
  331. expect(@agent.memory['callCount']).to eq(2)
  332. @agent.memory['callCount'] = 20
  333. @agent.check
  334. expect(@agent.memory['callCount']).to eq(21)
  335. }.not_to(change { AgentLog.count })
  336. }.not_to(change { Event.count })
  337. end
  338. end
  339. describe "using CoffeeScript" do
  340. it "will accept a 'language' of 'CoffeeScript'" do
  341. @agent.options['code'] = 'Agent.check = -> this.log("hello from coffeescript")'
  342. @agent.options['language'] = 'CoffeeScript'
  343. @agent.save!
  344. expect {
  345. @agent.check
  346. }.not_to raise_error
  347. expect(AgentLog.last.message).to eq("hello from coffeescript")
  348. end
  349. end
  350. describe "user credentials" do
  351. it "can access an existing credential" do
  352. @agent.send(:set_credential, 'test', 'hello')
  353. @agent.options['code'] = 'Agent.check = function() { this.log(this.credential("test")); };'
  354. @agent.save!
  355. @agent.check
  356. expect(AgentLog.last.message).to eq("hello")
  357. end
  358. it "will create a new credential" do
  359. @agent.options['code'] = 'Agent.check = function() { this.credential("test","1234"); };'
  360. @agent.save!
  361. expect {
  362. @agent.check
  363. }.to change(UserCredential, :count).by(1)
  364. end
  365. it "updates an existing credential" do
  366. @agent.send(:set_credential, 'test', 1234)
  367. @agent.options['code'] = 'Agent.check = function() { this.credential("test","12345"); };'
  368. @agent.save!
  369. expect {
  370. @agent.check
  371. }.to change(UserCredential, :count).by(0)
  372. expect(@agent.user.user_credentials.last.credential_value).to eq('12345')
  373. end
  374. end
  375. end
  376. describe "KVS" do
  377. before do
  378. Agents::KeyValueStoreAgent.create!(
  379. name: "kvs1",
  380. options: {
  381. key: "{{ key }}",
  382. value: "{{ value }}",
  383. variable: "var1",
  384. },
  385. memory: {
  386. x: "Huginn",
  387. },
  388. user: users(:jane),
  389. control_targets: [@agent]
  390. )
  391. Agents::KeyValueStoreAgent.create!(
  392. name: "kvs2",
  393. options: {
  394. key: "{{ key }}",
  395. value: "{{ value }}",
  396. variable: "var2",
  397. },
  398. memory: {
  399. y: "Muninn",
  400. },
  401. user: users(:jane),
  402. control_targets: [@agent]
  403. )
  404. end
  405. it "can be accessed via Agent.kvs()" do
  406. @agent.options['code'] = <<~JS
  407. Agent.check = function() {
  408. this.createEvent({ message: `I got values from KVS: ${this.kvs.var1["x"]} and ${this.kvs.var2["y"]}.` });
  409. };
  410. JS
  411. @agent.save!
  412. expect {
  413. @agent.check
  414. }.to change { @agent.events.count }.by(1)
  415. created_event = @agent.events.last
  416. expect(created_event.payload).to eq("message" => "I got values from KVS: Huginn and Muninn.")
  417. end
  418. end
  419. end