@@ -24,6 +24,7 @@ gem 'coffee-rails', '~> 3.2.1' |
||
24 | 24 |
gem 'uglifier', '>= 1.0.3' |
25 | 25 |
gem 'select2-rails' |
26 | 26 |
gem 'jquery-rails' |
27 |
+gem 'ace-rails-ap' |
|
27 | 28 |
|
28 | 29 |
gem 'geokit-rails3' |
29 | 30 |
gem 'kramdown' |
@@ -37,6 +38,8 @@ gem 'twitter-stream', '>=0.1.16' |
||
37 | 38 |
gem 'em-http-request' |
38 | 39 |
gem 'weibo_2' |
39 | 40 |
|
41 |
+gem 'therubyracer' |
|
42 |
+ |
|
40 | 43 |
platforms :ruby_18 do |
41 | 44 |
gem 'system_timer' |
42 | 45 |
gem 'fastercsv' |
@@ -1,6 +1,7 @@ |
||
1 | 1 |
GEM |
2 | 2 |
remote: https://rubygems.org/ |
3 | 3 |
specs: |
4 |
+ ace-rails-ap (2.0.1) |
|
4 | 5 |
actionmailer (3.2.13) |
5 | 6 |
actionpack (= 3.2.13) |
6 | 7 |
mail (~> 2.5.3) |
@@ -124,6 +125,7 @@ GEM |
||
124 | 125 |
actionpack (>= 3.0.0) |
125 | 126 |
activesupport (>= 3.0.0) |
126 | 127 |
kramdown (1.1.0) |
128 |
+ libv8 (3.16.14.3) |
|
127 | 129 |
mail (2.5.4) |
128 | 130 |
mime-types (~> 1.16) |
129 | 131 |
treetop (~> 1.4.8) |
@@ -174,6 +176,7 @@ GEM |
||
174 | 176 |
rake (10.1.0) |
175 | 177 |
rdoc (3.12.2) |
176 | 178 |
json (~> 1.4) |
179 |
+ ref (1.0.5) |
|
177 | 180 |
rest-client (1.6.7) |
178 | 181 |
mime-types (>= 1.16) |
179 | 182 |
rr (1.1.2) |
@@ -224,6 +227,9 @@ GEM |
||
224 | 227 |
system_timer (1.2.4) |
225 | 228 |
term-ansicolor (1.2.2) |
226 | 229 |
tins (~> 0.8) |
230 |
+ therubyracer (0.12.0) |
|
231 |
+ libv8 (~> 3.16.14.0) |
|
232 |
+ ref |
|
227 | 233 |
thor (0.18.1) |
228 | 234 |
tilt (1.4.1) |
229 | 235 |
tins (0.13.1) |
@@ -267,6 +273,7 @@ PLATFORMS |
||
267 | 273 |
ruby |
268 | 274 |
|
269 | 275 |
DEPENDENCIES |
276 |
+ ace-rails-ap |
|
270 | 277 |
better_errors |
271 | 278 |
binding_of_caller |
272 | 279 |
bootstrap-kaminari-views |
@@ -300,6 +307,7 @@ DEPENDENCIES |
||
300 | 307 |
select2-rails |
301 | 308 |
shoulda-matchers |
302 | 309 |
system_timer |
310 |
+ therubyracer |
|
303 | 311 |
twilio-ruby |
304 | 312 |
|
305 | 313 |
twitter-stream (>= 0.1.16) |
@@ -0,0 +1,27 @@ |
||
1 |
+#= require ace/ace |
|
2 |
+#= require ace/mode-javascript.js |
|
3 |
+#= require ace/mode-markdown.js |
|
4 |
+#= require_self |
|
5 |
+ |
|
6 |
+$ -> |
|
7 |
+ editor = ace.edit("ace-credential-value") |
|
8 |
+ editor.getSession().setTabSize(2) |
|
9 |
+ editor.getSession().setUseSoftTabs(true) |
|
10 |
+ editor.getSession().setUseWrapMode(false) |
|
11 |
+ editor.setTheme("ace/theme/chrome") |
|
12 |
+ |
|
13 |
+ setMode = -> |
|
14 |
+ mode = $("#user_credential_mode").val() |
|
15 |
+ if mode == 'java_script' |
|
16 |
+ editor.getSession().setMode("ace/mode/javascript") |
|
17 |
+ else |
|
18 |
+ editor.getSession().setMode("ace/mode/text") |
|
19 |
+ |
|
20 |
+ setMode() |
|
21 |
+ $("#user_credential_mode").on 'change', setMode |
|
22 |
+ |
|
23 |
+ $textarea = $('#user_credential_credential_value').hide() |
|
24 |
+ editor.getSession().setValue($textarea.val()) |
|
25 |
+ |
|
26 |
+ $textarea.closest('form').on 'submit', -> |
|
27 |
+ $textarea.val(editor.getSession().getValue()) |
@@ -126,3 +126,11 @@ span.not-applicable:after { |
||
126 | 126 |
#show-tabs li a.recent-errors { |
127 | 127 |
font-weight: bold; |
128 | 128 |
} |
129 |
+ |
|
130 |
+// Credentials |
|
131 |
+ |
|
132 |
+#ace-credential-value { |
|
133 |
+ position: relative; |
|
134 |
+ width: 940px; |
|
135 |
+ height: 400px; |
|
136 |
+} |
@@ -16,7 +16,7 @@ class Agent < ActiveRecord::Base |
||
16 | 16 |
load_types_in "Agents" |
17 | 17 |
|
18 | 18 |
SCHEDULES = %w[every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d |
19 |
- midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm] |
|
19 |
+ midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm never] |
|
20 | 20 |
|
21 | 21 |
EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })] |
22 | 22 |
|
@@ -296,6 +296,7 @@ class Agent < ActiveRecord::Base |
||
296 | 296 |
# Given a schedule name, run `check` via `bulk_check` on all Agents with that schedule. |
297 | 297 |
# This is called by bin/schedule.rb for each schedule in `SCHEDULES`. |
298 | 298 |
def run_schedule(schedule) |
299 |
+ return if schedule == 'never' |
|
299 | 300 |
types = where(:schedule => schedule).group(:type).pluck(:type) |
300 | 301 |
types.each do |type| |
301 | 302 |
type.constantize.bulk_check(schedule) |
@@ -0,0 +1,186 @@ |
||
1 |
+require 'date' |
|
2 |
+require 'cgi' |
|
3 |
+ |
|
4 |
+module Agents |
|
5 |
+ class JavaScriptAgent < Agent |
|
6 |
+ default_schedule "never" |
|
7 |
+ |
|
8 |
+ description <<-MD |
|
9 |
+ This Agent allows you to write code in JavaScript that can create and receive events. If other Agents aren't meeting your needs, try this one! |
|
10 |
+ |
|
11 |
+ You can put code in the `code` option, or put your code in a Credential and reference it from `code` with `credential:<name>` (recommended). |
|
12 |
+ |
|
13 |
+ You can implement `Agent.check` and `Agent.receive` as you see fit. The following methods will be available on Agent in the JavaScript environment: |
|
14 |
+ |
|
15 |
+ * `this.createEvent(payload)` |
|
16 |
+ * `this.incomingEvents()` |
|
17 |
+ * `this.memory()` |
|
18 |
+ * `this.memory(key)` |
|
19 |
+ * `this.memory(keyToSet, valueToSet)` |
|
20 |
+ * `this.options()` |
|
21 |
+ * `this.options(key)` |
|
22 |
+ * `this.log(message)` |
|
23 |
+ * `this.error(message)` |
|
24 |
+ MD |
|
25 |
+ |
|
26 |
+ def validate_options |
|
27 |
+ cred_name = credential_referenced_by_code |
|
28 |
+ if cred_name |
|
29 |
+ errors.add(:base, "The credential '#{cred_name}' referenced by code cannot be found") unless credential(cred_name).present? |
|
30 |
+ else |
|
31 |
+ errors.add(:base, "The 'code' option is required") unless options['code'].present? |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ def working? |
|
36 |
+ return false if recent_error_logs? |
|
37 |
+ |
|
38 |
+ if options['expected_update_period_in_days'].present? |
|
39 |
+ return false unless event_created_within?(options['expected_update_period_in_days']) |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ if options['expected_receive_period_in_days'].present? |
|
43 |
+ return false unless last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ true |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ def check |
|
50 |
+ log_errors do |
|
51 |
+ execute_js("check") |
|
52 |
+ end |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ def receive(incoming_events) |
|
56 |
+ log_errors do |
|
57 |
+ execute_js("receive", incoming_events) |
|
58 |
+ end |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ def default_options |
|
62 |
+ js_code = <<-JS |
|
63 |
+ Agent.check = function() { |
|
64 |
+ if (this.options('make_event')) { |
|
65 |
+ this.createEvent({ 'message': 'I made an event!' }); |
|
66 |
+ var callCount = this.memory('callCount') || 0; |
|
67 |
+ this.memory('callCount', callCount + 1); |
|
68 |
+ } |
|
69 |
+ }; |
|
70 |
+ |
|
71 |
+ Agent.receive = function() { |
|
72 |
+ var events = this.incomingEvents(); |
|
73 |
+ for(var i = 0; i < events.length; i++) { |
|
74 |
+ this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload }); |
|
75 |
+ } |
|
76 |
+ } |
|
77 |
+ JS |
|
78 |
+ |
|
79 |
+ { |
|
80 |
+ "code" => js_code.gsub(/[\n\r\t]/, '').strip, |
|
81 |
+ 'expected_receive_period_in_days' => "2", |
|
82 |
+ 'expected_update_period_in_days' => "2" |
|
83 |
+ } |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ private |
|
87 |
+ |
|
88 |
+ def execute_js(js_function, incoming_events = []) |
|
89 |
+ js_function = js_function == "check" ? "check" : "receive" |
|
90 |
+ context = V8::Context.new |
|
91 |
+ context.eval(setup_javascript) |
|
92 |
+ |
|
93 |
+ context["doCreateEvent"] = lambda { |a, y| create_event(payload: clean_nans(JSON.parse(y))).payload.to_json } |
|
94 |
+ context["getIncomingEvents"] = lambda { |a| incoming_events.to_json } |
|
95 |
+ context["getOptions"] = lambda { |a, x| options.to_json } |
|
96 |
+ context["doLog"] = lambda { |a, x| log x } |
|
97 |
+ context["doError"] = lambda { |a, x| error x } |
|
98 |
+ context["getMemory"] = lambda do |a, x, y| |
|
99 |
+ if x && y |
|
100 |
+ memory[x] = clean_nans(y) |
|
101 |
+ else |
|
102 |
+ memory.to_json |
|
103 |
+ end |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ context.eval(code) |
|
107 |
+ context.eval("Agent.#{js_function}();") |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ def code |
|
111 |
+ cred = credential_referenced_by_code |
|
112 |
+ if cred |
|
113 |
+ credential(cred) || 'Agent.check = function() { this.error("Unable to find credential"); };' |
|
114 |
+ else |
|
115 |
+ options['code'] |
|
116 |
+ end |
|
117 |
+ end |
|
118 |
+ |
|
119 |
+ def credential_referenced_by_code |
|
120 |
+ options['code'] =~ /\Acredential:(.*)\Z/ && $1 |
|
121 |
+ end |
|
122 |
+ |
|
123 |
+ def setup_javascript |
|
124 |
+ <<-JS |
|
125 |
+ function Agent() {}; |
|
126 |
+ |
|
127 |
+ Agent.createEvent = function(opts) { |
|
128 |
+ return JSON.parse(doCreateEvent(JSON.stringify(opts))); |
|
129 |
+ } |
|
130 |
+ |
|
131 |
+ Agent.incomingEvents = function() { |
|
132 |
+ return JSON.parse(getIncomingEvents()); |
|
133 |
+ } |
|
134 |
+ |
|
135 |
+ Agent.memory = function(key, value) { |
|
136 |
+ if (typeof(key) !== "undefined" && typeof(value) !== "undefined") { |
|
137 |
+ getMemory(key, value); |
|
138 |
+ } else if (typeof(key) !== "undefined") { |
|
139 |
+ return JSON.parse(getMemory())[key]; |
|
140 |
+ } else { |
|
141 |
+ return JSON.parse(getMemory()); |
|
142 |
+ } |
|
143 |
+ } |
|
144 |
+ |
|
145 |
+ Agent.options = function(key) { |
|
146 |
+ if (typeof(key) !== "undefined") { |
|
147 |
+ return JSON.parse(getOptions())[key]; |
|
148 |
+ } else { |
|
149 |
+ return JSON.parse(getOptions()); |
|
150 |
+ } |
|
151 |
+ } |
|
152 |
+ |
|
153 |
+ Agent.log = function(message) { |
|
154 |
+ doLog(message); |
|
155 |
+ } |
|
156 |
+ |
|
157 |
+ Agent.error = function(message) { |
|
158 |
+ doError(message); |
|
159 |
+ } |
|
160 |
+ |
|
161 |
+ Agent.check = function(){}; |
|
162 |
+ Agent.receive = function(){}; |
|
163 |
+ JS |
|
164 |
+ end |
|
165 |
+ |
|
166 |
+ def log_errors |
|
167 |
+ begin |
|
168 |
+ yield |
|
169 |
+ rescue V8::Error => e |
|
170 |
+ error "JavaScript error: #{e.message}" |
|
171 |
+ end |
|
172 |
+ end |
|
173 |
+ |
|
174 |
+ def clean_nans(input) |
|
175 |
+ if input.is_a?(Array) |
|
176 |
+ input.map {|v| clean_nans(v) } |
|
177 |
+ elsif input.is_a?(Hash) |
|
178 |
+ input.inject({}) { |m, (k, v)| m[k] = clean_nans(v); m } |
|
179 |
+ elsif input.is_a?(Float) && input.nan? |
|
180 |
+ 'NaN' |
|
181 |
+ else |
|
182 |
+ input |
|
183 |
+ end |
|
184 |
+ end |
|
185 |
+ end |
|
186 |
+end |
@@ -1,13 +1,17 @@ |
||
1 | 1 |
class UserCredential < ActiveRecord::Base |
2 |
- attr_accessible :credential_name, :credential_value |
|
2 |
+ MODES = %w[text java_script] |
|
3 |
+ |
|
4 |
+ attr_accessible :credential_name, :credential_value, :mode |
|
3 | 5 |
|
4 | 6 |
belongs_to :user |
5 | 7 |
|
6 | 8 |
validates_presence_of :credential_name |
7 | 9 |
validates_presence_of :credential_value |
10 |
+ validates_inclusion_of :mode, :in => MODES |
|
8 | 11 |
validates_presence_of :user_id |
9 | 12 |
validates_uniqueness_of :credential_name, :scope => :user_id |
10 | 13 |
|
14 |
+ before_validation :default_mode_to_text |
|
11 | 15 |
before_save :trim_fields |
12 | 16 |
|
13 | 17 |
protected |
@@ -16,4 +20,8 @@ class UserCredential < ActiveRecord::Base |
||
16 | 20 |
credential_name.strip! |
17 | 21 |
credential_value.strip! |
18 | 22 |
end |
23 |
+ |
|
24 |
+ def default_mode_to_text |
|
25 |
+ self.mode = 'text' unless mode.present? |
|
26 |
+ end |
|
19 | 27 |
end |
@@ -44,7 +44,6 @@ |
||
44 | 44 |
</div> |
45 | 45 |
</div> |
46 | 46 |
|
47 |
- |
|
48 | 47 |
<div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
49 | 48 |
<div class="control-group"> |
50 | 49 |
<%= f.label :keep_events_for, "Keep events", :class => 'control-label' %> |
@@ -18,9 +18,17 @@ |
||
18 | 18 |
</div> |
19 | 19 |
|
20 | 20 |
<div class="control-group"> |
21 |
+ <%= f.label :mode, :class => 'control-label' %> |
|
22 |
+ <div class="controls"> |
|
23 |
+ <%= f.select :mode, options_for_select(UserCredential::MODES.map {|s| [s.classify, s] }, @user_credential.mode), {}, :class => 'span4' %> |
|
24 |
+ </div> |
|
25 |
+ </div> |
|
26 |
+ |
|
27 |
+ <div class="control-group"> |
|
21 | 28 |
<%= f.label :credential_value, :class => 'control-label' %> |
22 | 29 |
<div class="controls"> |
23 | 30 |
<%= f.text_area :credential_value, :class => 'span8', :rows => 10 %> |
31 |
+ <div id="ace-credential-value"></div> |
|
24 | 32 |
</div> |
25 | 33 |
</div> |
26 | 34 |
|
@@ -28,3 +36,5 @@ |
||
28 | 36 |
<%= f.submit "Save Credential", :class => "btn btn-primary" %> |
29 | 37 |
</div> |
30 | 38 |
<% end %> |
39 |
+ |
|
40 |
+<%= javascript_include_tag "user_credentials" %> |
@@ -48,7 +48,7 @@ Huginn::Application.configure do |
||
48 | 48 |
end |
49 | 49 |
|
50 | 50 |
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
51 |
- config.assets.precompile += %w( graphing.js ) |
|
51 |
+ config.assets.precompile += %w( graphing.js user_credentials.js ) |
|
52 | 52 |
|
53 | 53 |
# Enable threaded mode |
54 | 54 |
# config.threadsafe! |
@@ -0,0 +1,5 @@ |
||
1 |
+class AddModeToUserCredentials < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :user_credentials, :mode, :string, :default => 'text', :null => false |
|
4 |
+ end |
|
5 |
+end |
@@ -11,7 +11,7 @@ |
||
11 | 11 |
# |
12 | 12 |
# It's strongly recommended to check this file into your version control system. |
13 | 13 |
|
14 |
-ActiveRecord::Schema.define(:version => 20140127164931) do |
|
14 |
+ActiveRecord::Schema.define(:version => 20140210062747) do |
|
15 | 15 |
|
16 | 16 |
create_table "agent_logs", :force => true do |t| |
17 | 17 |
t.integer "agent_id", :null => false |
@@ -96,11 +96,12 @@ ActiveRecord::Schema.define(:version => 20140127164931) do |
||
96 | 96 |
add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id" |
97 | 97 |
|
98 | 98 |
create_table "user_credentials", :force => true do |t| |
99 |
- t.integer "user_id", :null => false |
|
100 |
- t.string "credential_name", :null => false |
|
101 |
- t.text "credential_value", :null => false |
|
102 |
- t.datetime "created_at", :null => false |
|
103 |
- t.datetime "updated_at", :null => false |
|
99 |
+ t.integer "user_id", :null => false |
|
100 |
+ t.string "credential_name", :null => false |
|
101 |
+ t.text "credential_value", :null => false |
|
102 |
+ t.datetime "created_at", :null => false |
|
103 |
+ t.datetime "updated_at", :null => false |
|
104 |
+ t.string "mode", :default => "text", :null => false |
|
104 | 105 |
end |
105 | 106 |
|
106 | 107 |
add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true |
@@ -1,9 +1,9 @@ |
||
1 | 1 |
bob_website_agent_event: |
2 | 2 |
user: bob |
3 | 3 |
agent: bob_website_agent |
4 |
- payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_json.inspect %> |
|
4 |
+ payload: <%= { :title => "foo", :url => "http://foo.com" }.to_json.inspect %> |
|
5 | 5 |
|
6 | 6 |
jane_website_agent_event: |
7 | 7 |
user: jane |
8 | 8 |
agent: jane_website_agent |
9 |
- payload: <%= [{ :title => "foo", :url => "http://foo.com" }].to_json.inspect %> |
|
9 |
+ payload: <%= { :title => "foo", :url => "http://foo.com" }.to_json.inspect %> |
@@ -2,15 +2,19 @@ bob_aws_key: |
||
2 | 2 |
user: bob |
3 | 3 |
credential_name: aws_key |
4 | 4 |
credential_value: 2222222222-bob |
5 |
+ mode: text |
|
5 | 6 |
bob_aws_secret: |
6 | 7 |
user: bob |
7 | 8 |
credential_name: aws_secret |
8 | 9 |
credential_value: 1111111111-bob |
10 |
+ mode: text |
|
9 | 11 |
jane_aws_key: |
10 | 12 |
user: jane |
11 | 13 |
credential_name: aws_key |
12 | 14 |
credential_value: 2222222222-jane |
15 |
+ mode: text |
|
13 | 16 |
jane_aws_secret: |
14 | 17 |
user: jane |
15 | 18 |
credential_name: aws_secret |
16 | 19 |
credential_value: 1111111111-jabe |
20 |
+ mode: text |
@@ -25,6 +25,12 @@ describe Agent do |
||
25 | 25 |
do_not_allow(Agents::WebsiteAgent).async_check |
26 | 26 |
Agent.run_schedule("blah") |
27 | 27 |
end |
28 |
+ |
|
29 |
+ it "will not run the 'never' schedule" do |
|
30 |
+ agents(:bob_weather_agent).update_attribute 'schedule', 'never' |
|
31 |
+ do_not_allow(Agents::WebsiteAgent).async_check |
|
32 |
+ Agent.run_schedule("never") |
|
33 |
+ end |
|
28 | 34 |
end |
29 | 35 |
|
30 | 36 |
describe "credential" do |
@@ -0,0 +1,228 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::JavaScriptAgent do |
|
4 |
+ before do |
|
5 |
+ @valid_params = { |
|
6 |
+ :name => "somename", |
|
7 |
+ :options => { |
|
8 |
+ :code => "Agent.check = function() { this.createEvent({ 'message': 'hi' }); };", |
|
9 |
+ } |
|
10 |
+ } |
|
11 |
+ |
|
12 |
+ @agent = Agents::JavaScriptAgent.new(@valid_params) |
|
13 |
+ @agent.user = users(:jane) |
|
14 |
+ @agent.save! |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ describe "validations" do |
|
18 |
+ it "requires 'code'" do |
|
19 |
+ @agent.should be_valid |
|
20 |
+ @agent.options['code'] = '' |
|
21 |
+ @agent.should_not be_valid |
|
22 |
+ @agent.options.delete('code') |
|
23 |
+ @agent.should_not be_valid |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ it "accepts a credential, but it must exist" do |
|
27 |
+ @agent.should be_valid |
|
28 |
+ @agent.options['code'] = 'credential:foo' |
|
29 |
+ @agent.should_not be_valid |
|
30 |
+ users(:jane).user_credentials.create! :credential_name => "foo", :credential_value => "bar" |
|
31 |
+ @agent.reload.should be_valid |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ describe "#working?" do |
|
36 |
+ describe "when expected_update_period_in_days is set" do |
|
37 |
+ it "returns false when more than expected_update_period_in_days have passed since the last event creation" do |
|
38 |
+ @agent.options['expected_update_period_in_days'] = 1 |
|
39 |
+ @agent.save! |
|
40 |
+ @agent.should_not be_working |
|
41 |
+ @agent.check |
|
42 |
+ @agent.reload.should be_working |
|
43 |
+ three_days_from_now = 3.days.from_now |
|
44 |
+ stub(Time).now { three_days_from_now } |
|
45 |
+ @agent.should_not be_working |
|
46 |
+ end |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ describe "when expected_receive_period_in_days is set" do |
|
50 |
+ it "returns false when more than expected_receive_period_in_days have passed since the last event was received" do |
|
51 |
+ @agent.options['expected_receive_period_in_days'] = 1 |
|
52 |
+ @agent.save! |
|
53 |
+ @agent.should_not be_working |
|
54 |
+ Agents::JavaScriptAgent.async_receive @agent.id, [events(:bob_website_agent_event).id] |
|
55 |
+ @agent.reload.should be_working |
|
56 |
+ two_days_from_now = 2.days.from_now |
|
57 |
+ stub(Time).now { two_days_from_now } |
|
58 |
+ @agent.reload.should_not be_working |
|
59 |
+ end |
|
60 |
+ end |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ describe "executing code" do |
|
64 |
+ it "works by default" do |
|
65 |
+ @agent.options = @agent.default_options |
|
66 |
+ @agent.options['make_event'] = true |
|
67 |
+ @agent.save! |
|
68 |
+ |
|
69 |
+ lambda { |
|
70 |
+ lambda { |
|
71 |
+ @agent.receive([events(:bob_website_agent_event)]) |
|
72 |
+ @agent.check |
|
73 |
+ }.should_not change { AgentLog.count } |
|
74 |
+ }.should change { Event.count }.by(2) |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ |
|
78 |
+ describe "using credentials as code" do |
|
79 |
+ before do |
|
80 |
+ @agent.user.user_credentials.create :credential_name => 'code-foo', :credential_value => 'Agent.check = function() { this.log("ran it"); };' |
|
81 |
+ @agent.options['code'] = 'credential:code-foo' |
|
82 |
+ @agent.save! |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ it "accepts credentials" do |
|
86 |
+ @agent.check |
|
87 |
+ AgentLog.last.message.should == "ran it" |
|
88 |
+ end |
|
89 |
+ |
|
90 |
+ it "logs an error when the credential goes away" do |
|
91 |
+ @agent.user.user_credentials.delete_all |
|
92 |
+ @agent.reload.check |
|
93 |
+ AgentLog.last.message.should == "Unable to find credential" |
|
94 |
+ end |
|
95 |
+ end |
|
96 |
+ |
|
97 |
+ describe "error handling" do |
|
98 |
+ it "should log an error when V8 has issues" do |
|
99 |
+ @agent.options['code'] = 'syntax error!' |
|
100 |
+ @agent.save! |
|
101 |
+ lambda { |
|
102 |
+ lambda { |
|
103 |
+ @agent.check |
|
104 |
+ }.should_not raise_error |
|
105 |
+ }.should change { AgentLog.count }.by(1) |
|
106 |
+ AgentLog.last.message.should =~ /Unexpected identifier/ |
|
107 |
+ AgentLog.last.level.should == 4 |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ it "should log an error when JavaScript throws" do |
|
111 |
+ @agent.options['code'] = 'Agent.check = function() { throw "oh no"; };' |
|
112 |
+ @agent.save! |
|
113 |
+ lambda { |
|
114 |
+ lambda { |
|
115 |
+ @agent.check |
|
116 |
+ }.should_not raise_error |
|
117 |
+ }.should change { AgentLog.count }.by(1) |
|
118 |
+ AgentLog.last.message.should =~ /oh no/ |
|
119 |
+ AgentLog.last.level.should == 4 |
|
120 |
+ end |
|
121 |
+ |
|
122 |
+ it "won't store NaNs" do |
|
123 |
+ @agent.options['code'] = 'Agent.check = function() { this.memory("foo", NaN); };' |
|
124 |
+ @agent.save! |
|
125 |
+ @agent.check |
|
126 |
+ @agent.memory['foo'].should == 'NaN' # string |
|
127 |
+ @agent.save! |
|
128 |
+ lambda { @agent.reload.memory }.should_not raise_error |
|
129 |
+ end |
|
130 |
+ end |
|
131 |
+ |
|
132 |
+ describe "creating events" do |
|
133 |
+ it "creates events with this.createEvent in the JavaScript environment" do |
|
134 |
+ @agent.options['code'] = 'Agent.check = function() { this.createEvent({ message: "This is an event!", stuff: { foo: 5 } }); };' |
|
135 |
+ @agent.save! |
|
136 |
+ lambda { |
|
137 |
+ lambda { |
|
138 |
+ @agent.check |
|
139 |
+ }.should_not change { AgentLog.count } |
|
140 |
+ }.should change { Event.count }.by(1) |
|
141 |
+ created_event = @agent.events.last |
|
142 |
+ created_event.payload.should == { 'message' => "This is an event!", 'stuff' => { 'foo' => 5 } } |
|
143 |
+ end |
|
144 |
+ end |
|
145 |
+ |
|
146 |
+ describe "logging" do |
|
147 |
+ it "can output AgentLogs with this.log and this.error in the JavaScript environment" do |
|
148 |
+ @agent.options['code'] = 'Agent.check = function() { this.log("woah"); this.error("WOAH!"); };' |
|
149 |
+ @agent.save! |
|
150 |
+ lambda { |
|
151 |
+ lambda { |
|
152 |
+ @agent.check |
|
153 |
+ }.should_not raise_error |
|
154 |
+ }.should change { AgentLog.count }.by(2) |
|
155 |
+ |
|
156 |
+ log1, log2 = AgentLog.last(2) |
|
157 |
+ |
|
158 |
+ log1.message.should == "woah" |
|
159 |
+ log1.level.should == 3 |
|
160 |
+ log2.message.should == "WOAH!" |
|
161 |
+ log2.level.should == 4 |
|
162 |
+ end |
|
163 |
+ end |
|
164 |
+ |
|
165 |
+ describe "getting incoming events" do |
|
166 |
+ it "can access incoming events in the JavaScript enviroment via this.incomingEvents" do |
|
167 |
+ event = Event.new |
|
168 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
169 |
+ event.payload = { :data => "Something you should know about" } |
|
170 |
+ event.save! |
|
171 |
+ event.reload |
|
172 |
+ |
|
173 |
+ @agent.options['code'] = <<-JS |
|
174 |
+ Agent.receive = function() { |
|
175 |
+ var events = this.incomingEvents(); |
|
176 |
+ for(var i = 0; i < events.length; i++) { |
|
177 |
+ this.createEvent({ 'message': 'I got an event!', 'event_was': events[i].payload }); |
|
178 |
+ } |
|
179 |
+ } |
|
180 |
+ JS |
|
181 |
+ |
|
182 |
+ @agent.save! |
|
183 |
+ lambda { |
|
184 |
+ lambda { |
|
185 |
+ @agent.receive([events(:bob_website_agent_event), event]) |
|
186 |
+ }.should_not change { AgentLog.count } |
|
187 |
+ }.should change { Event.count }.by(2) |
|
188 |
+ created_event = @agent.events.first |
|
189 |
+ created_event.payload.should == { 'message' => "I got an event!", 'event_was' => { 'data' => "Something you should know about" } } |
|
190 |
+ end |
|
191 |
+ end |
|
192 |
+ |
|
193 |
+ describe "getting and setting memory, getting options" do |
|
194 |
+ it "can access options via this.options and work with memory via this.memory" do |
|
195 |
+ @agent.options['code'] = <<-JS |
|
196 |
+ Agent.check = function() { |
|
197 |
+ if (this.options('make_event')) { |
|
198 |
+ var callCount = this.memory('callCount') || 0; |
|
199 |
+ this.memory('callCount', callCount + 1); |
|
200 |
+ } |
|
201 |
+ }; |
|
202 |
+ JS |
|
203 |
+ |
|
204 |
+ @agent.save! |
|
205 |
+ |
|
206 |
+ lambda { |
|
207 |
+ lambda { |
|
208 |
+ |
|
209 |
+ @agent.check |
|
210 |
+ @agent.memory['callCount'].should_not be_present |
|
211 |
+ |
|
212 |
+ @agent.options['make_event'] = true |
|
213 |
+ @agent.check |
|
214 |
+ @agent.memory['callCount'].should == 1 |
|
215 |
+ |
|
216 |
+ @agent.check |
|
217 |
+ @agent.memory['callCount'].should == 2 |
|
218 |
+ |
|
219 |
+ @agent.memory['callCount'] = 20 |
|
220 |
+ @agent.check |
|
221 |
+ @agent.memory['callCount'].should == 21 |
|
222 |
+ |
|
223 |
+ }.should_not change { AgentLog.count } |
|
224 |
+ }.should_not change { Event.count } |
|
225 |
+ end |
|
226 |
+ end |
|
227 |
+ end |
|
228 |
+end |