Merged modifications with original project updates

James Peret 7 years ago
parent
commit
1e0df92b79
141 changed files with 2647 additions and 1141 deletions
  1. 5 6
      .travis.yml
  2. 12 0
      CHANGES.md
  3. 31 33
      Gemfile
  4. 200 184
      Gemfile.lock
  5. 2 2
      README.md
  6. 1 1
      Rakefile
  7. 4 0
      app/assets/javascripts/ace.js.coffee
  8. 7 2
      app/assets/javascripts/components/form_configurable.js.coffee
  9. 1 1
      app/assets/javascripts/components/utils.js.coffee
  10. 16 8
      app/assets/javascripts/pages/agent-edit-page.js.coffee
  11. 11 3
      app/concerns/file_handling.rb
  12. 1 1
      app/concerns/form_configurable.rb
  13. 17 6
      app/concerns/liquid_droppable.rb
  14. 26 5
      app/concerns/liquid_interpolatable.rb
  15. 0 1
      app/concerns/oauthable.rb
  16. 14 12
      app/concerns/sortable_events.rb
  17. 1 0
      app/concerns/web_request_concern.rb
  18. 30 9
      app/controllers/admin/users_controller.rb
  19. 4 2
      app/controllers/agents/dry_runs_controller.rb
  20. 11 5
      app/controllers/agents_controller.rb
  21. 23 7
      app/controllers/application_controller.rb
  22. 10 1
      app/controllers/scenario_imports_controller.rb
  23. 11 2
      app/controllers/scenarios_controller.rb
  24. 8 2
      app/controllers/user_credentials_controller.rb
  25. 5 5
      app/controllers/web_requests_controller.rb
  26. 10 0
      app/helpers/application_helper.rb
  27. 9 8
      app/jobs/agent_propagate_job.rb
  28. 5 5
      app/models/agent.rb
  29. 3 5
      app/models/agent_log.rb
  30. 65 3
      app/models/agents/data_output_agent.rb
  31. 10 10
      app/models/agents/email_digest_agent.rb
  32. 2 1
      app/models/agents/google_calendar_publish_agent.rb
  33. 2 0
      app/models/agents/http_status_agent.rb
  34. 45 28
      app/models/agents/post_agent.rb
  35. 51 47
      app/models/agents/pushover_agent.rb
  36. 107 31
      app/models/agents/rss_agent.rb
  37. 1 1
      app/models/agents/trigger_agent.rb
  38. 10 1
      app/models/agents/twitter_action_agent.rb
  39. 29 15
      app/models/agents/weather_agent.rb
  40. 17 4
      app/models/agents/webhook_agent.rb
  41. 1 1
      app/models/agents/website_agent.rb
  42. 0 15
      app/models/contact.rb
  43. 0 2
      app/models/control_link.rb
  44. 5 3
      app/models/event.rb
  45. 0 2
      app/models/link.rb
  46. 0 3
      app/models/scenario.rb
  47. 21 12
      app/models/service.rb
  48. 15 5
      app/models/user.rb
  49. 0 2
      app/models/user_credential.rb
  50. 5 3
      app/presenters/form_configurable_agent_presenter.rb
  51. 3 0
      app/views/admin/users/_form.html.erb
  52. 3 2
      app/views/admin/users/index.html.erb
  53. 1 1
      app/views/agents/_form.html.erb
  54. 7 0
      app/views/agents/agent_views/manual_event_agent/_show.html.erb
  55. 1 1
      app/views/agents/index.html.erb
  56. 44 0
      app/views/application/undefined_agents.html.erb
  57. 8 0
      app/views/layouts/_navigation.html.erb
  58. 3 3
      app/views/logs/index.html.erb
  59. 4 3
      bin/rails
  60. 3 2
      bin/rake
  61. 3 2
      bin/rspec
  62. 6 6
      bin/spring
  63. 1 1
      config.ru
  64. 1 10
      config/application.rb
  65. 1 1
      config/boot.rb
  66. 1 4
      config/environment.rb
  67. 25 13
      config/environments/development.rb
  68. 19 12
      config/environments/production.rb
  69. 7 7
      config/environments/test.rb
  70. 2 2
      config/initializers/action_mailer.rb
  71. 0 1
      config/initializers/ar_mysql_column_charset.rb
  72. 5 0
      config/initializers/cookies_serializer.rb
  73. 10 10
      config/initializers/delayed_job.rb
  74. 24 0
      config/initializers/new_framework_defaults.rb
  75. 1 1
      config/initializers/sanitizer.rb
  76. 7 8
      config/initializers/silence_worker_status_logger.rb
  77. 1 1
      config/initializers/wrap_parameters.rb
  78. 5 0
      config/routes.rb
  79. 6 0
      config/spring.rb
  80. 2 0
      db/migrate/20140505201716_migrate_agents_to_liquid_templating.rb
  81. 0 2
      db/migrate/20140813110107_set_charset_for_mysql.rb
  82. 25 0
      db/migrate/20160607055850_change_events_order_to_events_list_order.rb
  83. 8 0
      db/migrate/20160807000122_remove_queue_from_email_digest_agent_memory.rb
  84. 15 0
      db/migrate/20160823151303_set_emit_error_event_for_twitter_action_agents.rb
  85. 58 0
      db/migrate/20161004120214_update_pushover_agent_options.rb
  86. 9 0
      db/migrate/20161007030910_reset_data_output_agents.rb
  87. 4 4
      db/seeds/seeder.rb
  88. 2 2
      doc/README.md
  89. 1 1
      doc/deployment/capistrano/deploy.rb
  90. 5 5
      doc/manual/installation.md
  91. 3 3
      doc/manual/requirements.md
  92. 1 1
      docker/multi-process/scripts/init
  93. 1 1
      docker/scripts/prepare
  94. 13 0
      docker/scripts/setup
  95. 41 31
      docker/single-process/docker-compose.yml
  96. 11 7
      docker/single-process/environment.yml
  97. 77 63
      docker/single-process/postgresql.yml
  98. 1 1
      docker/single-process/scripts/init
  99. 0 13
      lib/ar_mysql_column_charset.rb
  100. 0 118
      lib/ar_mysql_column_charset/main.rb
  101. 286 0
      lib/feedjira_extension.rb
  102. 0 1
      lib/tasks/ar_mysql_column_charset.rake
  103. 1 1
      lib/tasks/production.rake
  104. 24 5
      lib/utils.rb
  105. 61 2
      spec/concerns/liquid_interpolatable_spec.rb
  106. 40 3
      spec/controllers/admin/users_controller_spec.rb
  107. 9 9
      spec/controllers/agents/dry_runs_controller_spec.rb
  108. 69 48
      spec/controllers/agents_controller_spec.rb
  109. 8 8
      spec/controllers/events_controller_spec.rb
  110. 5 5
      spec/controllers/jobs_controller_spec.rb
  111. 4 4
      spec/controllers/logs_controller_spec.rb
  112. 9 8
      spec/controllers/omniauth_callbacks_controller_spec.rb
  113. 1 1
      spec/controllers/scenario_imports_controller_spec.rb
  114. 29 23
      spec/controllers/scenarios_controller_spec.rb
  115. 4 4
      spec/controllers/services_controller_spec.rb
  116. 14 14
      spec/controllers/user_credentials_controller_spec.rb
  117. 13 7
      spec/controllers/users/registrations_controller_spec.rb
  118. 14 14
      spec/controllers/web_requests_controller_spec.rb
  119. 2 2
      spec/data_fixtures/onethingwell.atom
  120. 2 1
      spec/data_fixtures/urlTest.html
  121. 76 0
      spec/features/dry_running_spec.rb
  122. 10 0
      spec/features/form_configurable_feature_spec.rb
  123. 46 0
      spec/features/scenario_import_spec.rb
  124. 21 0
      spec/features/undefined_agents_spec.rb
  125. 22 0
      spec/fixtures/agents.yml
  126. 5 5
      spec/models/agents/boxcar_agent_spec.rb
  127. 52 6
      spec/models/agents/data_output_agent_spec.rb
  128. 28 25
      spec/models/agents/email_digest_agent_spec.rb
  129. 18 0
      spec/models/agents/post_agent_spec.rb
  130. 34 32
      spec/models/agents/pushover_agent_spec.rb
  131. 92 2
      spec/models/agents/rss_agent_spec.rb
  132. 43 12
      spec/models/agents/twitter_action_agent_spec.rb
  133. 10 0
      spec/models/agents/weather_agent_spec.rb
  134. 26 0
      spec/models/agents/webhook_agent_spec.rb
  135. 7 2
      spec/models/agents/website_agent_spec.rb
  136. 25 0
      spec/models/service_spec.rb
  137. 0 8
      spec/models/user_credential_spec.rb
  138. 27 0
      spec/models/users_spec.rb
  139. 2 2
      spec/rails_helper.rb
  140. 23 3
      spec/support/shared_examples/file_handling_consumer.rb
  141. 142 36
      vendor/assets/javascripts/jquery.serializeObject.js

+ 5 - 6
.travis.yml

@@ -24,18 +24,17 @@ matrix:
24 24
     - env: DOCKER_IMAGE=cantino/huginn DOCKERFILE=docker/multi-process/Dockerfile
25 25
     - env: RSPEC_TASK=spec:features
26 26
   include:
27
-    - rvm: 2.3.0
27
+    - rvm: 2.3.1
28 28
       env: DATABASE_ADAPTER=mysql2 DOCKER_IMAGE=cantino/huginn-single-process DOCKERFILE=docker/single-process/Dockerfile
29
-    - rvm: 2.3.0
29
+    - rvm: 2.3.1
30 30
       env: DATABASE_ADAPTER=mysql2 DOCKER_IMAGE=cantino/huginn DOCKERFILE=docker/multi-process/Dockerfile
31
-    - rvm: 2.3.0
31
+    - rvm: 2.3.1
32 32
       env: RSPEC_TASK=spec:features DATABASE_ADAPTER=mysql2
33
-    - rvm: 2.3.0
33
+    - rvm: 2.3.1
34 34
       env: RSPEC_TASK=spec:features DATABASE_ADAPTER=postgresql DATABASE_USERNAME=postgres
35 35
 rvm:
36
-- 2.1
37 36
 - 2.2
38
-- 2.3.0
37
+- 2.3.1
39 38
 cache: bundler
40 39
 bundler_args: --without development production
41 40
 before_install:

+ 12 - 0
CHANGES.md

@@ -2,6 +2,18 @@
2 2
 
3 3
 | DateOfChange   | Changes                                                                                                      |
4 4
 |----------------|--------------------------------------------------------------------------------------------------------------|
5
+| Oct 17, 2016   | Normalize URL in `to_uri` and `uri_expand` liquid filters.                                                   |
6
+| Oct 06, 2016   | `RssAgent` is reimplemented migrating its underlying feed parser from FeedNormalizer to Feedjira. [1564](https://github.com/cantino/huginn/pull/1564)     |
7
+| Oct 05, 2016   | Migrate to Rails 5. [1688](https://github.com/cantino/huginn/pull/1688)                                      |
8
+| Oct 05, 2016   | Improve URL normalization in `WebsiteAgent`. [1719](https://github.com/cantino/huginn/pull/1719)             |
9
+| Oct 05, 2016   | `PushoverAgent` now treats parameter options as templates rather than default values. [1720](https://github.com/cantino/huginn/pull/1720) |
10
+| Sep 19, 2016   | Add multipart file upload to `PostAgent`. [1690](https://github.com/cantino/huginn/pull/1690)                |
11
+| Sep 08, 2016   | Allow `TwitterUserAgent` to retry failed actions. [1645](https://github.com/cantino/huginn/pull/1645)        |
12
+| Aug 16, 2016   | `EmailDigestAgent` now relies on received events, rather in memory. [1624](https://github.com/cantino/huginn/pull/1624) |
13
+| Aug 08, 2016   | `DataOutputAgent` now limits events after ordering. [1444](https://github.com/cantino/huginn/pull/1444)      |
14
+| Aug 05, 2016   | Add `api_key` option to `UserLocationAgent`. [1613](https://github.com/cantino/huginn/pull/1613)             |
15
+| Jul 25, 2016   | Add `LiquidOutputAgent`. [1587](https://github.com/cantino/huginn/pull/1587)                                 |
16
+| Jul 25, 2016   | Allow `PostAgent` headers to interpolate event data. [1606](https://github.com/cantino/huginn/pull/1606)     |
5 17
 | Jul 25, 2016   | Remove `smtp.yml` configuration file, the SMTP configuration now needs to be done via environment variables. [1595](https://github.com/cantino/huginn/pull/1595) |
6 18
 | Jul 25, 2016   | Change `jsonpath` gem to a fork located at [https://github.com/Skarlso/jsonpathv2](https://github.com/Skarlso/jsonpathv2) [1596](https://github.com/cantino/huginn/pull/1596) |
7 19
 | Jul 20, 2016   | Add redirection information to the `HttpStatusAgent` [1590](https://github.com/cantino/huginn/pull/1590) |

+ 31 - 33
Gemfile

@@ -1,7 +1,7 @@
1 1
 source 'https://rubygems.org'
2 2
 
3
-# Ruby 2.0 is the minimum requirement
4
-ruby ['2.0.0', RUBY_VERSION].max
3
+# Ruby 2.2.2 is the minimum requirement
4
+ruby ['2.2.2', RUBY_VERSION].max
5 5
 
6 6
 # Load vendored dotenv gem and .env file
7 7
 require File.join(File.dirname(__FILE__), 'lib/gemfile_helper.rb')
@@ -38,7 +38,8 @@ gem 'slack-notifier', '~> 1.0.0'  # SlackAgent
38 38
 gem 'hypdf', '~> 1.0.10'          # PDFInfoAgent
39 39
 
40 40
 # Weibo Agents
41
-gem 'weibo_2', github: 'cantino/weibo_2', branch: 'master'
41
+# FIXME needs to loosen omniauth dependency
42
+gem 'weibo_2', github: 'dsander/weibo_2', branch: 'master'
42 43
 
43 44
 # GoogleCalendarPublishAgent
44 45
 gem "google-api-client", require: 'google/api_client'
@@ -46,11 +47,11 @@ gem "google-api-client", require: 'google/api_client'
46 47
 # Twitter Agents
47 48
 gem 'twitter', '~> 5.14.0' # Must to be loaded before cantino-twitter-stream.
48 49
 gem 'twitter-stream', github: 'cantino/twitter-stream', branch: 'huginn'
49
-gem 'omniauth-twitter'
50
+gem 'omniauth-twitter', '~> 1.2.1'
50 51
 
51 52
 # Tumblr Agents
52 53
 gem 'tumblr_client', github: 'tumblr/tumblr_client', branch: 'master'  # '>= 0.8.5'
53
-gem 'omniauth-tumblr'
54
+gem 'omniauth-tumblr', '~> 1.2'
54 55
 
55 56
 # Dropbox Agents
56 57
 gem 'dropbox-api'
@@ -71,7 +72,7 @@ gem 'aws-sdk-core', '~> 2.2.15'
71 72
 
72 73
 # Optional Services.
73 74
 gem 'omniauth-37signals'          # BasecampAgent
74
-gem 'omniauth-wunderlist', github: 'wunderlist/omniauth-wunderlist', ref: 'd0910d0396107b9302aa1bc50e74bb140990ccb8'
75
+gem 'omniauth-wunderlist'
75 76
 
76 77
 # Bundler <1.5 does not recognize :x64_mingw as a valid platform name.
77 78
 # Unfortunately, it can't self-update because it errors when encountering :x64_mingw.
@@ -80,44 +81,41 @@ unless Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.5.0')
80 81
   exit 1
81 82
 end
82 83
 
83
-gem 'protected_attributes', '~>1.0.8' # This must be loaded before some other gems, like delayed_job.
84 84
 gem 'ace-rails-ap', '~> 2.0.1'
85 85
 gem 'bootstrap-kaminari-views', '~> 0.0.3'
86 86
 gem 'bundler', '>= 1.5.0'
87
-gem 'coffee-rails', '~> 4.1.1'
87
+gem 'coffee-rails', '~> 4.2'
88 88
 gem 'daemons', '~> 1.1.9'
89 89
 gem 'delayed_job', '~> 4.1.0'
90
-gem 'delayed_job_active_record', github: 'collectiveidea/delayed_job_active_record', branch: 'master'
91
-gem 'devise', '~> 3.5.4'
90
+gem 'delayed_job_active_record', github: 'dsander/delayed_job_active_record', branch: 'rails5'
91
+gem 'devise','~> 4.2.0'
92 92
 gem 'em-http-request', '~> 1.1.2'
93 93
 gem 'faraday', '~> 0.9.0'
94 94
 gem 'faraday_middleware', github: 'lostisland/faraday_middleware', branch: 'master'  # '>= 0.10.1'
95
-gem 'feed-normalizer'
95
+gem 'feedjira', '~> 2.0'
96 96
 gem 'font-awesome-sass', '~> 4.3.2'
97 97
 gem 'foreman', '~> 0.63.0'
98
-# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5
99
-# in its own Gemfile.
100 98
 gem 'geokit', '~> 1.8.4'
101
-gem 'geokit-rails', '~> 2.0.1'
99
+gem 'geokit-rails', '~> 2.2.0'
102 100
 gem 'httparty', '~> 0.13'
103 101
 gem 'httmultiparty', '~> 0.3.16'
104
-gem 'jquery-rails', '~> 3.1.3'
102
+gem 'jquery-rails', '~> 4.2.1'
105 103
 gem 'huginn_agent', '~> 0.4.0'
106 104
 gem 'json', '~> 1.8.1'
107
-gem 'jsonpathv2', '~> 0.0.3'
108
-gem 'kaminari', '~> 0.16.1'
105
+gem 'jsonpathv2', '~> 0.0.8'
106
+gem 'kaminari', github: "amatsuda/kaminari", branch: '0-17-stable'
109 107
 gem 'kramdown', '~> 1.3.3'
110 108
 gem 'liquid', '~> 3.0.3'
109
+gem 'loofah', '~> 2.0'
111 110
 gem 'mini_magick'
112 111
 gem 'multi_xml'
113 112
 gem 'nokogiri', '1.6.8'
114
-gem 'omniauth'
115
-gem 'rails', '4.2.5.2'
113
+gem 'omniauth', '~> 1.3.1'
114
+gem 'rails', '~> 5.0.0.1'
116 115
 gem 'rufus-scheduler', '~> 3.0.8', require: false
117
-gem 'sass-rails',   '~> 5.0.3'
116
+gem 'sass-rails',   '~> 5.0.6'
118 117
 gem 'select2-rails', '~> 3.5.4'
119 118
 gem 'spectrum-rails'
120
-gem 'string-scrub'	# for ruby <2.1
121 119
 gem 'therubyracer', '~> 0.12.2'
122 120
 gem 'typhoeus', '~> 0.6.3'
123 121
 gem 'uglifier', '~> 2.7.2'
@@ -126,12 +124,12 @@ gem 'mixpanel_client'
126 124
 group :development do
127 125
   gem 'better_errors', '~> 1.1'
128 126
   gem 'binding_of_caller'
129
-  gem 'quiet_assets'
130 127
   gem 'guard', '~> 2.13.0'
131 128
   gem 'guard-livereload', '~> 2.5.1'
132 129
   gem 'guard-rspec', '~> 4.6.4'
133 130
   gem 'rack-livereload', '~> 0.3.16'
134
-  gem 'letter_opener_web'
131
+  gem 'letter_opener_web', '~> 1.3.0'
132
+  gem 'web-console'
135 133
 
136 134
   gem 'capistrano', '~> 3.4.0'
137 135
   gem 'capistrano-rails', '~> 1.1'
@@ -139,7 +137,8 @@ group :development do
139 137
 
140 138
   if_true(ENV['SPRING']) do
141 139
     gem 'spring-commands-rspec', '~> 1.0.4'
142
-    gem 'spring', '~> 1.6.3'
140
+    gem 'spring', '~> 1.7.2'
141
+    gem 'spring-watcher-listen', '~> 2.0.0'
143 142
   end
144 143
 
145 144
   group :test do
@@ -150,10 +149,11 @@ group :development do
150 149
     gem 'pry-rails'
151 150
     gem 'pry-byebug'
152 151
     gem 'rr'
153
-    gem 'rspec', '~> 3.2'
152
+    gem 'rspec', '~> 3.5'
154 153
     gem 'rspec-collection_matchers', '~> 1.1.0'
155
-    gem 'rspec-rails', '~> 3.1'
156
-    gem 'rspec-html-matchers', '~> 0.7'
154
+    gem 'rspec-rails', '~> 3.5.2'
155
+    gem 'rspec-html-matchers', '~> 0.8'
156
+    gem 'rails-controller-testing'
157 157
     gem 'shoulda-matchers'
158 158
     gem 'vcr'
159 159
     gem 'webmock', '~> 1.17.4', require: false
@@ -162,15 +162,17 @@ group :development do
162 162
 end
163 163
 
164 164
 group :production do
165
-  gem 'rack', '> 1.5.0'
166
-  gem 'unicorn', '~> 4.9.0'
165
+  gem 'unicorn', '~> 5.1.0'
167 166
 end
168 167
 
169 168
 # Platform requirements.
169
+require 'rbconfig'
170 170
 gem 'ffi', '>= 1.9.4'		# required by typhoeus; 1.9.4 has fixes for *BSD.
171 171
 gem 'tzinfo', '>= 1.2.0'	# required by rails; 1.2.0 has support for *BSD and Solaris.
172 172
 # Windows does not have zoneinfo files, so bundle the tzinfo-data gem.
173 173
 gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw]
174
+# BSD systems require rb-kqueue for "listen" to avoid polling for changes.
175
+gem 'rb-kqueue', '>= 0.2', require: /bsd|dragonfly/i === RbConfig::CONFIG['target_os']
174 176
 
175 177
 
176 178
 on_heroku = ENV['ON_HEROKU'] ||
@@ -185,10 +187,6 @@ ENV['DATABASE_ADAPTER'] ||=
185 187
     'mysql2'
186 188
   end
187 189
 
188
-if_true(on_heroku) do
189
-  gem 'rails_12factor', group: :production
190
-end
191
-
192 190
 if_true(ENV['DATABASE_ADAPTER'].strip == 'postgresql') do
193 191
   gem 'pg', '~> 0.18.3'
194 192
 end

+ 200 - 184
Gemfile.lock

@@ -1,4 +1,13 @@
1 1
 GIT
2
+  remote: git://github.com/amatsuda/kaminari.git
3
+  revision: abbf93d557208ee1d0b612c612cd079f86ed54f4
4
+  branch: 0-17-stable
5
+  specs:
6
+    kaminari (0.17.0)
7
+      actionpack (>= 3.0.0)
8
+      activesupport (>= 3.0.0)
9
+
10
+GIT
2 11
   remote: git://github.com/cantino/twitter-stream.git
3 12
   revision: f7e7edb0bae013bffabf3598e7147773d9fd370f
4 13
   branch: huginn
@@ -9,24 +18,24 @@ GIT
9 18
       simple_oauth (~> 0.3.0)
10 19
 
11 20
 GIT
12
-  remote: git://github.com/cantino/weibo_2.git
13
-  revision: 00e57d29d8252126014b038cd738b02e05e4cfc5
14
-  branch: master
21
+  remote: git://github.com/dsander/delayed_job_active_record.git
22
+  revision: b314972ccc92e0e8b03b1589174d8fb9a82b3cd0
23
+  branch: rails5
15 24
   specs:
16
-    weibo_2 (0.1.7)
17
-      hashie (~> 2.0.4)
18
-      multi_json (~> 1)
19
-      oauth2 (~> 0.9.1)
20
-      rest-client (~> 1.8)
25
+    delayed_job_active_record (4.1.1)
26
+      activerecord (>= 3.0, < 5.1)
27
+      delayed_job (>= 3.0, < 5)
21 28
 
22 29
 GIT
23
-  remote: git://github.com/collectiveidea/delayed_job_active_record.git
24
-  revision: 61e688e03b2ef4004b08de6d1e0a123fda8fffad
30
+  remote: git://github.com/dsander/weibo_2.git
31
+  revision: e5b77f21a7e9a666b582c48e16b1e96fca198cf8
25 32
   branch: master
26 33
   specs:
27
-    delayed_job_active_record (4.1.0)
28
-      activerecord (>= 3.0, < 5.1)
29
-      delayed_job (>= 3.0, < 5)
34
+    weibo_2 (0.1.7)
35
+      hashie (~> 3)
36
+      multi_json (~> 1)
37
+      oauth2 (~> 1)
38
+      rest-client (~> 1.8)
30 39
 
31 40
 GIT
32 41
   remote: git://github.com/lostisland/faraday_middleware.git
@@ -49,15 +58,6 @@ GIT
49 58
       oauth
50 59
       simple_oauth
51 60
 
52
-GIT
53
-  remote: git://github.com/wunderlist/omniauth-wunderlist.git
54
-  revision: d0910d0396107b9302aa1bc50e74bb140990ccb8
55
-  ref: d0910d0396107b9302aa1bc50e74bb140990ccb8
56
-  specs:
57
-    omniauth-wunderlist (0.0.1)
58
-      omniauth (~> 1.0)
59
-      omniauth-oauth2 (~> 1.1)
60
-
61 61
 PATH
62 62
   remote: vendor/gems/dotenv-2.0.1
63 63
   specs:
@@ -69,50 +69,52 @@ GEM
69 69
   remote: https://rubygems.org/
70 70
   specs:
71 71
     ace-rails-ap (2.0.1)
72
-    actionmailer (4.2.5.2)
73
-      actionpack (= 4.2.5.2)
74
-      actionview (= 4.2.5.2)
75
-      activejob (= 4.2.5.2)
72
+    actioncable (5.0.0.1)
73
+      actionpack (= 5.0.0.1)
74
+      nio4r (~> 1.2)
75
+      websocket-driver (~> 0.6.1)
76
+    actionmailer (5.0.0.1)
77
+      actionpack (= 5.0.0.1)
78
+      actionview (= 5.0.0.1)
79
+      activejob (= 5.0.0.1)
76 80
       mail (~> 2.5, >= 2.5.4)
77
-      rails-dom-testing (~> 1.0, >= 1.0.5)
78
-    actionpack (4.2.5.2)
79
-      actionview (= 4.2.5.2)
80
-      activesupport (= 4.2.5.2)
81
-      rack (~> 1.6)
82
-      rack-test (~> 0.6.2)
83
-      rails-dom-testing (~> 1.0, >= 1.0.5)
81
+      rails-dom-testing (~> 2.0)
82
+    actionpack (5.0.0.1)
83
+      actionview (= 5.0.0.1)
84
+      activesupport (= 5.0.0.1)
85
+      rack (~> 2.0)
86
+      rack-test (~> 0.6.3)
87
+      rails-dom-testing (~> 2.0)
84 88
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
85
-    actionview (4.2.5.2)
86
-      activesupport (= 4.2.5.2)
89
+    actionview (5.0.0.1)
90
+      activesupport (= 5.0.0.1)
87 91
       builder (~> 3.1)
88 92
       erubis (~> 2.7.0)
89
-      rails-dom-testing (~> 1.0, >= 1.0.5)
93
+      rails-dom-testing (~> 2.0)
90 94
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
91
-    activejob (4.2.5.2)
92
-      activesupport (= 4.2.5.2)
93
-      globalid (>= 0.3.0)
94
-    activemodel (4.2.5.2)
95
-      activesupport (= 4.2.5.2)
96
-      builder (~> 3.1)
97
-    activerecord (4.2.5.2)
98
-      activemodel (= 4.2.5.2)
99
-      activesupport (= 4.2.5.2)
100
-      arel (~> 6.0)
101
-    activesupport (4.2.5.2)
95
+    activejob (5.0.0.1)
96
+      activesupport (= 5.0.0.1)
97
+      globalid (>= 0.3.6)
98
+    activemodel (5.0.0.1)
99
+      activesupport (= 5.0.0.1)
100
+    activerecord (5.0.0.1)
101
+      activemodel (= 5.0.0.1)
102
+      activesupport (= 5.0.0.1)
103
+      arel (~> 7.0)
104
+    activesupport (5.0.0.1)
105
+      concurrent-ruby (~> 1.0, >= 1.0.2)
102 106
       i18n (~> 0.7)
103
-      json (~> 1.7, >= 1.7.7)
104 107
       minitest (~> 5.1)
105
-      thread_safe (~> 0.3, >= 0.3.4)
106 108
       tzinfo (~> 1.1)
107 109
     addressable (2.3.8)
108
-    arel (6.0.3)
110
+    arel (7.1.1)
109 111
     autoparse (0.3.3)
110 112
       addressable (>= 2.3.1)
111 113
       extlib (>= 0.9.15)
112 114
       multi_json (>= 1.0.0)
113 115
     aws-sdk-core (2.2.15)
114 116
       jmespath (~> 1.0)
115
-    bcrypt (3.1.10)
117
+    bcrypt (3.1.11)
116 118
     better_errors (1.1.0)
117 119
       coderay (>= 1.0.0)
118 120
       erubis (>= 2.6.6)
@@ -147,15 +149,15 @@ GEM
147 149
     chronic (0.10.2)
148 150
     cliver (0.3.2)
149 151
     coderay (1.1.0)
150
-    coffee-rails (4.1.1)
152
+    coffee-rails (4.2.1)
151 153
       coffee-script (>= 2.2.0)
152
-      railties (>= 4.0.0, < 5.1.x)
154
+      railties (>= 4.0.0, < 5.2.x)
153 155
     coffee-script (2.4.1)
154 156
       coffee-script-source
155 157
       execjs
156 158
     coffee-script-source (1.10.0)
157 159
     colorize (0.7.7)
158
-    concurrent-ruby (1.0.1)
160
+    concurrent-ruby (1.0.2)
159 161
     cookiejar (0.3.2)
160 162
     coveralls (0.7.12)
161 163
       multi_json (~> 1.10)
@@ -168,16 +170,15 @@ GEM
168 170
     daemons (1.1.9)
169 171
     database_cleaner (1.5.3)
170 172
     debug_inspector (0.0.2)
171
-    delayed_job (4.1.1)
172
-      activesupport (>= 3.0, < 5.0)
173
+    delayed_job (4.1.2)
174
+      activesupport (>= 3.0, < 5.1)
173 175
     delorean (2.1.0)
174 176
       chronic
175
-    devise (3.5.4)
177
+    devise (4.2.0)
176 178
       bcrypt (~> 3.0)
177 179
       orm_adapter (~> 0.1)
178
-      railties (>= 3.2.6, < 5)
180
+      railties (>= 4.1.0, < 5.1)
179 181
       responders
180
-      thread_safe (~> 0.1)
181 182
       warden (~> 1.2.3)
182 183
     diff-lcs (1.2.5)
183 184
     docile (1.1.5)
@@ -211,15 +212,17 @@ GEM
211 212
       oauth (>= 0.4.1)
212 213
     execjs (2.6.0)
213 214
     extlib (0.9.16)
214
-    faraday (0.9.1)
215
+    faraday (0.9.2)
215 216
       multipart-post (>= 1.2, < 3)
216
-    feed-normalizer (1.5.2)
217
-      hpricot (>= 0.6)
218
-      simple-rss (>= 1.1)
217
+    feedjira (2.0.0)
218
+      faraday (~> 0.9)
219
+      faraday_middleware (~> 0.9)
220
+      loofah (~> 2.0)
221
+      sax-machine (~> 1.0)
219 222
     ffi (1.9.10)
220 223
     font-awesome-sass (4.3.2.1)
221 224
       sass (~> 3.2)
222
-    forecast_io (2.0.0)
225
+    forecast_io (2.0.1)
223 226
       faraday
224 227
       hashie
225 228
       multi_json
@@ -229,10 +232,10 @@ GEM
229 232
     formatador (0.2.5)
230 233
     geokit (1.8.5)
231 234
       multi_json (>= 1.3.2)
232
-    geokit-rails (2.0.1)
235
+    geokit-rails (2.2.0)
233 236
       geokit (~> 1.5)
234 237
       rails (>= 3.0)
235
-    globalid (0.3.6)
238
+    globalid (0.3.7)
236 239
       activesupport (>= 4.1.0)
237 240
     google-api-client (0.7.1)
238 241
       addressable (>= 2.3.2)
@@ -260,15 +263,14 @@ GEM
260 263
       guard (~> 2.8)
261 264
       guard-compat (~> 1.0)
262 265
       multi_json (~> 1.8)
263
-    guard-rspec (4.6.4)
266
+    guard-rspec (4.6.5)
264 267
       guard (~> 2.1)
265 268
       guard-compat (~> 1.1)
266 269
       rspec (>= 2.99.0, < 4.0)
267
-    hashie (2.0.5)
270
+    hashie (3.4.6)
268 271
     haversine (0.3.0)
269 272
     hipchat (1.2.0)
270 273
       httparty
271
-    hpricot (0.8.6)
272 274
     httmultiparty (0.3.16)
273 275
       httparty (>= 0.7.3)
274 276
       mimemagic
@@ -288,17 +290,15 @@ GEM
288 290
       httparty (~> 0.13)
289 291
     i18n (0.7.0)
290 292
     jmespath (1.1.3)
291
-    jquery-rails (3.1.3)
292
-      railties (>= 3.0, < 5.0)
293
+    jquery-rails (4.2.1)
294
+      rails-dom-testing (>= 1, < 3)
295
+      railties (>= 4.2.0)
293 296
       thor (>= 0.14, < 2.0)
294 297
     json (1.8.3)
295
-    jsonpathv2 (0.0.3)
298
+    jsonpathv2 (0.0.8)
296 299
       multi_json
297 300
     jwt (1.4.1)
298
-    kaminari (0.16.1)
299
-      actionpack (>= 3.0.0)
300
-      activesupport (>= 3.0.0)
301
-    kgio (2.9.3)
301
+    kgio (2.10.0)
302 302
     kramdown (1.3.3)
303 303
     launchy (2.4.2)
304 304
       addressable (~> 2.3)
@@ -308,7 +308,7 @@ GEM
308 308
       actionmailer (>= 3.2)
309 309
       letter_opener (~> 1.0)
310 310
       railties (>= 3.2)
311
-    libv8 (3.16.14.13)
311
+    libv8 (3.16.14.15)
312 312
     liquid (3.0.6)
313 313
     listen (3.0.5)
314 314
       rb-fsevent (>= 0.9.3)
@@ -318,20 +318,24 @@ GEM
318 318
     lumberjack (1.0.10)
319 319
     macaddr (1.7.1)
320 320
       systemu (~> 2.6.2)
321
-    mail (2.6.3)
322
-      mime-types (>= 1.16, < 3)
321
+    mail (2.6.4)
322
+      mime-types (>= 1.16, < 4)
323 323
     memoizable (0.4.2)
324 324
       thread_safe (~> 0.3, >= 0.3.1)
325 325
     method_source (0.8.2)
326
-    mime-types (2.99.1)
326
+    mime-types (2.99.3)
327 327
     mimemagic (0.3.1)
328 328
     mini_magick (4.2.3)
329 329
     mini_portile2 (2.1.0)
330
+<<<<<<< HEAD
330 331
     minitest (5.8.4)
331 332
     mixpanel_client (4.1.4)
332 333
       typhoeus
334
+=======
335
+    minitest (5.9.0)
336
+>>>>>>> 0fcd8e285ebe9c04fb6ce5abd5a1f776021bdf89
333 337
     mqtt (0.3.1)
334
-    multi_json (1.11.2)
338
+    multi_json (1.12.1)
335 339
     multi_xml (0.5.5)
336 340
     multipart-post (2.0.0)
337 341
     mysql2 (0.3.20)
@@ -340,8 +344,9 @@ GEM
340 344
     net-ftp-list (3.2.8)
341 345
     net-scp (1.2.1)
342 346
       net-ssh (>= 2.6.5)
343
-    net-ssh (2.9.2)
347
+    net-ssh (3.0.2)
344 348
     netrc (0.10.3)
349
+    nio4r (1.2.1)
345 350
     nokogiri (1.6.8)
346 351
       mini_portile2 (~> 2.1.0)
347 352
       pkg-config (~> 1.1.7)
@@ -349,15 +354,15 @@ GEM
349 354
       nenv (~> 0.1)
350 355
       shellany (~> 0.0)
351 356
     oauth (0.4.7)
352
-    oauth2 (0.9.4)
357
+    oauth2 (1.2.0)
353 358
       faraday (>= 0.8, < 0.10)
354 359
       jwt (~> 1.0)
355 360
       multi_json (~> 1.3)
356 361
       multi_xml (~> 0.5)
357
-      rack (~> 1.2)
358
-    omniauth (1.2.2)
362
+      rack (>= 1.2, < 3)
363
+    omniauth (1.3.1)
359 364
       hashie (>= 1.2, < 4)
360
-      rack (~> 1.0)
365
+      rack (>= 1.0, < 3)
361 366
     omniauth-37signals (1.0.5)
362 367
       omniauth (~> 1.0)
363 368
       omniauth-oauth2 (~> 1.0)
@@ -367,19 +372,21 @@ GEM
367 372
       evernote-thrift
368 373
       multi_json (~> 1.0)
369 374
       omniauth-oauth (~> 1.0)
370
-    omniauth-oauth (1.0.1)
375
+    omniauth-oauth (1.1.0)
371 376
       oauth
372 377
       omniauth (~> 1.0)
373
-    omniauth-oauth2 (1.1.2)
374
-      faraday (>= 0.8, < 0.10)
375
-      multi_json (~> 1.3)
376
-      oauth2 (~> 0.9.3)
378
+    omniauth-oauth2 (1.3.1)
379
+      oauth2 (~> 1.0)
377 380
       omniauth (~> 1.2)
378
-    omniauth-tumblr (1.1)
379
-      omniauth-oauth (~> 1.0)
380
-    omniauth-twitter (1.0.1)
381
-      multi_json (~> 1.3)
381
+    omniauth-tumblr (1.2)
382
+      multi_json
382 383
       omniauth-oauth (~> 1.0)
384
+    omniauth-twitter (1.2.1)
385
+      json (~> 1.3)
386
+      omniauth-oauth (~> 1.1)
387
+    omniauth-wunderlist (0.0.2)
388
+      omniauth (~> 1.0)
389
+      omniauth-oauth2 (~> 1.1)
383 390
     orm_adapter (0.5.0)
384 391
     pg (0.18.3)
385 392
     pkg-config (1.1.7)
@@ -389,8 +396,6 @@ GEM
389 396
       multi_json (~> 1.0)
390 397
       websocket-driver (>= 0.2.0)
391 398
     polyglot (0.3.5)
392
-    protected_attributes (1.0.8)
393
-      activemodel (>= 4.0.1, < 5.0)
394 399
     pry (0.10.3)
395 400
       coderay (~> 1.1.0)
396 401
       method_source (~> 0.8.1)
@@ -400,49 +405,47 @@ GEM
400 405
       pry (~> 0.10)
401 406
     pry-rails (0.3.4)
402 407
       pry (>= 0.9.10)
403
-    quiet_assets (1.1.0)
404
-      railties (>= 3.1, < 5.0)
405
-    rack (1.6.4)
408
+    rack (2.0.1)
406 409
     rack-livereload (0.3.16)
407 410
       rack
408 411
     rack-test (0.6.3)
409 412
       rack (>= 1.0)
410
-    rails (4.2.5.2)
411
-      actionmailer (= 4.2.5.2)
412
-      actionpack (= 4.2.5.2)
413
-      actionview (= 4.2.5.2)
414
-      activejob (= 4.2.5.2)
415
-      activemodel (= 4.2.5.2)
416
-      activerecord (= 4.2.5.2)
417
-      activesupport (= 4.2.5.2)
413
+    rails (5.0.0.1)
414
+      actioncable (= 5.0.0.1)
415
+      actionmailer (= 5.0.0.1)
416
+      actionpack (= 5.0.0.1)
417
+      actionview (= 5.0.0.1)
418
+      activejob (= 5.0.0.1)
419
+      activemodel (= 5.0.0.1)
420
+      activerecord (= 5.0.0.1)
421
+      activesupport (= 5.0.0.1)
418 422
       bundler (>= 1.3.0, < 2.0)
419
-      railties (= 4.2.5.2)
420
-      sprockets-rails
421
-    rails-deprecated_sanitizer (1.0.3)
422
-      activesupport (>= 4.2.0.alpha)
423
-    rails-dom-testing (1.0.7)
424
-      activesupport (>= 4.2.0.beta, < 5.0)
423
+      railties (= 5.0.0.1)
424
+      sprockets-rails (>= 2.0.0)
425
+    rails-controller-testing (1.0.1)
426
+      actionpack (~> 5.x)
427
+      actionview (~> 5.x)
428
+      activesupport (~> 5.x)
429
+    rails-dom-testing (2.0.1)
430
+      activesupport (>= 4.2.0, < 6.0)
425 431
       nokogiri (~> 1.6.0)
426
-      rails-deprecated_sanitizer (>= 1.0.1)
427 432
     rails-html-sanitizer (1.0.3)
428 433
       loofah (~> 2.0)
429
-    rails_12factor (0.0.3)
430
-      rails_serve_static_assets
431
-      rails_stdout_logging
432
-    rails_serve_static_assets (0.0.4)
433
-    rails_stdout_logging (0.0.3)
434
-    railties (4.2.5.2)
435
-      actionpack (= 4.2.5.2)
436
-      activesupport (= 4.2.5.2)
434
+    railties (5.0.0.1)
435
+      actionpack (= 5.0.0.1)
436
+      activesupport (= 5.0.0.1)
437
+      method_source
437 438
       rake (>= 0.8.7)
438 439
       thor (>= 0.18.1, < 2.0)
439
-    raindrops (0.13.0)
440
-    rake (10.5.0)
440
+    raindrops (0.17.0)
441
+    rake (11.2.2)
441 442
     rb-fsevent (0.9.7)
442 443
     rb-inotify (0.9.5)
443 444
       ffi (>= 0.5.0)
445
+    rb-kqueue (0.2.4)
446
+      ffi (>= 0.5.0)
444 447
     ref (2.0.0)
445
-    responders (2.1.1)
448
+    responders (2.3.0)
446 449
       railties (>= 4.2.0, < 5.1)
447 450
     rest-client (1.8.0)
448 451
       http-cookie (>= 1.0.2, < 2.0)
@@ -450,32 +453,32 @@ GEM
450 453
       netrc (~> 0.7)
451 454
     retriable (2.0.2)
452 455
     rr (1.1.2)
453
-    rspec (3.2.0)
454
-      rspec-core (~> 3.2.0)
455
-      rspec-expectations (~> 3.2.0)
456
-      rspec-mocks (~> 3.2.0)
456
+    rspec (3.5.0)
457
+      rspec-core (~> 3.5.0)
458
+      rspec-expectations (~> 3.5.0)
459
+      rspec-mocks (~> 3.5.0)
457 460
     rspec-collection_matchers (1.1.2)
458 461
       rspec-expectations (>= 2.99.0.beta1)
459
-    rspec-core (3.2.1)
460
-      rspec-support (~> 3.2.0)
461
-    rspec-expectations (3.2.0)
462
+    rspec-core (3.5.2)
463
+      rspec-support (~> 3.5.0)
464
+    rspec-expectations (3.5.0)
462 465
       diff-lcs (>= 1.2.0, < 2.0)
463
-      rspec-support (~> 3.2.0)
464
-    rspec-html-matchers (0.7.0)
466
+      rspec-support (~> 3.5.0)
467
+    rspec-html-matchers (0.8.1)
465 468
       nokogiri (~> 1)
466
-      rspec (~> 3)
467
-    rspec-mocks (3.2.1)
469
+      rspec (>= 3.0.0.a, < 4)
470
+    rspec-mocks (3.5.0)
468 471
       diff-lcs (>= 1.2.0, < 2.0)
469
-      rspec-support (~> 3.2.0)
470
-    rspec-rails (3.2.1)
471
-      actionpack (>= 3.0, < 4.3)
472
-      activesupport (>= 3.0, < 4.3)
473
-      railties (>= 3.0, < 4.3)
474
-      rspec-core (~> 3.2.0)
475
-      rspec-expectations (~> 3.2.0)
476
-      rspec-mocks (~> 3.2.0)
477
-      rspec-support (~> 3.2.0)
478
-    rspec-support (3.2.2)
472
+      rspec-support (~> 3.5.0)
473
+    rspec-rails (3.5.2)
474
+      actionpack (>= 3.0)
475
+      activesupport (>= 3.0)
476
+      railties (>= 3.0)
477
+      rspec-core (~> 3.5.0)
478
+      rspec-expectations (~> 3.5.0)
479
+      rspec-mocks (~> 3.5.0)
480
+      rspec-support (~> 3.5.0)
481
+    rspec-support (3.5.0)
479 482
     rturk (2.12.1)
480 483
       erector
481 484
       nokogiri
@@ -486,12 +489,13 @@ GEM
486 489
       tzinfo
487 490
     safe_yaml (1.0.4)
488 491
     sass (3.4.14)
489
-    sass-rails (5.0.3)
490
-      railties (>= 4.0.0, < 5.0)
492
+    sass-rails (5.0.6)
493
+      railties (>= 4.0.0, < 6)
491 494
       sass (~> 3.1)
492 495
       sprockets (>= 2.8, < 4.0)
493 496
       sprockets-rails (>= 2.0, < 4.0)
494
-      tilt (~> 1.1)
497
+      tilt (>= 1.1, < 3)
498
+    sax-machine (1.3.2)
495 499
     select2-rails (3.5.9.3)
496 500
       thor (~> 0.14)
497 501
     shellany (0.0.1)
@@ -502,7 +506,6 @@ GEM
502 506
       faraday (>= 0.9.0.rc5)
503 507
       jwt (>= 0.1.5)
504 508
       multi_json (>= 1.0.0)
505
-    simple-rss (1.3.1)
506 509
     simple_oauth (0.3.1)
507 510
     simplecov (0.9.2)
508 511
       docile (~> 1.1.0)
@@ -513,13 +516,16 @@ GEM
513 516
     slop (3.6.0)
514 517
     spectrum-rails (1.3.4)
515 518
       railties (>= 3.1)
516
-    spring (1.6.3)
519
+    spring (1.7.2)
517 520
     spring-commands-rspec (1.0.4)
518 521
       spring (>= 0.9.1)
519
-    sprockets (3.5.2)
522
+    spring-watcher-listen (2.0.0)
523
+      listen (>= 2.7, < 4.0)
524
+      spring (~> 1.2)
525
+    sprockets (3.7.0)
520 526
       concurrent-ruby (~> 1.0)
521 527
       rack (> 1, < 3)
522
-    sprockets-rails (3.0.3)
528
+    sprockets-rails (3.2.0)
523 529
       actionpack (>= 4.0)
524 530
       activesupport (>= 4.0)
525 531
       sprockets (>= 3.0.0)
@@ -527,7 +533,6 @@ GEM
527 533
       colorize (>= 0.7.0)
528 534
       net-scp (>= 1.1.2)
529 535
       net-ssh (>= 2.8.0)
530
-    string-scrub (0.0.5)
531 536
     systemu (2.6.4)
532 537
     term-ansicolor (1.3.2)
533 538
       tins (~> 1.0)
@@ -536,7 +541,7 @@ GEM
536 541
       ref
537 542
     thor (0.19.1)
538 543
     thread_safe (0.3.5)
539
-    tilt (1.4.1)
544
+    tilt (2.0.5)
540 545
     tins (1.10.1)
541 546
     treetop (1.5.3)
542 547
       polyglot (~> 0.3)
@@ -565,20 +570,24 @@ GEM
565 570
     unf (0.1.4)
566 571
       unf_ext
567 572
     unf_ext (0.0.7.1)
568
-    unicorn (4.9.0)
573
+    unicorn (5.1.0)
569 574
       kgio (~> 2.6)
570
-      rack
571 575
       raindrops (~> 0.7)
572 576
     uuid (2.3.7)
573 577
       macaddr (~> 1.0)
574 578
     uuidtools (2.1.5)
575 579
     vcr (2.9.2)
576
-    warden (1.2.4)
580
+    warden (1.2.6)
577 581
       rack (>= 1.0)
582
+    web-console (3.3.1)
583
+      actionview (>= 5.0)
584
+      activemodel (>= 5.0)
585
+      debug_inspector
586
+      railties (>= 5.0)
578 587
     webmock (1.17.4)
579 588
       addressable (>= 2.2.7)
580 589
       crack (>= 0.3.2)
581
-    websocket-driver (0.6.3)
590
+    websocket-driver (0.6.4)
582 591
       websocket-extensions (>= 0.1.0)
583 592
     websocket-extensions (0.1.2)
584 593
     wunderground (1.2.0)
@@ -603,14 +612,14 @@ DEPENDENCIES
603 612
   capistrano-bundler (~> 1.1.4)
604 613
   capistrano-rails (~> 1.1)
605 614
   capybara-select2
606
-  coffee-rails (~> 4.1.1)
615
+  coffee-rails (~> 4.2)
607 616
   coveralls (~> 0.7.4)
608 617
   daemons (~> 1.1.9)
609 618
   database_cleaner (~> 1.5.3)
610 619
   delayed_job (~> 4.1.0)
611 620
   delayed_job_active_record!
612 621
   delorean
613
-  devise (~> 3.5.4)
622
+  devise (~> 4.2.0)
614 623
   dotenv!
615 624
   dotenv-rails!
616 625
   dropbox-api
@@ -618,13 +627,13 @@ DEPENDENCIES
618 627
   evernote_oauth
619 628
   faraday (~> 0.9.0)
620 629
   faraday_middleware!
621
-  feed-normalizer
630
+  feedjira (~> 2.0)
622 631
   ffi (>= 1.9.4)
623 632
   font-awesome-sass (~> 4.3.2)
624 633
   forecast_io (~> 2.0.0)
625 634
   foreman (~> 0.63.0)
626 635
   geokit (~> 1.8.4)
627
-  geokit-rails (~> 2.0.1)
636
+  geokit-rails (~> 2.2.0)
628 637
   google-api-client
629 638
   guard (~> 2.13.0)
630 639
   guard-livereload (~> 2.5.1)
@@ -635,14 +644,15 @@ DEPENDENCIES
635 644
   httparty (~> 0.13)
636 645
   huginn_agent (~> 0.4.0)
637 646
   hypdf (~> 1.0.10)
638
-  jquery-rails (~> 3.1.3)
647
+  jquery-rails (~> 4.2.1)
639 648
   json (~> 1.8.1)
640
-  jsonpathv2 (~> 0.0.3)
641
-  kaminari (~> 0.16.1)
649
+  jsonpathv2 (~> 0.0.8)
650
+  kaminari!
642 651
   kramdown (~> 1.3.3)
643
-  letter_opener_web
652
+  letter_opener_web (~> 1.3.0)
644 653
   liquid (~> 3.0.3)
645 654
   listen (~> 3.0.5)
655
+  loofah (~> 2.0)
646 656
   mini_magick
647 657
   mixpanel_client
648 658
   mqtt
@@ -650,39 +660,37 @@ DEPENDENCIES
650 660
   mysql2 (~> 0.3.20)
651 661
   net-ftp-list (~> 3.2.8)
652 662
   nokogiri (= 1.6.8)
653
-  omniauth
663
+  omniauth (~> 1.3.1)
654 664
   omniauth-37signals
655 665
   omniauth-dropbox
656 666
   omniauth-evernote
657
-  omniauth-tumblr
658
-  omniauth-twitter
659
-  omniauth-wunderlist!
667
+  omniauth-tumblr (~> 1.2)
668
+  omniauth-twitter (~> 1.2.1)
669
+  omniauth-wunderlist
660 670
   pg (~> 0.18.3)
661 671
   poltergeist
662
-  protected_attributes (~> 1.0.8)
663 672
   pry-byebug
664 673
   pry-rails
665
-  quiet_assets
666
-  rack (> 1.5.0)
667 674
   rack-livereload (~> 0.3.16)
668
-  rails (= 4.2.5.2)
669
-  rails_12factor
675
+  rails (~> 5.0.0.1)
676
+  rails-controller-testing
677
+  rb-kqueue (>= 0.2)
670 678
   rr
671
-  rspec (~> 3.2)
679
+  rspec (~> 3.5)
672 680
   rspec-collection_matchers (~> 1.1.0)
673
-  rspec-html-matchers (~> 0.7)
674
-  rspec-rails (~> 3.1)
681
+  rspec-html-matchers (~> 0.8)
682
+  rspec-rails (~> 3.5.2)
675 683
   rturk (~> 2.12.1)
676 684
   ruby-growl (~> 4.1.0)
677 685
   rufus-scheduler (~> 3.0.8)
678
-  sass-rails (~> 5.0.3)
686
+  sass-rails (~> 5.0.6)
679 687
   select2-rails (~> 3.5.4)
680 688
   shoulda-matchers
681 689
   slack-notifier (~> 1.0.0)
682 690
   spectrum-rails
683
-  spring (~> 1.6.3)
691
+  spring (~> 1.7.2)
684 692
   spring-commands-rspec (~> 1.0.4)
685
-  string-scrub
693
+  spring-watcher-listen (~> 2.0.0)
686 694
   therubyracer (~> 0.12.2)
687 695
   tumblr_client!
688 696
   twilio-ruby (~> 3.11.5)
@@ -692,15 +700,23 @@ DEPENDENCIES
692 700
   tzinfo (>= 1.2.0)
693 701
   tzinfo-data
694 702
   uglifier (~> 2.7.2)
695
-  unicorn (~> 4.9.0)
703
+  unicorn (~> 5.1.0)
696 704
   vcr
705
+  web-console
697 706
   webmock (~> 1.17.4)
698 707
   weibo_2!
699 708
   wunderground (~> 1.2.0)
700 709
   xmpp4r (~> 0.5.6)
701 710
 
702 711
 RUBY VERSION
712
+<<<<<<< HEAD
703 713
    ruby 2.0.0p648
704 714
 
705 715
 BUNDLED WITH
706 716
    1.12.5
717
+=======
718
+   ruby 2.3.1p112
719
+
720
+BUNDLED WITH
721
+   1.13.2
722
+>>>>>>> 0fcd8e285ebe9c04fb6ce5abd5a1f776021bdf89

+ 2 - 2
README.md

@@ -64,7 +64,7 @@ If you just want to play around, you can simply fork this repository, then perfo
64 64
 * Run `git remote add upstream https://github.com/cantino/huginn.git` to add the main repository as a remote for your fork.
65 65
 * Copy `.env.example` to `.env` (`cp .env.example .env`) and edit `.env`, at least updating the `APP_SECRET_TOKEN` variable.
66 66
 * Run `bundle` to install dependencies
67
-* Run `bundle exec rake db:create`, `bundle exec rake db:migrate`, and then `bundle exec rake db:seed` to create a development MySQL database with some example Agents.
67
+* Run `bundle exec rake db:create`, `bundle exec rake db:migrate`, and then `bundle exec rake db:seed` to create a development database with some example Agents.
68 68
 * Run `bundle exec foreman start`, visit [http://localhost:3000/][localhost], and login with the username of `admin` and the password of `password`.
69 69
 * Setup some Agents!
70 70
 * Read the [wiki][wiki] for usage examples and to get started making new Agents.
@@ -133,5 +133,5 @@ We assume your deployment will run over SSL. This is a very good idea! However,
133 133
 
134 134
 Huginn is provided under the MIT License.
135 135
 
136
-[![Build Status](https://travis-ci.org/cantino/huginn.svg)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.svg)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE)
136
+[![Build Status](https://travis-ci.org/cantino/huginn.svg)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.svg)](https://coveralls.io/r/cantino/huginn) [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE)
137 137
 

+ 1 - 1
Rakefile

@@ -2,6 +2,6 @@
2 2
 # Add your own tasks in files placed in lib/tasks ending in .rake,
3 3
 # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 4
 
5
-require File.expand_path('../config/application', __FILE__)
5
+require_relative 'config/application'
6 6
 
7 7
 Huginn::Application.load_tasks

+ 4 - 0
app/assets/javascripts/ace.js.coffee

@@ -2,3 +2,7 @@
2 2
 #= require ace/mode-javascript.js
3 3
 #= require ace/mode-markdown.js
4 4
 #= require ace/mode-coffee.js
5
+#= require ace/mode-sql.js
6
+#= require ace/mode-json.js
7
+#= require ace/mode-yaml.js
8
+#= require ace/mode-text.js

+ 7 - 2
app/assets/javascripts/components/form_configurable.js.coffee

@@ -53,10 +53,13 @@ $ ->
53 53
     updateDropdownData = (form_data, element, data) ->
54 54
       returnedResults[form_data.attribute] = {text: 'Options', children: data}
55 55
       $(element).trigger('change')
56
+      $("input[role~=completable]").off 'select2-opening', select2OpeningCallback
56 57
       $(element).select2('open')
58
+      $("input[role~=completable]").on 'select2-opening', select2OpeningCallback
57 59
 
58
-    $("input[role~=completable]").on 'select2-open', (e) ->
60
+    select2OpeningCallback = (e) ->
59 61
       form_data = getFormData(e.currentTarget)
62
+      delete returnedResults[form_data.attribute] if returnedResults[form_data.attribute] && !$(e.currentTarget).data('cacheResponse')
60 63
       return if returnedResults[form_data.attribute]
61 64
 
62 65
       $.ajax '/agents/complete',
@@ -67,10 +70,12 @@ $ ->
67 70
         error: (data) ->
68 71
           updateDropdownData(form_data, e.currentTarget, [{id: undefined, text: 'Error loading data.'}])
69 72
 
73
+    $("input[role~=completable]").on 'select2-opening', select2OpeningCallback
74
+
70 75
     $("input[type=radio][role~=form-configurable]").change (e) ->
71 76
       input = $(e.currentTarget).parents().siblings("input[data-attribute=#{$(e.currentTarget).data('attribute')}]")
72 77
       if $(e.currentTarget).val() == 'manual'
73 78
         input.removeClass('hidden')
74 79
       else
75 80
         input.val($(e.currentTarget).val())
76
-        input.addClass('hidden')
81
+        input.addClass('hidden')

+ 1 - 1
app/assets/javascripts/components/utils.js.coffee

@@ -69,7 +69,7 @@ class @Utils
69 69
               json = $(e.target).find('.payload-editor').val()
70 70
               json = '{}' if json == ''
71 71
               try
72
-                payload = JSON.parse(json)
72
+                payload = JSON.parse(json.replace(/\\\\([n|r|t])/g, "\\$1"))
73 73
                 throw true unless payload.constructor is Object
74 74
                 if Object.keys(payload).length == 0
75 75
                   json = ''

+ 16 - 8
app/assets/javascripts/pages/agent-edit-page.js.coffee

@@ -36,7 +36,7 @@ class @AgentEditPage
36 36
       @handleTypeChange(true)
37 37
 
38 38
       # Update the dropdown to match agent description as well as agent name
39
-      $('#agent_type').select2
39
+      $('select#agent_type').select2
40 40
         width: 'resolve'
41 41
         formatResult: formatAgentForSelect
42 42
         escapeMarkup: (m) ->
@@ -177,20 +177,28 @@ class @AgentEditPage
177 177
   buildAce: ->
178 178
     $(".ace-editor").each ->
179 179
       unless $(this).data('initialized')
180
-        $(this).data('initialized', true)
181
-        $source = $($(this).data('source')).hide()
180
+        $this = $(this)
181
+        $this.data('initialized', true)
182
+        $source = $($this.data('source')).hide()
182 183
         editor = ace.edit(this)
183
-        $(this).data('ace-editor', editor)
184
+        $this.data('ace-editor', editor)
184 185
         session = editor.getSession()
185 186
         session.setTabSize(2)
186 187
         session.setUseSoftTabs(true)
187 188
         session.setUseWrapMode(false)
188 189
 
189 190
         setSyntax = ->
190
-          switch $("[name='agent[options][language]']").val()
191
-            when 'JavaScript' then session.setMode("ace/mode/javascript")
192
-            when 'CoffeeScript' then session.setMode("ace/mode/coffee")
193
-            else session.setMode("ace/mode/text")
191
+          if mode = $this.data('mode')
192
+            session.setMode("ace/mode/" + mode)
193
+
194
+          if theme = $this.data('theme')
195
+            editor.setTheme("ace/theme/" + theme);
196
+
197
+          if mode = $("[name='agent[options][language]']").val()
198
+            switch mode
199
+              when 'JavaScript' then session.setMode("ace/mode/javascript")
200
+              when 'CoffeeScript' then session.setMode("ace/mode/coffee")
201
+              else session.setMode("ace/mode/" + mode)
194 202
 
195 203
         $("[name='agent[options][language]']").on 'change', setSyntax
196 204
         setSyntax()

+ 11 - 3
app/concerns/file_handling.rb

@@ -5,13 +5,21 @@ module FileHandling
5 5
     { file_pointer: { file: file, agent_id: id } }
6 6
   end
7 7
 
8
+  def has_file_pointer?(event)
9
+    event.payload['file_pointer'] &&
10
+      event.payload['file_pointer']['file'] &&
11
+      event.payload['file_pointer']['agent_id']
12
+  end
13
+
8 14
   def get_io(event)
9
-    return nil unless event.payload['file_pointer'] &&
10
-                      event.payload['file_pointer']['file'] &&
11
-                      event.payload['file_pointer']['agent_id']
15
+    return nil unless has_file_pointer?(event)
12 16
     event.user.agents.find(event.payload['file_pointer']['agent_id']).get_io(event.payload['file_pointer']['file'])
13 17
   end
14 18
 
19
+  def get_upload_io(event)
20
+    Faraday::UploadIO.new(get_io(event), MIME::Types.type_for(File.basename(event.payload['file_pointer']['file'])).first.try(:content_type))
21
+  end
22
+
15 23
   def emitting_file_handling_agent_description
16 24
     @emitting_file_handling_agent_description ||=
17 25
       "This agent only emits a 'file pointer', not the data inside the files, the following agents can consume the created events: `#{receiving_file_handling_agents.join('`, `')}`. Read more about the concept in the [wiki](https://github.com/cantino/huginn/wiki/How-Huginn-works-with-files)."

+ 1 - 1
app/concerns/form_configurable.rb

@@ -32,7 +32,7 @@ module FormConfigurable
32 32
       options = args.extract_options!.reverse_merge(roles: [], type: :string)
33 33
 
34 34
       if args.all? { |arg| arg.is_a?(Symbol) }
35
-        options.assert_valid_keys([:type, :roles, :values, :ace])
35
+        options.assert_valid_keys([:type, :roles, :values, :ace, :cache_response])
36 36
       end
37 37
 
38 38
       if options[:type] == :array && (options[:values].blank? || !options[:values].is_a?(Array))

+ 17 - 6
app/concerns/liquid_droppable.rb

@@ -18,6 +18,11 @@ module LiquidDroppable
18 18
         yield [name, __send__(name)]
19 19
       }
20 20
     end
21
+
22
+    def as_json
23
+      return {} unless defined?(self.class::METHODS)
24
+      Hash[self.class::METHODS.map { |m| [m, send(m).as_json]}]
25
+    end
21 26
   end
22 27
 
23 28
   included do
@@ -33,12 +38,10 @@ module LiquidDroppable
33 38
     self.class::Drop.new(self)
34 39
   end
35 40
 
36
-  class MatchDataDrop < Liquid::Drop
37
-    def initialize(object)
38
-      @object = object
39
-    end
41
+  class MatchDataDrop < Drop
42
+    METHODS = %w[pre_match post_match names size]
40 43
 
41
-    %w[pre_match post_match names size].each { |attr|
44
+    METHODS.each { |attr|
42 45
       define_method(attr) {
43 46
         @object.__send__(attr)
44 47
       }
@@ -64,7 +67,9 @@ module LiquidDroppable
64 67
   require 'uri'
65 68
 
66 69
   class URIDrop < Drop
67
-    URI::Generic::COMPONENT.each { |attr|
70
+    METHODS = URI::Generic::COMPONENT
71
+
72
+    METHODS.each { |attr|
68 73
       define_method(attr) {
69 74
         @object.__send__(attr)
70 75
       }
@@ -76,4 +81,10 @@ module LiquidDroppable
76 81
       URIDrop.new(self)
77 82
     end
78 83
   end
84
+
85
+  class ::ActiveRecord::Associations::CollectionProxy
86
+    def to_liquid
87
+      self.to_a.to_liquid
88
+    end
89
+  end
79 90
 end

+ 26 - 5
app/concerns/liquid_interpolatable.rb

@@ -92,7 +92,9 @@ module LiquidInterpolatable
92 92
 
93 93
   def interpolate_string(string, self_object = nil)
94 94
     interpolate_with(self_object) do
95
-      Liquid::Template.parse(string).render!(interpolation_context)
95
+      catch :as_object do
96
+        Liquid::Template.parse(string).render!(interpolation_context)
97
+      end
96 98
     end
97 99
   end
98 100
 
@@ -128,9 +130,9 @@ module LiquidInterpolatable
128 130
     # fragment.
129 131
     def to_uri(uri, base_uri = nil)
130 132
       if base_uri
131
-        URI(base_uri) + uri.to_s
133
+        Utils.normalize_uri(base_uri) + Utils.normalize_uri(uri.to_s)
132 134
       else
133
-        URI(uri.to_s)
135
+        Utils.normalize_uri(uri.to_s)
134 136
       end
135 137
     rescue URI::Error
136 138
       nil
@@ -149,7 +151,7 @@ module LiquidInterpolatable
149 151
       else
150 152
         url = url.to_s
151 153
         begin
152
-          uri = URI(url)
154
+          uri = Utils.normalize_uri(url)
153 155
         rescue URI::Error
154 156
           return url
155 157
         end
@@ -170,7 +172,7 @@ module LiquidInterpolatable
170 172
             case response.status
171 173
             when 301, 302, 303, 307
172 174
               if location = response['location']
173
-                uri += location
175
+                uri += Utils.normalize_uri(location)
174 176
                 next
175 177
               end
176 178
             end
@@ -225,6 +227,25 @@ module LiquidInterpolatable
225 227
       JSON.dump(input)
226 228
     end
227 229
 
230
+    # Returns a Ruby object
231
+    #
232
+    # It can be used as a JSONPath replacement for Agents that only support Liquid:
233
+    #
234
+    # Event:   {"something": {"nested": {"data": 1}}}
235
+    # Liquid:  {{something.nested | as_object}}
236
+    # Returns: {"data": 1}
237
+    #
238
+    # Splitting up a string with Liquid filters and return the Array:
239
+    #
240
+    # Event:   {"data": "A,B,C"}}
241
+    # Liquid:  {{data | split: ',' | as_object}}
242
+    # Returns: ['A', 'B', 'C']
243
+    #
244
+    # as_object ALWAYS has be the last filter in a Liquid expression!
245
+    def as_object(object)
246
+      throw :as_object, object.as_json
247
+    end
248
+
228 249
     private
229 250
 
230 251
     def logger

+ 0 - 1
app/concerns/oauthable.rb

@@ -3,7 +3,6 @@ module Oauthable
3 3
 
4 4
   included do |base|
5 5
     @valid_oauth_providers = :all
6
-    attr_accessible :service_id
7 6
     validates_presence_of :service_id
8 7
   end
9 8
 

+ 14 - 12
app/concerns/sortable_events.rb

@@ -5,6 +5,8 @@ module SortableEvents
5 5
     validate :validate_events_order
6 6
   end
7 7
 
8
+  EVENTS_ORDER_KEY = 'events_order'.freeze
9
+
8 10
   def description_events_order(*args)
9 11
     self.class.description_events_order(*args)
10 12
   end
@@ -23,9 +25,9 @@ module SortableEvents
23 25
       !can_order_created_events?
24 26
     end
25 27
 
26
-    def description_events_order(events = 'events created in each run')
28
+    def description_events_order(events = 'events created in each run', events_order_key = EVENTS_ORDER_KEY)
27 29
       <<-MD.lstrip
28
-        To specify the order of #{events}, set `events_order` to an array of sort keys, each of which looks like either `expression` or `[expression, type, descending]`, as described as follows:
30
+        To specify the order of #{events}, set `#{events_order_key}` to an array of sort keys, each of which looks like either `expression` or `[expression, type, descending]`, as described as follows:
29 31
 
30 32
         * _expression_ is a Liquid template to generate a string to be used as sort key.
31 33
 
@@ -48,8 +50,8 @@ module SortableEvents
48 50
     self.class.cannot_order_created_events?
49 51
   end
50 52
 
51
-  def events_order
52
-    options['events_order']
53
+  def events_order(key = EVENTS_ORDER_KEY)
54
+    options[key]
53 55
   end
54 56
 
55 57
   module AutomaticSorter
@@ -102,8 +104,8 @@ module SortableEvents
102 104
   }
103 105
   EXPRESSION_TYPES = EXPRESSION_PARSER.keys.freeze
104 106
 
105
-  def validate_events_order
106
-    case order_by = events_order
107
+  def validate_events_order(events_order_key = EVENTS_ORDER_KEY)
108
+    case order_by = events_order(events_order_key)
107 109
     when nil
108 110
     when Array
109 111
       # Each tuple may be either [expression, type, desc] or just
@@ -113,29 +115,29 @@ module SortableEvents
113 115
         when String
114 116
           # ok
115 117
         else
116
-          errors.add(:base, "first element of each events_order tuple must be a Liquid template")
118
+          errors.add(:base, "first element of each #{events_order_key} tuple must be a Liquid template")
117 119
           break
118 120
         end
119 121
         case type
120 122
         when nil, *EXPRESSION_TYPES
121 123
           # ok
122 124
         else
123
-          errors.add(:base, "second element of each events_order tuple must be #{EXPRESSION_TYPES.to_sentence(last_word_connector: ' or ')}")
125
+          errors.add(:base, "second element of each #{events_order_key} tuple must be #{EXPRESSION_TYPES.to_sentence(last_word_connector: ' or ')}")
124 126
           break
125 127
         end
126 128
         if !desc.nil? && boolify(desc).nil?
127
-          errors.add(:base, "third element of each events_order tuple must be a boolean value")
129
+          errors.add(:base, "third element of each #{events_order_key} tuple must be a boolean value")
128 130
           break
129 131
         end
130 132
       end
131 133
     else
132
-      errors.add(:base, "events_order must be an array of arrays")
134
+      errors.add(:base, "#{events_order_key} must be an array of arrays")
133 135
     end
134 136
   end
135 137
 
136 138
   # Sort given events in order specified by the "events_order" option
137
-  def sort_events(events)
138
-    order_by = events_order.presence or
139
+  def sort_events(events, events_order_key = EVENTS_ORDER_KEY)
140
+    order_by = events_order(events_order_key).presence or
139 141
       return events
140 142
 
141 143
     orders = order_by.map { |_, _, desc = false| boolify(desc) }

+ 1 - 0
app/concerns/web_request_concern.rb

@@ -113,6 +113,7 @@ module WebRequestConcern
113 113
       unless boolify(interpolated['disable_redirect_follow'])
114 114
         builder.use FaradayMiddleware::FollowRedirects
115 115
       end
116
+      builder.request :multipart
116 117
       builder.request :url_encoded
117 118
 
118 119
       if boolify(interpolated['disable_url_encoding'])

+ 30 - 9
app/controllers/admin/users_controller.rb

@@ -1,7 +1,7 @@
1 1
 class Admin::UsersController < ApplicationController
2
-  before_action :authenticate_admin!
2
+  before_action :authenticate_admin!, except: [:switch_back]
3 3
 
4
-  before_action :find_user, only: [:edit, :destroy, :update, :deactivate, :activate]
4
+  before_action :find_user, only: [:edit, :destroy, :update, :deactivate, :activate, :switch_to_user]
5 5
 
6 6
   helper_method :resource
7 7
 
@@ -19,10 +19,8 @@ class Admin::UsersController < ApplicationController
19 19
   end
20 20
 
21 21
   def create
22
-    admin = params[:user].delete(:admin)
23
-    @user = User.new(params[:user])
22
+    @user = User.new(user_params)
24 23
     @user.requires_no_invitation_code!
25
-    @user.admin = admin
26 24
 
27 25
     respond_to do |format|
28 26
       if @user.save
@@ -40,10 +38,8 @@ class Admin::UsersController < ApplicationController
40 38
   end
41 39
 
42 40
   def update
43
-    admin = params[:user].delete(:admin)
44
-    params[:user].except!(:password, :password_confirmation) if params[:user][:password].blank?
45
-    @user.assign_attributes(params[:user])
46
-    @user.admin = admin
41
+    params[:user].extract!(:password, :password_confirmation) if params[:user][:password].blank?
42
+    @user.assign_attributes(user_params)
47 43
 
48 44
     respond_to do |format|
49 45
       if @user.save
@@ -83,8 +79,33 @@ class Admin::UsersController < ApplicationController
83 79
     end
84 80
   end
85 81
 
82
+  # allow an admin to sign-in as any other user
83
+
84
+  def switch_to_user
85
+    if current_user != @user
86
+      old_user = current_user
87
+      bypass_sign_in(@user)
88
+      session[:original_admin_user_id] = old_user.id
89
+    end
90
+    redirect_to agents_path
91
+  end
92
+
93
+  def switch_back
94
+    if session[:original_admin_user_id].present?
95
+      bypass_sign_in(User.find(session[:original_admin_user_id]))
96
+      session.delete(:original_admin_user_id)
97
+    else
98
+      redirect_to(root_path, alert: 'You must be an admin acting as a different user to do that.') and return
99
+    end
100
+    redirect_to admin_users_path
101
+  end
102
+
86 103
   private
87 104
 
105
+  def user_params
106
+    params.require(:user).permit(:email, :username, :password, :password_confirmation, :admin)
107
+  end
108
+
88 109
   def find_user
89 110
     @user = User.find(params[:id])
90 111
   end

+ 4 - 2
app/controllers/agents/dry_runs_controller.rb

@@ -8,17 +8,19 @@ module Agents
8 8
                 elsif params[:source_ids]
9 9
                   Event.where(agent_id: current_user.agents.where(id: params[:source_ids]).pluck(:id))
10 10
                        .order("id DESC").limit(5)
11
+                else
12
+                  []
11 13
                 end
12 14
 
13 15
       render layout: false
14 16
     end
15 17
 
16 18
     def create
17
-      attrs = params[:agent] || {}
19
+      attrs = agent_params
18 20
       if agent = current_user.agents.find_by(id: params[:agent_id])
19 21
         # POST /agents/:id/dry_run
20 22
         if attrs.present?
21
-          attrs.merge!(memory: agent.memory)
23
+          attrs = attrs.merge(memory: agent.memory)
22 24
           type = agent.type
23 25
           agent = Agent.build_for_type(type, current_user, attrs)
24 26
         end

+ 11 - 5
app/controllers/agents_controller.rb

@@ -160,7 +160,7 @@ class AgentsController < ApplicationController
160 160
     @agent = current_user.agents.find(params[:id])
161 161
 
162 162
     respond_to do |format|
163
-      if @agent.update_attributes(params[:agent])
163
+      if @agent.update_attributes(agent_params)
164 164
         format.html { redirect_back "'#{@agent.name}' was successfully updated.", return: agents_path }
165 165
         format.json { render json: @agent, status: :ok, location: agent_path(@agent) }
166 166
       else
@@ -196,9 +196,9 @@ class AgentsController < ApplicationController
196 196
     build_agent
197 197
 
198 198
     if @agent.validate_option(params[:attribute])
199
-      render text: 'ok'
199
+      render plain: 'ok'
200 200
     else
201
-      render text: 'error', status: 403
201
+      render plain: 'error', status: 403
202 202
     end
203 203
   end
204 204
 
@@ -208,6 +208,12 @@ class AgentsController < ApplicationController
208 208
     render json: @agent.complete_option(params[:attribute])
209 209
   end
210 210
 
211
+  def destroy_undefined
212
+    current_user.undefined_agents.destroy_all
213
+
214
+    redirect_back "All undefined Agents have been deleted."
215
+  end
216
+
211 217
   protected
212 218
 
213 219
   # Sanitize params[:return] to prevent open redirect attacks, a common security issue.
@@ -220,9 +226,9 @@ class AgentsController < ApplicationController
220 226
   end
221 227
 
222 228
   def build_agent
223
-    @agent = Agent.build_for_type(params[:agent].delete(:type),
229
+    @agent = Agent.build_for_type(agent_params[:type],
224 230
                                   current_user,
225
-                                  params[:agent])
231
+                                  agent_params.except(:type))
226 232
   end
227 233
 
228 234
   def initialize_presenter

+ 23 - 7
app/controllers/application_controller.rb

@@ -6,18 +6,22 @@ class ApplicationController < ActionController::Base
6 6
 
7 7
   helper :all
8 8
 
9
-  def redirect_back(fallback_path, *args)
10
-    redirect_to :back, *args
11
-  rescue ActionController::RedirectBackError
12
-    redirect_to fallback_path, *args
9
+  rescue_from 'ActiveRecord::SubclassNotFound' do
10
+    @undefined_agent_types = current_user.undefined_agent_types
11
+
12
+    render template: 'application/undefined_agents'
13
+  end
14
+
15
+  def redirect_back(fallback_path, **args)
16
+    super(fallback_location: fallback_path, **args)
13 17
   end
14 18
 
15 19
   protected
16 20
 
17 21
   def configure_permitted_parameters
18
-    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:username, :email, :password, :password_confirmation, :remember_me, :invitation_code) }
19
-    devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :username, :email, :password, :remember_me) }
20
-    devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:username, :email, :password, :password_confirmation, :current_password) }
22
+    devise_parameter_sanitizer.permit(:sign_up, keys: [:username, :email, :password, :password_confirmation, :remember_me, :invitation_code])
23
+    devise_parameter_sanitizer.permit(:sign_in, keys: [:login, :username, :email, :password, :remember_me])
24
+    devise_parameter_sanitizer.permit(:account_update, keys: [:username, :email, :password, :password_confirmation, :current_password])
21 25
   end
22 26
 
23 27
   def authenticate_admin!
@@ -60,4 +64,16 @@ class ApplicationController < ActionController::Base
60 64
       @basecamp_agent = current_user.agents.where(type: 'Agents::BasecampAgent').first
61 65
     end
62 66
   end
67
+
68
+  def agent_params
69
+    return {} unless params[:agent]
70
+    @agent_params ||= begin
71
+      options = params[:agent].delete(:options) if params[:agent][:options].present?
72
+      params[:agent].permit(:memory, :name, :type, :schedule, :disabled, :keep_events_for, :propagate_immediately, :drop_pending_events, :service_id,
73
+                            source_ids: [], receiver_ids: [], scenario_ids: [], controller_ids: [], control_target_ids: []).tap do |agent_params|
74
+        agent_params[:options] = options if options
75
+        agent_params[:options].permit! if agent_params[:options].respond_to?(:permit!)
76
+      end
77
+    end
78
+  end
63 79
 end

+ 10 - 1
app/controllers/scenario_imports_controller.rb

@@ -4,7 +4,7 @@ class ScenarioImportsController < ApplicationController
4 4
   end
5 5
 
6 6
   def create
7
-    @scenario_import = ScenarioImport.new(params[:scenario_import])
7
+    @scenario_import = ScenarioImport.new(scenario_import_params)
8 8
     @scenario_import.set_user(current_user)
9 9
 
10 10
     if @scenario_import.valid? && @scenario_import.import_confirmed? && @scenario_import.import
@@ -13,4 +13,13 @@ class ScenarioImportsController < ApplicationController
13 13
       render action: "new"
14 14
     end
15 15
   end
16
+
17
+  private
18
+
19
+  def scenario_import_params
20
+    merges = params[:scenario_import].delete(:merges)
21
+    params.require(:scenario_import).permit(:url, :data, :file, :do_import) do |params|
22
+      params[:merges] = merges
23
+    end
24
+  end
16 25
 end

+ 11 - 2
app/controllers/scenarios_controller.rb

@@ -1,3 +1,5 @@
1
+require 'agents_exporter'
2
+
1 3
 class ScenariosController < ApplicationController
2 4
   include SortableTable
3 5
   skip_before_action :authenticate_user!, only: :export
@@ -69,7 +71,7 @@ class ScenariosController < ApplicationController
69 71
   end
70 72
 
71 73
   def create
72
-    @scenario = current_user.scenarios.build(params[:scenario])
74
+    @scenario = current_user.scenarios.build(scenario_params)
73 75
 
74 76
     respond_to do |format|
75 77
       if @scenario.save
@@ -86,7 +88,7 @@ class ScenariosController < ApplicationController
86 88
     @scenario = current_user.scenarios.find(params[:id])
87 89
 
88 90
     respond_to do |format|
89
-      if @scenario.update_attributes(params[:scenario])
91
+      if @scenario.update_attributes(scenario_params)
90 92
         format.html { redirect_to @scenario, notice: 'This Scenario was successfully updated.' }
91 93
         format.json { head :no_content }
92 94
       else
@@ -115,4 +117,11 @@ class ScenariosController < ApplicationController
115 117
       format.json { head :no_content }
116 118
     end
117 119
   end
120
+
121
+  private
122
+
123
+  def scenario_params
124
+    params.require(:scenario).permit(:name, :description, :public, :source_url,
125
+                                     :tag_fg_color, :tag_bg_color, :icon, agent_ids: [])
126
+  end
118 127
 end

+ 8 - 2
app/controllers/user_credentials_controller.rb

@@ -48,7 +48,7 @@ class UserCredentialsController < ApplicationController
48 48
   end
49 49
 
50 50
   def create
51
-    @user_credential = current_user.user_credentials.build(params[:user_credential])
51
+    @user_credential = current_user.user_credentials.build(user_credential_params)
52 52
 
53 53
     respond_to do |format|
54 54
       if @user_credential.save
@@ -65,7 +65,7 @@ class UserCredentialsController < ApplicationController
65 65
     @user_credential = current_user.user_credentials.find(params[:id])
66 66
 
67 67
     respond_to do |format|
68
-      if @user_credential.update_attributes(params[:user_credential])
68
+      if @user_credential.update_attributes(user_credential_params)
69 69
         format.html { redirect_to user_credentials_path, notice: 'Your credential was successfully updated.' }
70 70
         format.json { head :no_content }
71 71
       else
@@ -84,4 +84,10 @@ class UserCredentialsController < ApplicationController
84 84
       format.json { head :no_content }
85 85
     end
86 86
   end
87
+
88
+  private
89
+
90
+  def user_credential_params
91
+    params.require(:user_credential).permit(:credential_name, :credential_value, :mode)
92
+  end
87 93
 end

+ 5 - 5
app/controllers/web_requests_controller.rb

@@ -27,17 +27,17 @@ class WebRequestsController < ApplicationController
27 27
         content, status, content_type = agent.trigger_web_request(request)
28 28
 
29 29
         if content.is_a?(String)
30
-          render :text => content, :status => status || 200, :content_type => content_type || 'text/plain'
30
+          render plain: content, :status => status || 200, :content_type => content_type || 'text/plain'
31 31
         elsif content.is_a?(Hash)
32 32
           render :json => content, :status => status || 200
33 33
         else
34 34
           head(status || 200)
35 35
         end
36 36
       else
37
-        render :text => "agent not found", :status => 404
37
+        render plain: "agent not found", :status => 404
38 38
       end
39 39
     else
40
-      render :text => "user not found", :status => 404
40
+      render plain: "user not found", :status => 404
41 41
     end
42 42
   end
43 43
 
@@ -50,9 +50,9 @@ class WebRequestsController < ApplicationController
50 50
           agent.trigger_web_request(request)
51 51
         end
52 52
       }
53
-      render :text => "ok"
53
+      render plain: "ok"
54 54
     else
55
-      render :text => "user not found", :status => :not_found
55
+      render plain: "user not found", :status => :not_found
56 56
     end
57 57
   end
58 58
 end

+ 10 - 0
app/helpers/application_helper.rb

@@ -113,4 +113,14 @@ module ApplicationHelper
113 113
 
114 114
     @highlighted_ranges.any? { |range| range.cover?(id) }
115 115
   end
116
+
117
+  def agent_type_to_human(type)
118
+    type.gsub(/^.*::/, '').underscore.humanize.titleize
119
+  end
120
+
121
+  private
122
+
123
+  def user_omniauth_authorize_path(provider)
124
+    send "user_#{provider}_omniauth_authorize_path"
125
+  end
116 126
 end

+ 9 - 8
app/jobs/agent_propagate_job.rb

@@ -6,14 +6,15 @@ class AgentPropagateJob < ActiveJob::Base
6 6
   end
7 7
 
8 8
   def self.can_enqueue?
9
-    if Rails.configuration.active_job.queue_adapter == :delayed_job &&
10
-       Delayed::Job.where(failed_at: nil, queue: 'propagation').count > 0
11
-      return false
12
-    elsif Rails.configuration.active_job.queue_adapter == :resque &&
13
-          (Resque.size('propagation') > 0 ||
14
-           Resque.workers.select { |w| w.job && w.job['queue'] && w.job['queue']['propagation'] }.count > 0)
15
-      return false
9
+    case queue_adapter.class.name # not using class since it would load adapter dependent gems
10
+    when 'ActiveJob::QueueAdapters::DelayedJobAdapter'
11
+      return Delayed::Job.where(failed_at: nil, queue: 'propagation').count == 0
12
+    when 'ActiveJob::QueueAdapters::ResqueAdapter'
13
+      return Resque.size('propagation') == 0 &&
14
+             Resque.workers.select { |w| w.job && w.job['queue'] && w.job['queue']['propagation'] }.count == 0
15
+    else
16
+      raise NotImplementedError, "unsupported adapter: #{queue_adapter}"
16 17
     end
17
-    true
18 18
   end
19
+
19 20
 end

+ 5 - 5
app/models/agent.rb

@@ -24,8 +24,6 @@ class Agent < ActiveRecord::Base
24 24
 
25 25
   EVENT_RETENTION_SCHEDULES = [["Forever", 0], ['1 hour', 1.hour], ['6 hours', 6.hours], ["1 day", 1.day], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n.days] })]
26 26
 
27
-  attr_accessible :options, :memory, :name, :type, :schedule, :controller_ids, :control_target_ids, :disabled, :source_ids, :receiver_ids, :scenario_ids, :keep_events_for, :propagate_immediately, :drop_pending_events
28
-
29 27
   json_serialize :options, :memory
30 28
 
31 29
   validates_presence_of :name, :user
@@ -46,7 +44,7 @@ class Agent < ActiveRecord::Base
46 44
   after_save :possibly_update_event_expirations
47 45
 
48 46
   belongs_to :user, :inverse_of => :agents
49
-  belongs_to :service, :inverse_of => :agents
47
+  belongs_to :service, :inverse_of => :agents, optional: true
50 48
   has_many :events, -> { order("events.id desc") }, :dependent => :delete_all, :inverse_of => :agent
51 49
   has_one  :most_recent_event, -> { order("events.id desc") }, :inverse_of => :agent, :class_name => "Event"
52 50
   has_many :logs,  -> { order("agent_logs.id desc") }, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog"
@@ -445,7 +443,7 @@ class AgentDrop
445 443
     @object.short_type
446 444
   end
447 445
 
448
-  [
446
+  METHODS = [
449 447
     :name,
450 448
     :type,
451 449
     :options,
@@ -458,7 +456,9 @@ class AgentDrop
458 456
     :disabled,
459 457
     :keep_events_for,
460 458
     :propagate_immediately,
461
-  ].each { |attr|
459
+  ]
460
+
461
+  METHODS.each { |attr|
462 462
     define_method(attr) {
463 463
       @object.__send__(attr)
464 464
     } unless method_defined?(attr)

+ 3 - 5
app/models/agent_log.rb

@@ -2,13 +2,11 @@
2 2
 # in Agents' detail pages.  AgentLogs with a `level` of 4 or greater are considered "errors" and automatically update
3 3
 # Agents' `last_error_log_at` column.  These are often used to determine if an Agent is `working?`.
4 4
 class AgentLog < ActiveRecord::Base
5
-  attr_accessible :agent, :inbound_event, :level, :message, :outbound_event
6
-
7 5
   belongs_to :agent
8
-  belongs_to :inbound_event, :class_name => "Event"
9
-  belongs_to :outbound_event, :class_name => "Event"
6
+  belongs_to :inbound_event, :class_name => "Event", optional: true
7
+  belongs_to :outbound_event, :class_name => "Event", optional: true
10 8
 
11
-  validates_presence_of :agent, :message
9
+  validates_presence_of :message
12 10
   validates_numericality_of :level, :only_integer => true, :greater_than_or_equal_to => 0, :less_than => 5
13 11
 
14 12
   before_validation :scrub_message

+ 65 - 3
app/models/agents/data_output_agent.rb

@@ -46,9 +46,14 @@ module Agents
46 46
               "_contents": "tag contents (can be an object for nesting)"
47 47
             }
48 48
 
49
-        # Ordering events in the output
49
+        # Ordering events
50 50
 
51
-        #{description_events_order('events in the output')}
51
+        #{description_events_order('events')}
52
+
53
+        DataOutputAgent will select the last `events_to_show` entries of its received events sorted in the order specified by `events_order`, which is defaulted to the event creation time.
54
+        So, if you have multiple source agents that may create many events in a run, you may want to either increase `events_to_show` to have a larger "window", or specify the `events_order` option to an appropriate value (like `date_published`) so events from various sources are properly mixed in the resulted feed.
55
+
56
+        There is also an option `events_list_order` that only controls the order of events listed in the final output, without attempting to maintain a total order of received events.  It has the same format as `events_order` and is defaulted to `#{Utils.jsonify(DEFAULT_EVENTS_ORDER['events_list_order'])}` so the selected events are listed in reverse order like most popular RSS feeds list their articles.
52 57
 
53 58
         # Liquid Templating
54 59
 
@@ -176,6 +181,60 @@ module Agents
176 181
       interpolated['push_hubs'].presence || []
177 182
     end
178 183
 
184
+    DEFAULT_EVENTS_ORDER = {
185
+      'events_order' => nil,
186
+      'events_list_order' => [["{{_index_}}", "number", true]],
187
+    }
188
+
189
+    def events_order(key = SortableEvents::EVENTS_ORDER_KEY)
190
+      super || DEFAULT_EVENTS_ORDER[key]
191
+    end
192
+
193
+    def latest_events(reload = false)
194
+      received_events = received_events().reorder(id: :asc)
195
+
196
+      events =
197
+        if (event_ids = memory[:event_ids]) &&
198
+           memory[:events_order] == events_order &&
199
+           memory[:events_to_show] >= events_to_show
200
+          received_events.where(id: event_ids).to_a
201
+        else
202
+          memory[:last_event_id] = nil
203
+          reload = true
204
+          []
205
+        end
206
+
207
+      if reload
208
+        memory[:events_order] = events_order
209
+        memory[:events_to_show] = events_to_show
210
+
211
+        new_events =
212
+          if last_event_id = memory[:last_event_id]
213
+            received_events.where(Event.arel_table[:id].gt(last_event_id)).to_a
214
+          else
215
+            source_ids.flat_map { |source_id|
216
+              # dig twice as many events as the number of
217
+              # `events_to_show`
218
+              received_events.where(agent_id: source_id).
219
+                last(2 * events_to_show)
220
+            }.sort_by(&:id)
221
+          end
222
+
223
+        unless new_events.empty?
224
+          memory[:last_event_id] = new_events.last.id
225
+          events.concat(new_events)
226
+        end
227
+      end
228
+
229
+      events = sort_events(events).last(events_to_show)
230
+
231
+      if reload
232
+        memory[:event_ids] = events.map(&:id)
233
+      end
234
+
235
+      events
236
+    end
237
+
179 238
     def receive_web_request(params, method, format)
180 239
       unless interpolated['secrets'].include?(params['secret'])
181 240
         if format =~ /json/
@@ -185,7 +244,7 @@ module Agents
185 244
         end
186 245
       end
187 246
 
188
-      source_events = sort_events(received_events.order(id: :desc).limit(events_to_show).to_a)
247
+      source_events = sort_events(latest_events(), 'events_list_order')
189 248
 
190 249
       interpolation_context.stack do
191 250
         interpolation_context['events'] = source_events
@@ -252,6 +311,9 @@ module Agents
252 311
     def receive(incoming_events)
253 312
       url = feed_url(secret: interpolated['secrets'].first, format: :xml)
254 313
 
314
+      # Reload new events and update cache
315
+      latest_events(true)
316
+
255 317
       push_hubs.each do |hub|
256 318
         push_to_hub(hub, url)
257 319
       end

+ 10 - 10
app/models/agents/email_digest_agent.rb

@@ -7,7 +7,9 @@ module Agents
7 7
     cannot_create_events!
8 8
 
9 9
     description <<-MD
10
-      The Email Digest Agent collects any Events sent to it and sends them all via email when scheduled.
10
+      The Email Digest Agent collects any Events sent to it and sends them all via email when scheduled. The number of
11
+      used events also relies on the `Keep events` option of the emitting Agent, meaning that if events expire before
12
+      this agent is scheduled to run, they will not appear in the email.
11 13
 
12 14
       By default, the will have a `subject` and an optional `headline` before listing the Events.  If the Events'
13 15
       payloads contain a `message`, that will be highlighted, otherwise everything in
@@ -37,18 +39,16 @@ module Agents
37 39
     end
38 40
 
39 41
     def receive(incoming_events)
42
+      self.memory['events'] ||= []
40 43
       incoming_events.each do |event|
41
-        self.memory['queue'] ||= []
42
-        self.memory['queue'] << event.payload
43
-        self.memory['events'] ||= []
44 44
         self.memory['events'] << event.id
45 45
       end
46 46
     end
47 47
 
48 48
     def check
49
-      if self.memory['queue'] && self.memory['queue'].length > 0
50
-        ids = self.memory['events'].join(",")
51
-        groups = self.memory['queue'].map { |payload| present(payload) }
49
+      if self.memory['events'] && self.memory['events'].length > 0
50
+        payloads = received_events.reorder("events.id ASC").where(id: self.memory['events']).pluck(:payload).to_a
51
+        groups = payloads.map { |payload| present(payload) }
52 52
         recipients.each do |recipient|
53 53
           begin
54 54
             SystemMailer.send_message(
@@ -59,13 +59,13 @@ module Agents
59 59
               content_type: interpolated['content_type'],
60 60
               groups: groups
61 61
             ).deliver_now
62
-            log "Sent digest mail to #{recipient} with events [#{ids}]"
62
+
63
+            log "Sent digest mail to #{recipient}"
63 64
           rescue => e
64
-            error("Error sending digest mail to #{recipient} with events [#{ids}]: #{e.message}")
65
+            error("Error sending digest mail to #{recipient}: #{e.message}")
65 66
             raise
66 67
           end
67 68
         end
68
-        self.memory['queue'] = []
69 69
         self.memory['events'] = []
70 70
       end
71 71
     end

+ 2 - 1
app/models/agents/google_calendar_publish_agent.rb

@@ -91,7 +91,8 @@ module Agents
91 91
     end
92 92
 
93 93
     def receive(incoming_events)
94
-     incoming_events.each do |event|
94
+      require 'google_calendar'
95
+      incoming_events.each do |event|
95 96
         calendar = GoogleCalendar.new(interpolate_options(options, event), Rails.logger)
96 97
 
97 98
         calendar_event = JSON.parse(calendar.publish_as(interpolated(event)['calendar_id'], event.payload["message"]).response.body)

+ 2 - 0
app/models/agents/http_status_agent.rb

@@ -1,3 +1,5 @@
1
+require 'time_tracker'
2
+
1 3
 module Agents
2 4
 
3 5
   class HttpStatusAgent < Agent

+ 45 - 28
app/models/agents/post_agent.rb

@@ -1,6 +1,9 @@
1 1
 module Agents
2 2
   class PostAgent < Agent
3 3
     include WebRequestConcern
4
+    include FileHandling
5
+
6
+    consumes_file_pointer!
4 7
 
5 8
     MIME_RE = /\A\w+\/.+\z/
6 9
 
@@ -8,38 +11,44 @@ module Agents
8 11
     no_bulk_receive!
9 12
     default_schedule "never"
10 13
 
11
-    description <<-MD
12
-      A Post Agent receives events from other agents (or runs periodically), merges those events with the [Liquid-interpolated](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) contents of `payload`, and sends the results as POST (or GET) requests to a specified url.  To skip merging in the incoming event, but still send the interpolated payload, set `no_merge` to `true`.
14
+    description do
15
+      <<-MD
16
+        A Post Agent receives events from other agents (or runs periodically), merges those events with the [Liquid-interpolated](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) contents of `payload`, and sends the results as POST (or GET) requests to a specified url.  To skip merging in the incoming event, but still send the interpolated payload, set `no_merge` to `true`.
13 17
 
14
-      The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`).
18
+        The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`).
15 19
 
16
-      The `method` used can be any of `get`, `post`, `put`, `patch`, and `delete`.
20
+        The `method` used can be any of `get`, `post`, `put`, `patch`, and `delete`.
17 21
 
18
-      By default, non-GETs will be sent with form encoding (`application/x-www-form-urlencoded`).
22
+        By default, non-GETs will be sent with form encoding (`application/x-www-form-urlencoded`).
19 23
 
20
-      Change `content_type` to `json` to send JSON instead.
24
+        Change `content_type` to `json` to send JSON instead.
21 25
 
22
-      Change `content_type` to `xml` to send XML, where the name of the root element may be specified using `xml_root`, defaulting to `post`.
26
+        Change `content_type` to `xml` to send XML, where the name of the root element may be specified using `xml_root`, defaulting to `post`.
23 27
 
24
-      When `content_type` contains a [MIME](https://en.wikipedia.org/wiki/Media_type) type, and `payload` is a string, its interpolated value will be sent as a string in the HTTP request's body and the request's `Content-Type` HTTP header will be set to `content_type`. When `payload` is a string `no_merge` has to be set to `true`.
28
+        When `content_type` contains a [MIME](https://en.wikipedia.org/wiki/Media_type) type, and `payload` is a string, its interpolated value will be sent as a string in the HTTP request's body and the request's `Content-Type` HTTP header will be set to `content_type`. When `payload` is a string `no_merge` has to be set to `true`.
25 29
 
26
-      If `emit_events` is set to `true`, the server response will be emitted as an Event and can be fed to a WebsiteAgent for parsing (using its `data_from_event` and `type` options). No data processing
27
-      will be attempted by this Agent, so the Event's "body" value will always be raw text.
28
-      The Event will also have a "headers" hash and a "status" integer value.
29
-      Set `event_headers_style` to one of the following values to normalize the keys of "headers" for downstream agents' convenience:
30
+        If `emit_events` is set to `true`, the server response will be emitted as an Event and can be fed to a WebsiteAgent for parsing (using its `data_from_event` and `type` options). No data processing
31
+        will be attempted by this Agent, so the Event's "body" value will always be raw text.
32
+        The Event will also have a "headers" hash and a "status" integer value.
33
+        Set `event_headers_style` to one of the following values to normalize the keys of "headers" for downstream agents' convenience:
30 34
 
31
-        * `capitalized` (default) - Header names are capitalized; e.g. "Content-Type"
32
-        * `downcased` - Header names are downcased; e.g. "content-type"
33
-        * `snakecased` - Header names are snakecased; e.g. "content_type"
34
-        * `raw` - Backward compatibility option to leave them unmodified from what the underlying HTTP library returns.
35
+          * `capitalized` (default) - Header names are capitalized; e.g. "Content-Type"
36
+          * `downcased` - Header names are downcased; e.g. "content-type"
37
+          * `snakecased` - Header names are snakecased; e.g. "content_type"
38
+          * `raw` - Backward compatibility option to leave them unmodified from what the underlying HTTP library returns.
35 39
 
36
-      Other Options:
40
+        Other Options:
37 41
 
38
-        * `headers` - When present, it should be a hash of headers to send with the request.
39
-        * `basic_auth` - Specify HTTP basic auth parameters: `"username:password"`, or `["username", "password"]`.
40
-        * `disable_ssl_verification` - Set to `true` to disable ssl verification.
41
-        * `user_agent` - A custom User-Agent name (default: "Faraday v#{Faraday::VERSION}").
42
-    MD
42
+          * `headers` - When present, it should be a hash of headers to send with the request.
43
+          * `basic_auth` - Specify HTTP basic auth parameters: `"username:password"`, or `["username", "password"]`.
44
+          * `disable_ssl_verification` - Set to `true` to disable ssl verification.
45
+          * `user_agent` - A custom User-Agent name (default: "Faraday v#{Faraday::VERSION}").
46
+
47
+        #{receiving_file_handling_agent_description}
48
+
49
+        When receiving a `file_pointer` the request will be sent with multipart encoding (`multipart/form-data`) and `content_type` is ignored. `upload_key` can be used to specify the parameter in which the file will be sent, it defaults to `file`.
50
+      MD
51
+    end
43 52
 
44 53
     event_description <<-MD
45 54
       Events look like this:
@@ -125,9 +134,9 @@ module Agents
125 134
         interpolate_with(event) do
126 135
           outgoing = interpolated['payload'].presence || {}
127 136
           if boolify(interpolated['no_merge'])
128
-            handle outgoing, event.payload, headers(interpolated[:headers])
137
+            handle outgoing, event, headers(interpolated[:headers])
129 138
           else
130
-            handle outgoing.merge(event.payload), event.payload, headers(interpolated[:headers])
139
+            handle outgoing.merge(event.payload), event, headers(interpolated[:headers])
131 140
           end
132 141
         end
133 142
       end
@@ -162,8 +171,8 @@ module Agents
162 171
       }
163 172
     end
164 173
 
165
-    def handle(data, payload = {}, headers)
166
-      url = interpolated(payload)[:post_url]
174
+    def handle(data, event = Event.new, headers)
175
+      url = interpolated(event.payload)[:post_url]
167 176
 
168 177
       case method
169 178
       when 'get', 'delete'
@@ -171,13 +180,21 @@ module Agents
171 180
       when 'post', 'put', 'patch'
172 181
         params = nil
173 182
 
174
-        case (content_type = interpolated(payload)['content_type'])
183
+        content_type =
184
+          if has_file_pointer?(event)
185
+            data[interpolated(event.payload)['upload_key'].presence || 'file'] = get_upload_io(event)
186
+            nil
187
+          else
188
+            interpolated(event.payload)['content_type']
189
+          end
190
+
191
+        case content_type
175 192
         when 'json'
176 193
           headers['Content-Type'] = 'application/json; charset=utf-8'
177 194
           body = data.to_json
178 195
         when 'xml'
179 196
           headers['Content-Type'] = 'text/xml; charset=utf-8'
180
-          body = data.to_xml(root: (interpolated(payload)[:xml_root] || 'post'))
197
+          body = data.to_xml(root: (interpolated(event.payload)[:xml_root] || 'post'))
181 198
         when MIME_RE
182 199
           headers['Content-Type'] = content_type
183 200
           body = data.to_s

+ 51 - 47
app/models/agents/pushover_agent.rb

@@ -12,42 +12,41 @@ module Agents
12 12
 
13 13
       **You need a Pushover API Token:** [https://pushover.net/apps/build](https://pushover.net/apps/build)
14 14
 
15
-      **You must provide** a `message` or `text` key that will contain the body of the notification. This can come from an event or be set as a default. Pushover API has a `512` Character Limit including `title`. `message` will be truncated.
16
-
17 15
       * `token`: your application's API token
18 16
       * `user`: the user or group key (not e-mail address).
19 17
       * `expected_receive_period_in_days`:  is maximum number of days that you would expect to pass between events being received by this agent.
20 18
 
21
-      Your event can provide any of the following optional parameters or you can provide defaults:
19
+      The following options are all Liquid templates whose evaluated values will be posted to the Pushover API.  Only the `message` parameter is required, and if it is blank API call is omitted.
20
+
21
+      Pushover API has a `512` Character Limit including `title`.  `message` will be truncated.
22 22
 
23
+      * `message` - your message (required)
23 24
       * `device` - your user's device name to send the message directly to that device, rather than all of the user's devices
24 25
       * `title` or `subject` - your notification's title
25 26
       * `url` - a supplementary URL to show with your message - `512` Character Limit
26 27
       * `url_title` - a title for your supplementary URL, otherwise just the URL is shown - `100` Character Limit
28
+      * `timestamp` - a [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) of your message's date and time to display to the user, rather than the time your message is received by the Pushover API.
27 29
       * `priority` - send as `-1` to always send as a quiet notification, `0` is default, `1` to display as high-priority and bypass the user's quiet hours, or `2` for emergency priority: [Please read Pushover Docs on Emergency Priority](https://pushover.net/api#priority)
28 30
       * `sound` - the name of one of the sounds supported by device clients to override the user's default sound choice. [See PushOver docs for sound options.](https://pushover.net/api#sounds)
29 31
       * `retry` - Required for emergency priority - Specifies how often (in seconds) the Pushover servers will send the same notification to the user. Minimum value: `30`
30 32
       * `expire` - Required for emergency priority - Specifies how many seconds your notification will continue to be retried for (every retry seconds). Maximum value: `86400`
31 33
 
32
-      Your event can also pass along a timestamp parameter:
33
-
34
-      * `timestamp` - a [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) of your message's date and time to display to the user, rather than the time your message is received by the Pushover API.
35
-
36 34
     MD
37 35
 
38 36
     def default_options
39 37
       {
40 38
         'token' => '',
41 39
         'user' => '',
42
-        'message' => 'a default message',
43
-        'device' => '',
44
-        'title' => '',
45
-        'url' => '',
46
-        'url_title' => '',
47
-        'priority' => '0',
48
-        'sound' => 'pushover',
49
-        'retry' => '0',
50
-        'expire' => '0',
40
+        'message' => '{{ message }}',
41
+        'device' => '{{ device }}',
42
+        'title' => '{{ title }}',
43
+        'url' => '{{ url }}',
44
+        'url_title' => '{{ url_title }}',
45
+        'priority' => '{{ priority }}',
46
+        'timestamp' => '{{ timestamp }}',
47
+        'sound' => '{{ sound }}',
48
+        'retry' => '{{ retry }}',
49
+        'expire' => '{{ expire }}',
51 50
         'expected_receive_period_in_days' => '1'
52 51
       }
53 52
     end
@@ -60,38 +59,43 @@ module Agents
60 59
 
61 60
     def receive(incoming_events)
62 61
       incoming_events.each do |event|
63
-        payload_interpolated = interpolated(event)
64
-        message = (event.payload['message'].presence || event.payload['text'].presence || payload_interpolated['message']).to_s
65
-        if message.present?
66
-          post_params = {
67
-            'token' => payload_interpolated['token'],
68
-            'user' => payload_interpolated['user'],
69
-            'message' => message
70
-          }
71
-
72
-          post_params['device'] = event.payload['device'].presence || payload_interpolated['device']
73
-          post_params['title'] = event.payload['title'].presence || event.payload['subject'].presence || payload_interpolated['title']
74
-
75
-          url = (event.payload['url'].presence || payload_interpolated['url'] || '').to_s
76
-          url = url.slice 0..512
77
-          post_params['url'] = url
78
-
79
-          url_title = (event.payload['url_title'].presence || payload_interpolated['url_title']).to_s
80
-          url_title = url_title.slice 0..100
81
-          post_params['url_title'] = url_title
82
-
83
-          post_params['priority'] = (event.payload['priority'].presence || payload_interpolated['priority']).to_i
84
-
85
-          if event.payload.has_key? 'timestamp'
86
-            post_params['timestamp'] = (event.payload['timestamp']).to_s
62
+        interpolate_with(event) do
63
+          post_params = {}
64
+
65
+          # required parameters
66
+          %w[
67
+            token
68
+            user
69
+            message
70
+          ].all? { |key|
71
+            if value = String.try_convert(interpolated[key].presence)
72
+              post_params[key] = value
73
+            end
74
+          } or next
75
+
76
+          # optional parameters
77
+          %w[
78
+            device
79
+            title
80
+            url
81
+            url_title
82
+            priority
83
+            timestamp
84
+            sound
85
+            retry
86
+            expire
87
+          ].each do |key|
88
+            if value = String.try_convert(interpolated[key].presence)
89
+              case key
90
+              when 'url'
91
+                value.slice!(512..-1)
92
+              when 'url_title'
93
+                value.slice!(100..-1)
94
+              end
95
+              post_params[key] = value
96
+            end
87 97
           end
88 98
 
89
-          post_params['sound'] = (event.payload['sound'].presence || payload_interpolated['sound']).to_s
90
-
91
-          post_params['retry'] = (event.payload['retry'].presence || payload_interpolated['retry']).to_i
92
-
93
-          post_params['expire'] = (event.payload['expire'].presence || payload_interpolated['expire']).to_i
94
-
95 99
           send_notification(post_params)
96 100
         end
97 101
       end
@@ -102,7 +106,7 @@ module Agents
102 106
     end
103 107
 
104 108
     def send_notification(post_params)
105
-      response = HTTParty.post(API_URL, :query => post_params)
109
+      response = HTTParty.post(API_URL, query: post_params)
106 110
       puts response
107 111
     end
108 112
   end

+ 107 - 31
app/models/agents/rss_agent.rb

@@ -1,6 +1,3 @@
1
-require 'rss'
2
-require 'feed-normalizer'
3
-
4 1
 module Agents
5 2
   class RssAgent < Agent
6 3
     include WebRequestConcern
@@ -9,21 +6,23 @@ module Agents
9 6
     can_dry_run!
10 7
     default_schedule "every_1d"
11 8
 
9
+    gem_dependency_check { defined?(Feedjira::Feed) }
10
+
12 11
     DEFAULT_EVENTS_ORDER = [['{{date_published}}', 'time'], ['{{last_updated}}', 'time']]
13 12
 
14 13
     description do
15 14
       <<-MD
16 15
         The RSS Agent consumes RSS feeds and emits events when they change.
17 16
 
18
-        This Agent is fairly simple, using [feed-normalizer](https://github.com/aasmith/feed-normalizer) as a base.  For complex feeds
19
-        with additional field types, we recommend using a WebsiteAgent.  See [this example](https://github.com/cantino/huginn/wiki/Agent-configuration-examples#itunes-trailers).
17
+        This agent, using [Feedjira](https://github.com/feedjira/feedjira) as a base, can parse various types of RSS and Atom feeds and has some special handlers for FeedBurner, iTunes RSS, and so on.  However, supported fields are limited by its general and abstract nature.  For complex feeds with additional field types, we recommend using a WebsiteAgent.  See [this example](https://github.com/cantino/huginn/wiki/Agent-configuration-examples#itunes-trailers).
20 18
 
21 19
         If you want to *output* an RSS feed, use the DataOutputAgent.
22 20
 
23 21
         Options:
24 22
 
25 23
           * `url` - The URL of the RSS feed (an array of URLs can also be used; items with identical guids across feeds will be considered duplicates).
26
-          * `clean` - Attempt to use [feed-normalizer](https://github.com/aasmith/feed-normalizer)'s' `clean!` method to cleanup HTML in the feed.  Set to `true` to use.
24
+          * `include_feed_info` - Set to `true` to include feed information in each event.
25
+          * `clean` - Set to `true` to sanitize `description` and `content` as HTML fragments, removing unknown/unsafe elements and attributes.
27 26
           * `expected_update_period_in_days` - How often you expect this RSS feed to change.  If more than this amount of time passes without an update, the Agent will mark itself as not working.
28 27
           * `headers` - When present, it should be a hash of headers to send with the request.
29 28
           * `basic_auth` - Specify HTTP basic auth parameters: `"username:password"`, or `["username", "password"]`.
@@ -53,18 +52,46 @@ module Agents
53 52
       Events look like:
54 53
 
55 54
           {
55
+            "feed": {
56
+              "id": "...",
57
+              "type": "atom",
58
+              "generator": "...",
59
+              "url": "http://example.com/",
60
+              "links": [
61
+                { "href": "http://example.com/", "rel": "alternate", "type": "text/html" },
62
+                { "href": "http://example.com/index.atom", "rel": "self", "type": "application/atom+xml" }
63
+              ],
64
+              "title": "Some site title",
65
+              "description": "Some site description",
66
+              "copyright": "...",
67
+              "icon": "http://example.com/icon.png",
68
+              "authors": [ "..." ],
69
+              "date_published": "2014-09-11T01:30:00-07:00",
70
+              "last_updated": "2014-09-11T01:30:00-07:00"
71
+            },
56 72
             "id": "829f845279611d7925146725317b868d",
57
-            "date_published": "2014-09-11 01:30:00 -0700",
58
-            "last_updated": "Thu, 11 Sep 2014 01:30:00 -0700",
59 73
             "url": "http://example.com/...",
60 74
             "urls": [ "http://example.com/..." ],
75
+            "links": [
76
+              { "href": "http://example.com/...", "rel": "alternate" },
77
+            ],
78
+            "title": "Some title",
61 79
             "description": "Some description",
62 80
             "content": "Some content",
63
-            "title": "Some title",
64
-            "authors": [ ... ],
65
-            "categories": [ ... ]
81
+            "authors": [ "Some Author <email@address>" ],
82
+            "categories": [ "..." ],
83
+            "enclosure": {
84
+              "url" => "http://example.com/file.mp3", "type" => "audio/mpeg", "length" => "123456789"
85
+            },
86
+            "date_published": "2014-09-11T01:30:00-0700",
87
+            "last_updated": "2014-09-11T01:30:00-0700"
66 88
           }
67 89
 
90
+      Some notes:
91
+
92
+      - The `feed` key is present only if `include_feed_info` is set to true.
93
+      - Each element in `authors` is a string normalized in the format "*name* <*email*> (*url*)", where each space-separated part is optional.
94
+      - Timestamps are converted to the ISO 8601 format.
68 95
     MD
69 96
 
70 97
     def working?
@@ -82,8 +109,12 @@ module Agents
82 109
       validate_events_order
83 110
     end
84 111
 
85
-    def events_order
86
-      super.presence || DEFAULT_EVENTS_ORDER
112
+    def events_order(key = SortableEvents::EVENTS_ORDER_KEY)
113
+      if key == SortableEvents::EVENTS_ORDER_KEY
114
+        super.presence || DEFAULT_EVENTS_ORDER
115
+      else
116
+        raise ArgumentError, "unsupported key: #{key}"
117
+      end
87 118
     end
88 119
 
89 120
     def check
@@ -100,8 +131,7 @@ module Agents
100 131
         begin
101 132
           response = faraday.get(url)
102 133
           if response.success?
103
-            feed = FeedNormalizer::FeedNormalizer.parse(response.body, loose: true)
104
-            feed.clean! if boolify(interpolated['clean'])
134
+            feed = Feedjira::Feed.parse(response.body)
105 135
             new_events.concat feed_to_events(feed)
106 136
           else
107 137
             error "Failed to fetch #{url}: #{response.inspect}"
@@ -124,10 +154,6 @@ module Agents
124 154
       log "Fetched #{urls.to_sentence} and created #{created_event_count} event(s)."
125 155
     end
126 156
 
127
-    def get_entry_id(entry)
128
-      entry.id.presence || Digest::MD5.hexdigest(entry.content)
129
-    end
130
-
131 157
     def check_and_track(entry_id)
132 158
       memory['seen_ids'] ||= []
133 159
       if memory['seen_ids'].include?(entry_id)
@@ -139,21 +165,71 @@ module Agents
139 165
       end
140 166
     end
141 167
 
168
+    unless dependencies_missing?
169
+      require 'feedjira_extension'
170
+    end
171
+
172
+    def feed_data(feed)
173
+      type =
174
+        case feed.class.name
175
+        when /Atom/
176
+          'atom'
177
+        else
178
+          'rss'
179
+        end
180
+
181
+      {
182
+        id: feed.feed_id,
183
+        type: type,
184
+        url: feed.url,
185
+        links: feed.links,
186
+        title: feed.title,
187
+        description: feed.description,
188
+        copyright: feed.copyright,
189
+        generator: feed.generator,
190
+        icon: feed.icon,
191
+        authors: feed.authors,
192
+        date_published: feed.date_published,
193
+        last_updated: feed.last_updated,
194
+      }
195
+    end
196
+
197
+    def entry_data(entry)
198
+      {
199
+        id: entry.id,
200
+        url: entry.url,
201
+        urls: entry.links.map(&:href),
202
+        links: entry.links,
203
+        title: entry.title,
204
+        description: clean_fragment(entry.summary),
205
+        content: clean_fragment(entry.content || entry.summary),
206
+        image: entry.try(:image),
207
+        enclosure: entry.enclosure,
208
+        authors: entry.authors,
209
+        categories: Array(entry.try(:categories)),
210
+        date_published: entry.date_published,
211
+        last_updated: entry.last_updated,
212
+      }
213
+    end
214
+
142 215
     def feed_to_events(feed)
216
+      payload_base = {}
217
+
218
+      if boolify(interpolated['include_feed_info'])
219
+        payload_base[:feed] = feed_data(feed)
220
+      end
221
+
143 222
       feed.entries.map { |entry|
144
-        Event.new(payload: {
145
-                    id: get_entry_id(entry),
146
-                    date_published: entry.date_published,
147
-                    last_updated: entry.last_updated,
148
-                    url: entry.url,
149
-                    urls: entry.urls,
150
-                    description: entry.description,
151
-                    content: entry.content,
152
-                    title: entry.title,
153
-                    authors: entry.authors,
154
-                    categories: entry.categories
155
-                  })
223
+        Event.new(payload: payload_base.merge(entry_data(entry)))
156 224
       }
157 225
     end
226
+
227
+    def clean_fragment(fragment)
228
+      if boolify(interpolated['clean']) && fragment.present?
229
+        Loofah.scrub_fragment(fragment, :prune).to_s
230
+      else
231
+        fragment
232
+      end
233
+    end
158 234
   end
159 235
 end

+ 1 - 1
app/models/agents/trigger_agent.rb

@@ -8,7 +8,7 @@ module Agents
8 8
     description <<-MD
9 9
       The Trigger Agent will watch for a specific value in an Event payload.
10 10
 
11
-      The `rules` array contains hashes of `path`, `value`, and `type`.  The `path` value is a dotted path through a hash in [JSONPaths](http://goessner.net/articles/JsonPath/) syntax.
11
+      The `rules` array contains hashes of `path`, `value`, and `type`.  The `path` value is a dotted path through a hash in [JSONPaths](http://goessner.net/articles/JsonPath/) syntax. For simple events, this is usually just the name of the field you want, like 'text' for the text key of the event.
12 12
 
13 13
       The `type` can be one of #{VALID_COMPARISON_TYPES.map { |t| "`#{t}`" }.to_sentence} and compares with the `value`.  Note that regex patterns are matched case insensitively.  If you want case sensitive matching, prefix your pattern with `(?-i)`.
14 14
 

+ 10 - 1
app/models/agents/twitter_action_agent.rb

@@ -16,6 +16,7 @@ module Agents
16 16
       Set `expected_receive_period_in_days` to the maximum amount of time that you'd expect to pass between Events being received by this Agent.
17 17
       Set `retweet` to either true or false.
18 18
       Set `favorite` to either true or false.
19
+      Set `emit_error_events` to true to emit an Event when the action failed, otherwise the action will be retried.
19 20
     MD
20 21
 
21 22
     def validate_options
@@ -25,6 +26,9 @@ module Agents
25 26
       unless retweet? || favorite?
26 27
         errors.add(:base, "at least one action must be true")
27 28
       end
29
+      if emit_error_events?.nil?
30
+        errors.add(:base, "emit_error_events must be set to 'true' or 'false'")
31
+      end
28 32
     end
29 33
 
30 34
     def working?
@@ -36,6 +40,7 @@ module Agents
36 40
         'expected_receive_period_in_days' => '2',
37 41
         'favorite' => 'false',
38 42
         'retweet' => 'true',
43
+        'emit_error_events' => 'false'
39 44
       }
40 45
     end
41 46
 
@@ -47,6 +52,10 @@ module Agents
47 52
       boolify(options['favorite'])
48 53
     end
49 54
 
55
+    def emit_error_events?
56
+      boolify(options['emit_error_events'])
57
+    end
58
+
50 59
     def receive(incoming_events)
51 60
       tweets = tweets_from_events(incoming_events)
52 61
 
@@ -54,6 +63,7 @@ module Agents
54 63
         twitter.favorite(tweets) if favorite?
55 64
         twitter.retweet(tweets) if retweet?
56 65
       rescue Twitter::Error => e
66
+        raise e unless emit_error_events?
57 67
         create_event :payload => {
58 68
           'success' => false,
59 69
           'error' => e.message,
@@ -71,4 +81,3 @@ module Agents
71 81
     end
72 82
   end
73 83
 end
74
-

+ 29 - 15
app/models/agents/weather_agent.rb

@@ -14,17 +14,20 @@ module Agents
14 14
 
15 15
       You also must select `which_day` you would like to get the weather for where the number 0 is for today and 1 is for tomorrow and so on. Weather is only returned for 1 week at a time.
16 16
 
17
-      The weather can be provided by either Wunderground or ForecastIO. To choose which `service` to use, enter either `forecastio` or `wunderground`.
17
+      The weather can be provided by either Wunderground or Dark Sky. To choose which `service` to use, enter either `darksky` or `wunderground`.
18 18
 
19
-      The `location` can be a US zipcode, or any location that Wunderground supports. To find one, search [wunderground.com](http://wunderground.com) and copy the location part of the URL.  For example, a result for San Francisco gives `http://www.wunderground.com/US/CA/San_Francisco.html` and London, England gives `http://www.wunderground.com/q/zmw:00000.1.03772`.  The locations in each are `US/CA/San_Francisco` and `zmw:00000.1.03772`, respectively.
19
+      The `location` should be:
20 20
 
21
-      If you plan on using ForecastIO, the `location` must be a comma-separated string of co-ordinates (longitude, latitude). For example, San Francisco would be `37.7771,-122.4196`.
21
+      * For Wunderground: A US zipcode, or any location that Wunderground supports. To find one, search [wunderground.com](http://wunderground.com) and copy the location part of the URL.  For example, a result for San Francisco gives `http://www.wunderground.com/US/CA/San_Francisco.html` and London, England gives `http://www.wunderground.com/q/zmw:00000.1.03772`.  The locations in each are `US/CA/San_Francisco` and `zmw:00000.1.03772`, respectively.
22
+      * For Dark Sky: `location` must be a comma-separated string of co-ordinates (longitude, latitude). For example, San Francisco would be `37.7771,-122.4196`.
22 23
 
23 24
       You must setup an [API key for Wunderground](http://www.wunderground.com/weather/api/) in order to use this Agent with Wunderground.
24 25
 
25
-      You must setup an [API key for Forecast](https://developer.forecast.io/) in order to use this Agent with ForecastIO.
26
+      You must setup an [API key for Dark Sky](https://darksky.net/dev/) in order to use this Agent with Dark Sky.
26 27
 
27 28
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
29
+
30
+      If you want to see the returned texts in your language, then set the `language` parameter in ISO 639-1 format.
28 31
     MD
29 32
 
30 33
     event_description <<-MD
@@ -55,11 +58,11 @@ module Agents
55 58
     default_schedule "8pm"
56 59
 
57 60
     def working?
58
-      event_created_within?((interpolated['expected_update_period_in_days'].presence || 2).to_i) && !recent_error_logs?
61
+      event_created_within?((interpolated['expected_update_period_in_days'].presence || 2).to_i) && !recent_error_logs? && key_setup?
59 62
     end
60 63
 
61 64
     def key_setup?
62
-      interpolated['api_key'].present? && interpolated['api_key'] != "your-key"
65
+      interpolated['api_key'].present? && interpolated['api_key'] != "your-key" && interpolated['api_key'] != "put-your-key-here"
63 66
     end
64 67
 
65 68
     def default_options
@@ -68,10 +71,11 @@ module Agents
68 71
         'api_key' => 'your-key',
69 72
         'location' => '94103',
70 73
         'which_day' => '1',
74
+        'language' => 'EN',
71 75
         'expected_update_period_in_days' => '2'
72 76
       }
73 77
     end
74
-    
78
+
75 79
     def check
76 80
       if key_setup?
77 81
         create_event :payload => model(weather_provider, which_day).merge('location' => location)
@@ -79,7 +83,7 @@ module Agents
79 83
     end
80 84
 
81 85
     private
82
-    
86
+
83 87
     def weather_provider
84 88
       interpolated["service"].presence || "wunderground"
85 89
     end
@@ -92,30 +96,40 @@ module Agents
92 96
       interpolated["location"].presence || interpolated["zipcode"]
93 97
     end
94 98
 
99
+    def language
100
+      interpolated['language'].presence || 'EN'
101
+    end
102
+
95 103
     def validate_options
96
-      errors.add(:base, "service must be set to 'forecastio' or 'wunderground'") unless ["forecastio", "wunderground"].include?(weather_provider)
104
+      errors.add(:base, "service must be set to 'darksky' or 'wunderground'") unless %w[darksky forecastio wunderground].include?(weather_provider)
97 105
       errors.add(:base, "location is required") unless location.present?
98
-      errors.add(:base, "api_key is required") unless key_setup?
106
+      errors.add(:base, "api_key is required") unless interpolated['api_key'].present?
99 107
       errors.add(:base, "which_day selection is required") unless which_day.present?
100 108
     end
101 109
 
102 110
     def wunderground
103
-      Wunderground.new(interpolated['api_key']).forecast_for(location)['forecast']['simpleforecast']['forecastday'] if key_setup?
111
+      if key_setup?
112
+        forecast = Wunderground.new(interpolated['api_key'], language: language.upcase).forecast_for(location)
113
+        merged = {}
114
+        forecast['forecast']['simpleforecast']['forecastday'].each { |daily| merged[daily['period']] = daily }
115
+        forecast['forecast']['txt_forecast']['forecastday'].each { |daily| (merged[daily['period']] || {}).merge!(daily) }
116
+        merged
117
+      end
104 118
     end
105 119
 
106
-    def forecastio
120
+    def dark_sky
107 121
       if key_setup?
108 122
         ForecastIO.api_key = interpolated['api_key']
109 123
         lat, lng = location.split(',')
110
-        ForecastIO.forecast(lat,lng)['daily']['data']
124
+        ForecastIO.forecast(lat, lng, params: {lang: language.downcase})['daily']['data']
111 125
       end
112 126
     end
113 127
 
114 128
     def model(weather_provider,which_day)
115 129
       if weather_provider == "wunderground"
116 130
         wunderground[which_day]
117
-      elsif weather_provider == "forecastio"
118
-        forecastio.each do |value|
131
+      elsif weather_provider == "darksky" || weather_provider == "forecastio"
132
+        dark_sky.each do |value|
119 133
           timestamp = Time.at(value.time)
120 134
           if (timestamp.to_date - Time.now.to_date).to_i == which_day
121 135
             day = {

+ 17 - 4
app/models/agents/webhook_agent.rb

@@ -28,6 +28,7 @@ module Agents
28 28
           For example, "post,get" will enable POST and GET requests. Defaults
29 29
           to "post".
30 30
         * `response` - The response message to the request. Defaults to 'Event Created'.
31
+        * `code` - The response code to the request. Defaults to '201'.
31 32
         * `recaptcha_secret` - Setting this to a reCAPTCHA "secret" key makes your agent verify incoming requests with reCAPTCHA.  Don't forget to embed a reCAPTCHA snippet including your "site" key in the originating form(s).
32 33
         * `recaptcha_send_remote_addr` - Set this to true if your server is properly configured to set REMOTE_ADDR to the IP address of each visitor (instead of that of a proxy server).
33 34
       MD
@@ -53,8 +54,16 @@ module Agents
53 54
       return ["Not Authorized", 401] unless secret == options['secret']
54 55
 
55 56
       # check the verbs
57
+<<<<<<< HEAD
56 58
       # verbs = (interpolated['verbs'] || 'post').split(/,/).map { |x| x.strip.downcase }.select { |x| x.present? }
57 59
       # return ["Please use #{verbs.join('/').upcase} requests only", 401] unless verbs.include?(method)
60
+=======
61
+      verbs = (interpolated['verbs'] || 'post').split(/,/).map { |x| x.strip.downcase }.select { |x| x.present? }
62
+      return ["Please use #{verbs.join('/').upcase} requests only", 401] unless verbs.include?(method)
63
+      
64
+      # check the code
65
+      code = (interpolated['code'].presence || 201).to_i
66
+>>>>>>> 0fcd8e285ebe9c04fb6ce5abd5a1f776021bdf89
58 67
 
59 68
       # check the reCAPTCHA response if required
60 69
       if recaptcha_secret = interpolated['recaptcha_secret'].presence
@@ -86,7 +95,11 @@ module Agents
86 95
         create_event(payload: payload)
87 96
       end
88 97
 
98
+<<<<<<< HEAD
89 99
       [response_message, 200]
100
+=======
101
+      [interpolated(params)['response'] || 'Event Created', code]
102
+>>>>>>> 0fcd8e285ebe9c04fb6ce5abd5a1f776021bdf89
90 103
     end
91 104
 
92 105
     def working?
@@ -97,14 +110,14 @@ module Agents
97 110
       unless options['secret'].present?
98 111
         errors.add(:base, "Must specify a secret for 'Authenticating' requests")
99 112
       end
113
+
114
+      if options['code'].present? && options['code'].to_s !~ /\A\s*(\d+|\{.*)\s*\z/
115
+        errors.add(:base, "Must specify a code for request responses")
116
+      end
100 117
     end
101 118
 
102 119
     def payload_for(params)
103 120
       Utils.value_at(params, interpolated['payload_path']) || {}
104 121
     end
105
-
106
-    def response_message
107
-      interpolated['response'] || 'Event Created'
108
-    end
109 122
   end
110 123
 end

+ 1 - 1
app/models/agents/website_agent.rb

@@ -443,7 +443,7 @@ module Agents
443 443
     end
444 444
 
445 445
     def use_namespaces?
446
-      if value = interpolated.key?('use_namespaces')
446
+      if interpolated.key?('use_namespaces')
447 447
         boolify(interpolated['use_namespaces'])
448 448
       else
449 449
         interpolated['extract'].none? { |name, extraction_details|

+ 0 - 15
app/models/contact.rb

@@ -1,15 +0,0 @@
1
-# Contacts are used only for the contact form on the Huginn website.  If you host a public Huginn instance, you can use
2
-# these to receive messages from visitors.
3
-
4
-class Contact < ActiveRecord::Base
5
-  attr_accessible :email, :message, :name
6
-
7
-  validates_format_of :email, :with => /\A[A-Z0-9._%+-]+@[A-Z0-9.-]+\.(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|asia|jobs|museum)\Z/i
8
-  validates_presence_of :name, :message
9
-
10
-  after_create :send_contact
11
-
12
-  def send_contact
13
-    ContactMailer.send_contact(self).deliver
14
-  end
15
-end

+ 0 - 2
app/models/control_link.rb

@@ -1,7 +1,5 @@
1 1
 # A ControlLink connects Agents in a control flow from the `controller` to the `control_target`.
2 2
 class ControlLink < ActiveRecord::Base
3
-  attr_accessible :controller_id, :target_id
4
-
5 3
   belongs_to :controller, class_name: 'Agent', inverse_of: :control_links_as_controller
6 4
   belongs_to :control_target, class_name: 'Agent', inverse_of: :control_links_as_control_target
7 5
 end

+ 5 - 3
app/models/event.rb

@@ -7,13 +7,11 @@ class Event < ActiveRecord::Base
7 7
   include JSONSerializedField
8 8
   include LiquidDroppable
9 9
 
10
-  attr_accessible :lat, :lng, :location, :payload, :user_id, :user, :expires_at
11
-
12 10
   acts_as_mappable
13 11
 
14 12
   json_serialize :payload
15 13
 
16
-  belongs_to :user
14
+  belongs_to :user, optional: true
17 15
   belongs_to :agent, :counter_cache => true
18 16
 
19 17
   has_many :agent_logs_as_inbound_event, :class_name => "AgentLog", :foreign_key => :inbound_event_id, :dependent => :nullify
@@ -121,4 +119,8 @@ class EventDrop
121 119
   def _location_
122 120
     @object.location
123 121
   end
122
+
123
+  def as_json
124
+    {location: _location_.as_json, agent: @object.agent.to_liquid.as_json, payload: @payload.as_json, created_at: created_at.as_json}
125
+  end
124 126
 end

+ 0 - 2
app/models/link.rb

@@ -1,7 +1,5 @@
1 1
 # A Link connects Agents in a directed Event flow from the `source` to the `receiver`.
2 2
 class Link < ActiveRecord::Base
3
-  attr_accessible :source_id, :receiver_id
4
-
5 3
   belongs_to :source, :class_name => "Agent", :inverse_of => :links_as_source
6 4
   belongs_to :receiver, :class_name => "Agent", :inverse_of => :links_as_receiver
7 5
 

+ 0 - 3
app/models/scenario.rb

@@ -1,9 +1,6 @@
1 1
 class Scenario < ActiveRecord::Base
2 2
   include HasGuid
3 3
 
4
-  attr_accessible :name, :agent_ids, :description, :public, :source_url,
5
-                  :tag_fg_color, :tag_bg_color, :icon
6
-
7 4
   belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios
8 5
   has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario
9 6
   has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios

+ 21 - 12
app/models/service.rb

@@ -1,6 +1,4 @@
1 1
 class Service < ActiveRecord::Base
2
-  attr_accessible :provider, :name, :token, :secret, :refresh_token, :expires_at, :global, :options, :uid
3
-
4 2
   serialize :options, Hash
5 3
 
6 4
   belongs_to :user, :inverse_of => :services
@@ -57,17 +55,8 @@ class Service < ActiveRecord::Base
57 55
     (config = Devise.omniauth_configs[provider.to_sym]) && config.args[1]
58 56
   end
59 57
 
60
-  def self.provider_specific_options(omniauth)
61
-    case omniauth['provider'].to_sym
62
-      when :'37signals'
63
-        { user_id: omniauth['extra']['accounts'][0]['id'], name: omniauth['info']['name'] }
64
-      else
65
-        { name: omniauth['info']['nickname'] || omniauth['info']['name'] }
66
-    end
67
-  end
68
-
69 58
   def self.initialize_or_update_via_omniauth(omniauth)
70
-    options = provider_specific_options(omniauth)
59
+    options = get_options(omniauth)
71 60
 
72 61
     find_or_initialize_by(provider: omniauth['provider'], uid: omniauth['uid'].to_s).tap do |service|
73 62
       service.assign_attributes token: omniauth['credentials']['token'],
@@ -78,4 +67,24 @@ class Service < ActiveRecord::Base
78 67
                                 options: options
79 68
     end
80 69
   end
70
+
71
+  def self.register_options_provider(provider_name, &block)
72
+    option_providers[provider_name] = block
73
+  end
74
+
75
+  def self.get_options(omniauth)
76
+    option_providers.fetch(omniauth['provider'], option_providers['default']).call(omniauth)
77
+  end
78
+
79
+  private
80
+  @@option_providers = HashWithIndifferentAccess.new
81
+  cattr_reader :option_providers
82
+
83
+  register_options_provider('default') do |omniauth|
84
+    {name: omniauth['info']['nickname'] || omniauth['info']['name']}
85
+  end
86
+
87
+  register_options_provider('37signals') do |omniauth|
88
+    {user_id: omniauth['extra']['accounts'][0]['id'], name: omniauth['info']['name']}
89
+  end
81 90
 end

+ 15 - 5
app/models/user.rb

@@ -12,11 +12,6 @@ class User < ActiveRecord::Base
12 12
   # This is in addition to a real persisted field like 'username'
13 13
   attr_accessor :login
14 14
 
15
-  ACCESSIBLE_ATTRIBUTES = [ :email, :username, :login, :password, :password_confirmation, :remember_me, :invitation_code ]
16
-
17
-  attr_accessible *ACCESSIBLE_ATTRIBUTES
18
-  attr_accessible *(ACCESSIBLE_ATTRIBUTES + [:admin]), :as => :admin
19
-
20 15
   validates_presence_of :username
21 16
   validates :username, uniqueness: { case_sensitive: false }
22 17
   validates_format_of :username, :with => /\A[a-zA-Z0-9_-]{3,15}\Z/, :message => "can only contain letters, numbers, underscores, and dashes, and must be between 3 and 15 characters in length."
@@ -80,4 +75,19 @@ class User < ActiveRecord::Base
80 75
   def requires_no_invitation_code?
81 76
     !!@requires_no_invitation_code
82 77
   end
78
+
79
+  def undefined_agent_types
80
+    agents.reorder('').group(:type).pluck(:type).select do |type|
81
+      begin
82
+        type.constantize
83
+        false
84
+      rescue NameError
85
+        true
86
+      end
87
+    end
88
+  end
89
+
90
+  def undefined_agents
91
+    agents.where(type: undefined_agent_types).select('id, schedule, events_count, type as undefined')
92
+  end
83 93
 end

+ 0 - 2
app/models/user_credential.rb

@@ -1,8 +1,6 @@
1 1
 class UserCredential < ActiveRecord::Base
2 2
   MODES = %w[text java_script]
3 3
 
4
-  attr_accessible :credential_name, :credential_value, :mode
5
-
6 4
   belongs_to :user
7 5
 
8 6
   validates_presence_of :credential_name

+ 5 - 3
app/presenters/form_configurable_agent_presenter.rb

@@ -23,7 +23,9 @@ class FormConfigurableAgentPresenter < Decorator
23 23
       @view.content_tag 'div' do
24 24
         @view.concat @view.text_area_tag("agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3))
25 25
         if data[:ace].present?
26
-          @view.concat @view.content_tag('div', '', class: 'ace-editor', data: { source: "[name='agent[options][#{attribute}]']" })
26
+          ace_options = { source: "[name='agent[options][#{attribute}]']", mode: '', theme: ''}.deep_symbolize_keys!
27
+          ace_options.deep_merge!(data[:ace].deep_symbolize_keys) if data[:ace].is_a?(Hash)
28
+          @view.concat @view.content_tag('div', '', class: 'ace-editor', data: ace_options)
27 29
         end
28 30
       end
29 31
     when :boolean
@@ -43,7 +45,7 @@ class FormConfigurableAgentPresenter < Decorator
43 45
         @view.concat(@view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => "form-control #{@agent.send(:boolify, value) != nil ? 'hidden' : ''}"))
44 46
       end
45 47
     when :array, :string
46
-      @view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => 'form-control')
48
+      @view.text_field_tag "agent[options][#{attribute}]", value, html_options.deep_merge(:class => 'form-control', data: {cache_response: data[:cache_response] != false})
47 49
     end
48 50
   end
49
-end
51
+end

+ 3 - 0
app/views/admin/users/_form.html.erb

@@ -22,5 +22,8 @@
22 22
 <div class="row">
23 23
   <div class="col-md-12">
24 24
     <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, admin_users_path, class: "btn btn-default" %>
25
+    <% if @user.persisted? %>
26
+      <%= link_to 'Become User', switch_to_user_admin_user_path(@user), class: "btn btn-default btn-info", data: { confirm: 'This will log you in as another user. Would you like to continue?' } %>
27
+    <% end %>
25 28
   </div>
26 29
 </div>

+ 3 - 2
app/views/admin/users/index.html.erb

@@ -24,12 +24,13 @@
24 24
               <td><%= link_to user.username, edit_admin_user_path(user) %></td>
25 25
               <td><%= user.email %></td>
26 26
               <td><%= user_account_state(user) %></td>
27
-              <td><%= user.agents.active.count %></td>
28
-              <td><%= user.agents.inactive.count %></td>
27
+              <td><%= link_to user.agents.active.count, switch_to_user_admin_user_path(user), data: { confirm: 'This will log you in as another user. Would you like to continue?' } %></td>
28
+              <td><%= link_to user.agents.inactive.count, switch_to_user_admin_user_path(user), data: { confirm: 'This will log you in as another user. Would you like to continue?' } %></td>
29 29
               <td title='<%= user.created_at %>'><%= time_ago_in_words user.created_at %> ago</td>
30 30
               <td>
31 31
                 <div class="btn-group btn-group-xs">
32 32
                   <% if user != current_user %>
33
+                    <%= link_to 'Become User', switch_to_user_admin_user_path(user), class: "btn btn-default", data: { confirm: 'This will log you in as another user. Would you like to continue?' } %>
33 34
                     <% if user.active? %>
34 35
                       <%= link_to 'Deactivate', deactivate_admin_user_path(user), method: :put, class: "btn btn-default" %>
35 36
                     <% else %>

+ 1 - 1
app/views/agents/_form.html.erb

@@ -25,7 +25,7 @@
25 25
           <% if @agent.new_record? %>
26 26
             <div class="form-group type-select">
27 27
               <%= f.label :type %>
28
-              <%= f.select :type, options_for_select([['Select an Agent Type', 'Agent', {title: ''}]] + Agent.types.map {|type| [type.name.gsub(/^.*::/, '').underscore.humanize.titleize, type, {title: h(Agent.build_for_type(type.name,current_user,{}).html_description.lines.first.strip)}] }, @agent.type), {}, :class => 'form-control' %>
28
+              <%= f.select :type, options_for_select([['Select an Agent Type', 'Agent', {title: ''}]] + Agent.types.map {|type| [agent_type_to_human(type.name), type, {title: h(Agent.build_for_type(type.name,current_user,{}).html_description.lines.first.strip)}] }, @agent.type), {}, :class => 'form-control' %>
29 29
             </div>
30 30
           <% end %>
31 31
         </div>

+ 7 - 0
app/views/agents/agent_views/manual_event_agent/_show.html.erb

@@ -30,6 +30,13 @@
30 30
       e.preventDefault();
31 31
       var $form = $("#create-event-form");
32 32
       var $status = $("#event-creation-status");
33
+      try{
34
+        JSON.parse($form.find("textarea").val());
35
+      }
36
+      catch(err){
37
+        alert ('Sorry, there appears to be an error in your JSON input. Please fix it before continuing.');
38
+        return false;
39
+      }
33 40
       $.ajax({
34 41
         url: $form.attr('action'),
35 42
         method: "post",

+ 1 - 1
app/views/agents/index.html.erb

@@ -2,7 +2,7 @@
2 2
   <div class='row'>
3 3
     <div class='col-md-12'>
4 4
       <div class="page-header">
5
-        <h2>Your Agents</h2>
5
+        <h2><%= session[:original_admin_user_id].present? ? "#{current_user.username}’s Agents" : 'Your Agents' %></h2>
6 6
       </div>
7 7
 
8 8
       <%= render 'agents/table' %>

+ 44 - 0
app/views/application/undefined_agents.html.erb

@@ -0,0 +1,44 @@
1
+<div class="container">
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h3>
6
+          <div class="alert alert-danger" role="alert">
7
+            Error: Agent(s) are 'missing in action'
8
+          </div>
9
+        </h3>
10
+      </div>
11
+      <blockquote>
12
+        <p>
13
+          You have one or more Agents registered in the database for which no corresponding definition is available in the source code:
14
+        </p>
15
+        <ul>
16
+          <% @undefined_agent_types.each do |type| %>
17
+            <li><%= agent_type_to_human(type) %></li>
18
+          <% end %>
19
+        </ul>
20
+        <br/>
21
+        <p>
22
+          The issue most probably occurred because of one or more of the following reasons:
23
+        </p>
24
+        <ul>
25
+          <li>If the respective Agent is distributed as a Ruby gem, it might have been removed from the <code>ADDITIONAL_GEMS</code> environment setting.</li>
26
+          <li>If the respective Agent is distributed as part of the Huginn application codebase, it might have been removed from that either on purpose (because the Agent has been deprecated or been moved to an Agent gem) or accidentally. Please check if the Agent(s) in question are available in your Huginn codebase under the path <code>app/models/agents/</code>.</li>
27
+        </ul>
28
+        <br/>
29
+        <p>
30
+          You can fix the issue by adding the Agent(s) back to the application codebase by
31
+        </p>
32
+        <ul>
33
+          <li>adding the respective Agent(s) to the the <code>ADDITIONAL_GEMS</code> environment setting. Please see <a href="https://github.com/cantino/huginn_agent" target="_blank">https://github.com/cantino/huginn_agent</a> for documentation on how to properly set it.</li>
34
+          <li>adding the respective Agent(s) code to the Huginn application codebase (in case it was deleted accidentally).</li>
35
+          <li>deleting the respective Agent(s) from the database using the button below.</li>
36
+        </ul>
37
+        <br/>
38
+        <div class="btn-group">
39
+          <%= link_to icon_tag('glyphicon-trash') + ' Delete Missing Agents', undefined_agents_path, class: "btn btn-danger", method: :DELETE, data: { confirm: 'Are you sure all missing Agents should be deleted from the database?'} %>
40
+        </div>
41
+      </blockquote>
42
+    </div>
43
+  </div>
44
+</div>

+ 8 - 0
app/views/layouts/_navigation.html.erb

@@ -60,9 +60,17 @@
60 60
     <li class="dropdown">
61 61
       <a href="#" class="dropdown-toggle" data-toggle="dropdown">
62 62
         Account
63
+        <% if user_signed_in? && session[:original_admin_user_id].present? %>
64
+          <span class="label label-warning"><%= current_user.username %></span>
65
+        <% end %>
63 66
         <b class="caret"></b>
64 67
       </a>
65 68
       <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
69
+        <% if user_signed_in? && session[:original_admin_user_id].present? %>
70
+          <li>
71
+            <%= link_to 'Switch Back to Admin User', switch_back_admin_users_path, tabindex: '-1' %>
72
+          </li>
73
+        <% end %>
66 74
         <li>
67 75
           <% if user_signed_in? %>
68 76
             <%= link_to 'Account', edit_user_registration_path, :tabindex => "-1" %>

+ 3 - 3
app/views/logs/index.html.erb

@@ -13,13 +13,13 @@
13 13
 
14 14
         <td>
15 15
           <div class="btn-group btn-group-xs">
16
-            <% if log.inbound_event_id.present? %>
16
+            <% if log.inbound_event.present? %>
17 17
               <%= link_to 'Event In', event_path(log.inbound_event), class: "btn btn-default" %>
18 18
             <% else %>
19 19
               <%= link_to 'Event In', '#', class: "btn btn-default disabled" %>
20 20
             <% end %>
21 21
 
22
-            <% if log.outbound_event_id.present? %>
22
+            <% if log.outbound_event.present? %>
23 23
               <%= link_to 'Event Out', event_path(log.outbound_event), class: "btn btn-default" %>
24 24
             <% else %>
25 25
               <%= link_to 'Event Out', '#', class: "btn btn-default disabled" %>
@@ -31,4 +31,4 @@
31 31
       </tr>
32 32
     <% end %>
33 33
   </table>
34
-</div>
34
+</div>

+ 4 - 3
bin/rails

@@ -1,8 +1,9 @@
1 1
 #!/usr/bin/env ruby
2 2
 begin
3
-  load File.expand_path("../spring", __FILE__)
4
-rescue LoadError
3
+  load File.expand_path('../spring', __FILE__)
4
+rescue LoadError => e
5
+  raise unless e.message.include?('spring')
5 6
 end
6
-APP_PATH = File.expand_path('../../config/application',  __FILE__)
7
+APP_PATH = File.expand_path('../config/application', __dir__)
7 8
 require_relative '../config/boot'
8 9
 require 'rails/commands'

+ 3 - 2
bin/rake

@@ -1,7 +1,8 @@
1 1
 #!/usr/bin/env ruby
2 2
 begin
3
-  load File.expand_path("../spring", __FILE__)
4
-rescue LoadError
3
+  load File.expand_path('../spring', __FILE__)
4
+rescue LoadError => e
5
+  raise unless e.message.include?('spring')
5 6
 end
6 7
 require_relative '../config/boot'
7 8
 require 'rake'

+ 3 - 2
bin/rspec

@@ -1,7 +1,8 @@
1 1
 #!/usr/bin/env ruby
2 2
 begin
3
-  load File.expand_path("../spring", __FILE__)
4
-rescue LoadError
3
+  load File.expand_path('../spring', __FILE__)
4
+rescue LoadError => e
5
+  raise unless e.message.include?('spring')
5 6
 end
6 7
 require 'bundler/setup'
7 8
 load Gem.bin_path('rspec-core', 'rspec')

+ 6 - 6
bin/spring

@@ -4,12 +4,12 @@
4 4
 # It gets overwritten when you run the `spring binstub` command.
5 5
 
6 6
 unless defined?(Spring)
7
-  require "rubygems"
8
-  require "bundler"
7
+  require 'rubygems'
8
+  require 'bundler'
9 9
 
10
-  if match = Bundler.default_lockfile.read.match(/^GEM$.*?^    (?:  )*spring \((.*?)\)$.*?^$/m)
11
-    Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq }
12
-    gem "spring", match[1]
13
-    require "spring/binstub"
10
+  if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^    (?:  )*spring \((.*?)\)$.*?^$/m))
11
+    Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
12
+    gem 'spring', match[1]
13
+    require 'spring/binstub'
14 14
   end
15 15
 end

+ 1 - 1
config.ru

@@ -1,5 +1,5 @@
1 1
 # This file is used by Rack-based servers to start the application.
2 2
 
3
-require ::File.expand_path('../config/environment',  __FILE__)
3
+require_relative 'config/environment'
4 4
 
5 5
 run Huginn::Application

+ 1 - 10
config/application.rb

@@ -1,4 +1,4 @@
1
-require File.expand_path('../boot', __FILE__)
1
+require_relative 'boot'
2 2
 
3 3
 require 'rails/all'
4 4
 
@@ -39,15 +39,6 @@ module Huginn
39 39
     # like if you have constraints or database-specific column types
40 40
     # config.active_record.schema_format = :sql
41 41
 
42
-    # Enforce whitelist mode for mass assignment.
43
-    # This will create an empty whitelist of attributes available for mass-assignment for all models
44
-    # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
45
-    # parameters by using an attr_accessible or attr_protected declaration.
46
-    config.active_record.whitelist_attributes = true
47
-
48
-    # Do not swallow errors in after_commit/after_rollback callbacks.
49
-    config.active_record.raise_in_transactional_callbacks = true
50
-
51 42
     config.active_job.queue_adapter = :delayed_job
52 43
   end
53 44
 end

+ 1 - 1
config/boot.rb

@@ -1,6 +1,6 @@
1 1
 require 'rubygems'
2 2
 
3 3
 # Set up gems listed in the Gemfile.
4
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
4
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
5 5
 
6 6
 require 'bundler/setup' # Set up gems listed in the Gemfile.

+ 1 - 4
config/environment.rb

@@ -1,8 +1,5 @@
1 1
 # Load the rails application
2
-require File.expand_path('../application', __FILE__)
3
-
4
-# Remove the XML parser from the list that will be used to initialize the application's XML parser list.
5
-ActionDispatch::ParamsParser::DEFAULT_PARSERS.delete(Mime::XML)
2
+require_relative 'application'
6 3
 
7 4
 # Initialize the rails application
8 5
 Huginn::Application.initialize!

+ 25 - 13
config/environments/development.rb

@@ -16,9 +16,22 @@ Huginn::Application.configure do
16 16
   # Rake tasks automatically ignore this option for performance.
17 17
   config.eager_load = false
18 18
 
19
-  # Show full error reports and disable caching
20
-  config.consider_all_requests_local       = true
21
-  config.action_controller.perform_caching = false
19
+  # Show full error reports.
20
+  config.consider_all_requests_local = true
21
+
22
+  # Enable/disable caching. By default caching is disabled.
23
+  if Rails.root.join('tmp/caching-dev.txt').exist?
24
+    config.action_controller.perform_caching = true
25
+
26
+    config.cache_store = :memory_store
27
+    config.public_file_server.headers = {
28
+      'Cache-Control' => 'public, max-age=172800'
29
+    }
30
+  else
31
+    config.action_controller.perform_caching = false
32
+
33
+    config.cache_store = :null_store
34
+  end
22 35
 
23 36
   # Print deprecation notices to the Rails logger
24 37
   config.active_support.deprecation = :log
@@ -26,8 +39,8 @@ Huginn::Application.configure do
26 39
   # Only use best-standards-support built into browsers
27 40
   config.action_dispatch.best_standards_support = :builtin
28 41
 
29
-  # Raise exception on mass assignment protection for Active Record models
30
-  config.active_record.mass_assignment_sanitizer = :strict
42
+  # Raise exception for unpermitted parameters
43
+  config.action_controller.action_on_unpermitted_parameters = :raise
31 44
 
32 45
   # Raise an error on page load if there are pending migrations.
33 46
   config.active_record.migration_error = :page_load
@@ -35,14 +48,8 @@ Huginn::Application.configure do
35 48
   # Expands the lines which load the assets
36 49
   config.assets.debug = true
37 50
 
38
-  # Asset digests allow you to set far-future HTTP expiration dates on all assets,
39
-  # yet still be able to expire them through the digest params.
40
-  config.assets.digest = true
41
-
42
-  # Adds additional error checking when serving assets at runtime.
43
-  # Checks for improperly declared sprockets dependencies.
44
-  # Raises helpful error messages.
45
-  config.assets.raise_runtime_errors = true
51
+  # Suppress logger output for asset requests.
52
+  config.assets.quiet = true
46 53
 
47 54
   config.action_mailer.default_url_options = { :host => ENV['DOMAIN'] }
48 55
   config.action_mailer.asset_host = ENV['DOMAIN']
@@ -52,5 +59,10 @@ Huginn::Application.configure do
52 59
   else
53 60
     config.action_mailer.delivery_method = :letter_opener_web
54 61
   end
62
+  config.action_mailer.perform_caching = false
55 63
   # smtp_settings moved to config/initializers/action_mailer.rb
64
+
65
+  # Use an evented file watcher to asynchronously detect changes in source code,
66
+  # routes, locales, etc. This feature depends on the listen gem.
67
+  config.file_watcher = ActiveSupport::EventedFileUpdateChecker
56 68
 end

+ 19 - 12
config/environments/production.rb

@@ -14,15 +14,19 @@ Huginn::Application.configure do
14 14
   config.consider_all_requests_local       = false
15 15
   config.action_controller.perform_caching = true
16 16
 
17
-  # Enable Rack::Cache to put a simple HTTP cache in front of your application
18
-  # Add `rack-cache` to your Gemfile before enabling this.
19
-  # For large-scale production use, consider using a caching reverse proxy like
20
-  # NGINX, varnish or squid.
21
-  # config.action_dispatch.rack_cache = true
22
-
23 17
   # Disable serving static files from the `/public` folder by default since
24 18
   # Apache or NGINX already handles this.
25
-  config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
19
+  config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
20
+
21
+  if ENV["RAILS_LOG_TO_STDOUT"].present? ||
22
+       ENV['ON_HEROKU'] ||
23
+       ENV['HEROKU_POSTGRESQL_ROSE_URL'] ||
24
+       ENV['HEROKU_POSTGRESQL_GOLD_URL'] ||
25
+       File.read(File.join(File.dirname(__FILE__), '../../Procfile')) =~ /intended for Heroku/
26
+    logger           = ActiveSupport::Logger.new(STDOUT)
27
+    logger.formatter = config.log_formatter
28
+    config.logger = ActiveSupport::TaggedLogging.new(logger)
29
+  end
26 30
 
27 31
   # Compress JavaScripts and CSS
28 32
   config.assets.js_compressor  = :uglifier
@@ -31,15 +35,17 @@ Huginn::Application.configure do
31 35
   # Don't fallback to assets pipeline if a precompiled asset is missed
32 36
   config.assets.compile = false
33 37
 
34
-  # Generate digests for assets URLs
35
-  config.assets.digest = true
36
-
37 38
   # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
38 39
 
39 40
   # Specifies the header that your server uses for sending files.
40 41
   # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
41 42
   # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
42 43
 
44
+  # Mount Action Cable outside main process or domain
45
+  # config.action_cable.mount_path = nil
46
+  # config.action_cable.url = 'wss://example.com/cable'
47
+  # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
48
+
43 49
   # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
44 50
   config.force_ssl = ENV['FORCE_SSL'] == 'true'
45 51
 
@@ -47,7 +53,7 @@ Huginn::Application.configure do
47 53
   config.log_level = :info
48 54
 
49 55
   # Prepend all log lines with the following tags
50
-  config.log_tags = [ :uuid ] # :subdomain
56
+  config.log_tags = [ :request_id ] # :subdomain
51 57
 
52 58
   # Use a different logger for distributed setups
53 59
   # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
@@ -79,11 +85,12 @@ Huginn::Application.configure do
79 85
 
80 86
   config.action_mailer.default_url_options = { :host => ENV['DOMAIN'] }
81 87
   config.action_mailer.asset_host = ENV['DOMAIN']
82
-  if ENV['ASSET_HOST']
88
+  if ENV['ASSET_HOST'].present?
83 89
     config.action_mailer.asset_host = ENV['ASSET_HOST']
84 90
   end
85 91
   config.action_mailer.perform_deliveries = true
86 92
   config.action_mailer.raise_delivery_errors = true
87 93
   config.action_mailer.delivery_method = :smtp
94
+  config.action_mailer.perform_caching = false
88 95
   # smtp_settings moved to config/initializers/action_mailer.rb
89 96
 end

+ 7 - 7
config/environments/test.rb

@@ -13,8 +13,10 @@ Huginn::Application.configure do
13 13
   config.eager_load = false
14 14
 
15 15
   # Configure static asset server for tests with Cache-Control for performance
16
-  config.serve_static_files = true
17
-  config.static_cache_control = "public, max-age=3600"
16
+  config.public_file_server.enabled = true
17
+  config.public_file_server.headers = {
18
+    'Cache-Control' => 'public, max-age=3600'
19
+  }
18 20
 
19 21
   # Show full error reports and disable caching
20 22
   config.consider_all_requests_local       = true
@@ -25,6 +27,7 @@ Huginn::Application.configure do
25 27
 
26 28
   # Disable request forgery protection in test environment
27 29
   config.action_controller.allow_forgery_protection = false
30
+  config.action_mailer.perform_caching = false
28 31
 
29 32
   # Tell Action Mailer not to deliver emails to the real world.
30 33
   # The :test delivery method accumulates sent emails in the
@@ -33,11 +36,8 @@ Huginn::Application.configure do
33 36
 
34 37
   config.action_mailer.raise_delivery_errors = true
35 38
 
36
-  # Raise exception on mass assignment protection for Active Record models
37
-  config.active_record.mass_assignment_sanitizer = :strict
38
-
39
-  # Randomize the order test cases are executed.
40
-  config.active_support.test_order = :random
39
+  # Raise exception for unpermitted parameters
40
+  config.action_controller.action_on_unpermitted_parameters = :raise
41 41
 
42 42
   # Print deprecation notices to the stderr
43 43
   config.active_support.deprecation = :stderr

+ 2 - 2
config/initializers/action_mailer.rb

@@ -4,8 +4,8 @@ ActionMailer::Base.smtp_settings = {
4 4
   domain: ENV['SMTP_DOMAIN'],
5 5
   authentication: ENV['SMTP_AUTHENTICATION'] || "plain",
6 6
   enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true',
7
-  user_name: ENV['SMTP_USER_NAME'] || "",
8
-  password: ENV['SMTP_PASSWORD'] || "",
7
+  user_name: ENV['SMTP_USER_NAME'].presence,
8
+  password: ENV['SMTP_PASSWORD'].presence,
9 9
   openssl_verify_mode: ENV['SMTP_OPENSSL_VERIFY_MODE'].presence,
10 10
   ca_path: ENV['SMTP_OPENSSL_CA_PATH'].presence,
11 11
   ca_file: ENV['SMTP_OPENSSL_CA_FILE'].presence

+ 0 - 1
config/initializers/ar_mysql_column_charset.rb

@@ -1 +0,0 @@
1
-require 'ar_mysql_column_charset'

+ 5 - 0
config/initializers/cookies_serializer.rb

@@ -0,0 +1,5 @@
1
+# Be sure to restart your server when you modify this file.
2
+
3
+# Specify a serializer for the signed and encrypted cookie jars.
4
+# Valid options are :json, :marshal, and :hybrid.
5
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid

+ 10 - 10
config/initializers/delayed_job.rb

@@ -10,18 +10,18 @@ Delayed::Worker.logger = Rails.logger
10 10
 # Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log'))
11 11
 # Delayed::Worker.logger.level = Logger::DEBUG
12 12
 
13
-class Delayed::Job
14
-  scope :pending, -> { where("locked_at IS NULL AND attempts = 0") }
15
-  scope :awaiting_retry, -> { where("failed_at IS NULL AND attempts > 0 AND locked_at IS NULL") }
16
-  scope :failed, -> { where("failed_at IS NOT NULL") }
17
-end
13
+ActiveSupport.on_load(:delayed_job_active_record) do
14
+  class Delayed::Job
15
+    scope :pending, -> { where("locked_at IS NULL AND attempts = 0") }
16
+    scope :awaiting_retry, -> { where("failed_at IS NULL AND attempts > 0 AND locked_at IS NULL") }
17
+    scope :failed, -> { where("failed_at IS NOT NULL") }
18
+  end
18 19
 
19
-def database_deadlocks_when_using_optimized_strategy?
20
-  ENV["DATABASE_ADAPTER"] == "mysql2"
21
-end
20
+  database_deadlocks_when_using_optimized_strategy = lambda do
21
+    ENV["DATABASE_ADAPTER"] == "mysql2"
22
+  end
22 23
 
23
-if database_deadlocks_when_using_optimized_strategy?
24 24
   Delayed::Backend::ActiveRecord.configure do |config|
25 25
     config.reserve_sql_strategy = :default_sql
26
-  end
26
+  end if database_deadlocks_when_using_optimized_strategy.call
27 27
 end

+ 24 - 0
config/initializers/new_framework_defaults.rb

@@ -0,0 +1,24 @@
1
+# Be sure to restart your server when you modify this file.
2
+#
3
+# This file contains migration options to ease your Rails 5.0 upgrade.
4
+#
5
+# Read the Rails 5.0 release notes for more info on each option.
6
+
7
+# Enable per-form CSRF tokens. Previous versions had false.
8
+Rails.application.config.action_controller.per_form_csrf_tokens = true
9
+
10
+# Enable origin-checking CSRF mitigation. Previous versions had false.
11
+Rails.application.config.action_controller.forgery_protection_origin_check = true
12
+
13
+# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
14
+# Previous versions had false.
15
+ActiveSupport.to_time_preserves_timezone = true
16
+
17
+# Require `belongs_to` associations by default. Previous versions had false.
18
+Rails.application.config.active_record.belongs_to_required_by_default = true
19
+
20
+# Do not halt callback chains when a callback returns false. Previous versions had true.
21
+ActiveSupport.halt_callback_chains_on_return_false = false
22
+
23
+# Configure SSL options to enable HSTS with subdomains. Previous versions had false.
24
+Rails.application.config.ssl_options = { hsts: { subdomains: true } }

+ 1 - 1
config/initializers/sanitizer.rb

@@ -1,2 +1,2 @@
1 1
 ActionView::Base.sanitized_allowed_tags += Set.new(%w(style table thead tbody tr th td))
2
-ActionView::Base.sanitized_allowed_attributes += Set.new(%w(border cellspacing cellpadding valign))
2
+ActionView::Base.sanitized_allowed_attributes += Set.new(%w(border cellspacing cellpadding valign style))

+ 7 - 8
config/initializers/silence_worker_status_logger.rb

@@ -1,10 +1,9 @@
1
-Rails::Rack::Logger.class_eval do
2
-  def call_with_silence_worker_status(env)
3
-    previous_level = Rails.logger.level
4
-    Rails.logger.level = Logger::ERROR if env['PATH_INFO'] =~ %r{^/worker_status}
5
-    call_without_silence_worker_status(env)
6
-  ensure
7
-    Rails.logger.level = previous_level
1
+module SilencedLogger
2
+  def call(env)
3
+    return super(env) if env['PATH_INFO'] !~ %r{^/worker_status}
4
+    Rails.logger.silence(Logger::ERROR) do
5
+      super(env)
6
+    end
8 7
   end
9
-  alias_method_chain :call, :silence_worker_status
10 8
 end
9
+Rails::Rack::Logger.send(:prepend, SilencedLogger)

+ 1 - 1
config/initializers/wrap_parameters.rb

@@ -5,7 +5,7 @@
5 5
 
6 6
 # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 7
 ActiveSupport.on_load(:action_controller) do
8
-  wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
8
+  wrap_parameters format: [:json]
9 9
 end
10 10
 
11 11
 # To enable root element in JSON for ActiveRecord objects.

+ 5 - 0
config/routes.rb

@@ -15,6 +15,7 @@ Huginn::Application.routes.draw do
15 15
       get :event_descriptions
16 16
       post :validate
17 17
       post :complete
18
+      delete :undefined, action: :destroy_undefined
18 19
     end
19 20
 
20 21
     resources :logs, :only => [:index] do
@@ -84,6 +85,10 @@ Huginn::Application.routes.draw do
84 85
       member do
85 86
         put :deactivate
86 87
         put :activate
88
+        get :switch_to_user
89
+      end
90
+      collection do
91
+        get :switch_back
87 92
       end
88 93
     end
89 94
   end

+ 6 - 0
config/spring.rb

@@ -0,0 +1,6 @@
1
+%w(
2
+  .ruby-version
3
+  .rbenv-vars
4
+  tmp/restart.txt
5
+  tmp/caching-dev.txt
6
+).each { |path| Spring.watch(path) }

+ 2 - 0
db/migrate/20140505201716_migrate_agents_to_liquid_templating.rb

@@ -1,3 +1,5 @@
1
+require 'liquid_migrator'
2
+
1 3
 class MigrateAgentsToLiquidTemplating < ActiveRecord::Migration
2 4
   class Agent < ActiveRecord::Base
3 5
     include JSONSerializedField

+ 0 - 2
db/migrate/20140813110107_set_charset_for_mysql.rb

@@ -3,7 +3,6 @@ class SetCharsetForMysql < ActiveRecord::Migration
3 3
     @all_models ||= [
4 4
       Agent,
5 5
       AgentLog,
6
-      Contact,
7 6
       Event,
8 7
       Link,
9 8
       Scenario,
@@ -23,7 +22,6 @@ class SetCharsetForMysql < ActiveRecord::Migration
23 22
         all_models.each { |model|
24 23
           table_name = model.table_name
25 24
 
26
-          # `contacts` may not exist
27 25
           next unless connection.table_exists? table_name
28 26
 
29 27
           model.columns.each { |column|

+ 25 - 0
db/migrate/20160607055850_change_events_order_to_events_list_order.rb

@@ -0,0 +1,25 @@
1
+class ChangeEventsOrderToEventsListOrder < ActiveRecord::Migration
2
+  def up
3
+    Agents::DataOutputAgent.find_each do |agent|
4
+      if value = agent.options.delete('events_order')
5
+        agent.options['events_list_order'] = value
6
+        agent.save!(validate: false)
7
+      end
8
+    end
9
+  end
10
+
11
+  def down
12
+    Agents::DataOutputAgent.transaction do
13
+      Agents::DataOutputAgent.find_each do |agent|
14
+        if agent.options['events_order']
15
+          raise ActiveRecord::IrreversibleMigration, "Cannot revert migration because events_order is configured"
16
+        end
17
+
18
+        if value = agent.options.delete('events_list_order')
19
+          agent.options['events_order'] = value
20
+          agent.save!(validate: false)
21
+        end
22
+      end
23
+    end
24
+  end
25
+end

+ 8 - 0
db/migrate/20160807000122_remove_queue_from_email_digest_agent_memory.rb

@@ -0,0 +1,8 @@
1
+class RemoveQueueFromEmailDigestAgentMemory < ActiveRecord::Migration
2
+  def up
3
+    Agents::EmailDigestAgent.find_each do |agent|
4
+      agent.memory.delete("queue")
5
+      agent.save!(validate: false)
6
+    end
7
+  end
8
+end

+ 15 - 0
db/migrate/20160823151303_set_emit_error_event_for_twitter_action_agents.rb

@@ -0,0 +1,15 @@
1
+class SetEmitErrorEventForTwitterActionAgents < ActiveRecord::Migration
2
+  def up
3
+    Agents::TwitterActionAgent.find_each do |agent|
4
+      agent.options['emit_error_events'] = 'true'
5
+      agent.save!(validate: false)
6
+    end
7
+  end
8
+
9
+  def down
10
+    Agents::TwitterActionAgent.find_each do |agent|
11
+      agent.options.delete('emit_error_events')
12
+      agent.save!(validate: false)
13
+    end
14
+  end
15
+end

+ 58 - 0
db/migrate/20161004120214_update_pushover_agent_options.rb

@@ -0,0 +1,58 @@
1
+class UpdatePushoverAgentOptions < ActiveRecord::Migration
2
+  DEFAULT_OPTIONS = {
3
+    'message' => '{{ message | default: text }}',
4
+    'device' => '{{ device }}',
5
+    'title' => '{{ title | default: subject }}',
6
+    'url' => '{{ url }}',
7
+    'url_title' => '{{ url_title }}',
8
+    'priority' => '{{ priority }}',
9
+    'timestamp' => '{{ timestamp }}',
10
+    'sound' => '{{ sound }}',
11
+    'retry' => '{{ retry }}',
12
+    'expire' => '{{ expire }}',
13
+  }
14
+
15
+  def up
16
+    Agents::PushoverAgent.find_each do |agent|
17
+      options = agent.options
18
+      DEFAULT_OPTIONS.each_pair do |key, default|
19
+        current = options[key]
20
+
21
+        options[key] =
22
+          if current.blank?
23
+            default
24
+          else
25
+            "#{prefix_for(key)}#{current}#{suffix_for(key)}"
26
+          end
27
+      end
28
+      agent.save!(validate: false)
29
+    end
30
+  end
31
+
32
+  def down
33
+    Agents::PushoverAgent.transaction do
34
+      Agents::PushoverAgent.find_each do |agent|
35
+        options = agent.options
36
+        DEFAULT_OPTIONS.each_pair do |key, default|
37
+          current = options[key]
38
+
39
+          options[key] =
40
+            if current == default
41
+              ''
42
+            else
43
+              current[/\A#{Regexp.quote(prefix_for(key))}(.*)#{Regexp.quote(suffix_for(key))}\z/, 1]
44
+            end or raise ActiveRecord::IrreversibleMigration, "Cannot revert migration once Pushover agents are configured"
45
+        end
46
+        agent.save!(validate: false)
47
+      end
48
+    end
49
+  end
50
+
51
+  def prefix_for(key)
52
+    "{% capture _default_ %}"
53
+  end
54
+
55
+  def suffix_for(key)
56
+    "{% endcapture %}" << DEFAULT_OPTIONS[key].sub(/(?=\}\}\z)/, '| default: _default_ ')
57
+  end
58
+end

+ 9 - 0
db/migrate/20161007030910_reset_data_output_agents.rb

@@ -0,0 +1,9 @@
1
+class ResetDataOutputAgents < ActiveRecord::Migration
2
+  def up
3
+    Agents::DataOutputAgent.find_each do |agent|
4
+      agent.memory = {}
5
+      agent.save(validate: false)
6
+      agent.latest_events(true)
7
+    end
8
+  end
9
+end

+ 4 - 4
db/seeds/seeder.rb

@@ -1,14 +1,14 @@
1 1
 class Seeder
2 2
   def self.seed
3
-    user = User.find_or_initialize_by(:email => ENV['SEED_EMAIL'] || "admin@example.com")
3
+    user = User.find_or_initialize_by(:email => ENV['SEED_EMAIL'].presence || "admin@example.com")
4 4
     if user.persisted?
5 5
       puts "User with email '#{user.email}' already exists, not seeding."
6 6
       exit
7 7
     end
8 8
 
9
-    user.username = ENV['SEED_USERNAME'] || "admin"
10
-    user.password = ENV['SEED_PASSWORD'] || "password"
11
-    user.password_confirmation = ENV['SEED_PASSWORD'] || "password"
9
+    user.username = ENV['SEED_USERNAME'].presence || "admin"
10
+    user.password = ENV['SEED_PASSWORD'].presence || "password"
11
+    user.password_confirmation = ENV['SEED_PASSWORD'].presence || "password"
12 12
     user.invitation_code = User::INVITATION_CODES.first
13 13
     user.admin = true
14 14
     user.save!

+ 2 - 2
doc/README.md

@@ -8,7 +8,7 @@
8 8
 
9 9
 ### Manual installation
10 10
 
11
-Manual installation instructions which will guide through the steps to install Huginn on any Ubuntu 12.04/14.04 or Debian 6/7 server.
11
+Manual installation instructions which will guide through the steps to install Huginn on any Ubuntu 12.04/14.04/16.04 or Debian 6/7 server.
12 12
 
13 13
 - [Install](manual/README.md) Requirements, directory structures and installation from source.
14 14
 - [Update](manual/update.md) Update your installation.
@@ -17,4 +17,4 @@ Manual installation instructions which will guide through the steps to install H
17 17
 ### Heroku
18 18
 
19 19
 - [Deploy to Heroku](heroku/install.md)
20
-- [Update](heroku/update.md) an existing Heroku deployment
20
+- [Update](heroku/update.md) an existing Heroku deployment

+ 1 - 1
doc/deployment/capistrano/deploy.rb

@@ -68,7 +68,7 @@ namespace :foreman do
68 68
 end
69 69
 
70 70
 # If you want to use rvm on your server and have it maintained by Capistrano, uncomment these lines:
71
-#   set :rvm_ruby_string, '2.0.0@huginn'
71
+#   set :rvm_ruby_string, '2.3.1@huginn'
72 72
 #   set :rvm_type, :user
73 73
 #   before 'deploy', 'rvm:install_rvm'
74 74
 #   before 'deploy', 'rvm:install_ruby'

+ 5 - 5
doc/manual/installation.md

@@ -65,8 +65,8 @@ Remove the old Ruby versions if present:
65 65
 Download Ruby and compile it:
66 66
 
67 67
     mkdir /tmp/ruby && cd /tmp/ruby
68
-    curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.0.tar.bz2 | tar xj
69
-    cd ruby-2.3.0
68
+    curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.bz2 | tar xj
69
+    cd ruby-2.3.1
70 70
     ./configure --disable-install-rdoc
71 71
     make -j`nproc`
72 72
     sudo make install
@@ -110,7 +110,7 @@ Create a user for Huginn do not type the `mysql>`, this is part of the prompt. C
110 110
 
111 111
 Ensure you can use the InnoDB engine which is necessary to support long indexes
112 112
 
113
-    mysql> SET storage_engine=INNODB;
113
+    mysql> SET default_storage_engine=INNODB;
114 114
 
115 115
     # If this fails, check your MySQL config files (e.g. `/etc/mysql/*.cnf`, `/etc/mysql/conf.d/*`)
116 116
     # for the setting "innodb = off"
@@ -134,7 +134,7 @@ You should now see `ERROR 1049 (42000): Unknown database 'huginn_production'` wh
134 134
 You are done installing the database and can go back to the rest of the installation.
135 135
 
136 136
 
137
-## 6. Huginn
137
+## 5. Huginn
138 138
 
139 139
 ### Clone the Source
140 140
 
@@ -264,7 +264,7 @@ Export the init scripts:
264 264
 
265 265
     sudo bundle exec rake production:status
266 266
 
267
-## 7. Nginx
267
+## 6. Nginx
268 268
 
269 269
 **Note:** Nginx is the officially supported web server for Huginn. If you cannot or do not want to use Nginx as your web server, the wiki has a page on how to configure [apache](https://github.com/cantino/huginn/wiki/Apache-Huginn-configuration).
270 270
 

+ 3 - 3
doc/manual/requirements.md

@@ -4,7 +4,7 @@
4 4
 
5 5
 ### Supported Unix distributions by this guide
6 6
 
7
-- Ubuntu (14.04 and 12.04)
7
+- Ubuntu (16.04, 14.04 and 12.04)
8 8
 - Debian (Jessie and Wheezy)
9 9
 
10 10
 ### Unsupported Unix distributions
@@ -27,7 +27,7 @@ Please consider using a virtual machine to run Huginn on Windows.
27 27
 
28 28
 ## Ruby versions
29 29
 
30
-Huginn requires Ruby (MRI) 2.0, 2.1 or 2.2
30
+Huginn requires Ruby (MRI) 2.2 or 2.3.
31 31
 You will have to use the standard MRI implementation of Ruby.
32 32
 We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but Huginn needs several Gems that have native extensions.
33 33
 
@@ -65,4 +65,4 @@ A DelayedJob worker is a separate process which runs your Huginn Agents. It fetc
65 65
 
66 66
 Estimating the amount of workers needed is easy. One worker can perform just one check at a time.  
67 67
 If you have 60 Agents checking websites every minute which take about 1 second to respond, one worker is fine.  
68
-If you need more Agents or are dealing with slow/unreliable websites/services, you should consider running additional workers.
68
+If you need more Agents or are dealing with slow/unreliable websites/services, you should consider running additional workers.

+ 1 - 1
docker/multi-process/scripts/init

@@ -43,7 +43,7 @@ grep = /app/.env.example | sed -e 's/^#\([^ ]\)/\1/' | grep -v -e '^#' | \
43 43
 
44 44
 eval "echo RAILS_ENV=${RAILS_ENV}" >> .env
45 45
 eval "echo START_MYSQL=${START_MYSQL}" >> .env
46
-echo "ON_HEROKU=true" >> .env
46
+echo "RAILS_LOG_TO_STDOUT=true" >> .env
47 47
 echo "RAILS_SERVE_STATIC_FILES=true" >> .env
48 48
 
49 49
 chmod ugo+r /app/.env

+ 1 - 1
docker/scripts/prepare

@@ -27,7 +27,7 @@ $minimal_apt_get_install build-essential checkinstall git-core \
27 27
   libncurses5-dev libffi-dev libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev \
28 28
   graphviz libgraphviz-dev \
29 29
   libmysqlclient-dev libpq-dev libsqlite3-dev \
30
-  ruby2.2 ruby2.2-dev
30
+  ruby2.3 ruby2.3-dev
31 31
 locale-gen en_US.UTF-8
32 32
 update-locale LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8
33 33
 gem install --no-ri --no-rdoc bundler

+ 13 - 0
docker/scripts/setup

@@ -27,3 +27,16 @@ mv config/unicorn.rb.example config/unicorn.rb
27 27
 sed -ri 's/^listen .*$/listen ENV["PORT"]/' config/unicorn.rb
28 28
 sed -ri 's/^stderr_path.*$//' config/unicorn.rb
29 29
 sed -ri 's/^stdout_path.*$//' config/unicorn.rb
30
+
31
+# Add ENV variables to .env.example which are not present in it but usable
32
+cat >> /app/.env.example <<EOF
33
+ASSET_HOST=
34
+DEFAULT_SCENARIO_FILE=
35
+RAILS_SERVE_STATIC_FILES=
36
+SEED_EMAIL=
37
+SEED_PASSWORD=
38
+SEED_USERNAME=
39
+SMTP_OPENSSL_CA_FILE=
40
+SMTP_OPENSSL_CA_PATH=
41
+SMTP_OPENSSL_VERIFY_MODE=
42
+EOF

+ 41 - 31
docker/single-process/docker-compose.yml

@@ -1,35 +1,45 @@
1
-mysqldata:
2
-  image: mysql:5.7
3
-  command: /bin/true
1
+# This needs at least compose 1.6.0
2
+version: '2'
4 3
 
5
-mysql:
6
-  image: mysql:5.7
7
-  volumes_from:
8
-    - mysqldata
9
-  environment:
10
-    MYSQL_ROOT_PASSWORD: myrootpassword
11
-    MYSQL_DATABASE: huginn
12
-    MYSQL_USER: huginn
13
-    MYSQL_PASSWORD: myhuginnpassword
4
+services:
5
+  mysqldata:
6
+    image: mysql:5.7
7
+    command: /bin/true
14 8
 
15
-huginn_web:
16
-  image: cantino/huginn-single-process
17
-  restart: always
18
-  extends:
19
-    file: environment.yml
20
-    service: huginn_base
21
-  ports:
22
-    - 3000:3000
23
-  links:
24
-    - mysql
9
+  mysql:
10
+    image: mysql:5.7
11
+    volumes_from:
12
+      - mysqldata
13
+    environment:
14
+      MYSQL_ROOT_PASSWORD: myrootpassword
15
+      MYSQL_DATABASE: huginn
16
+      MYSQL_USER: huginn
17
+      MYSQL_PASSWORD: myhuginnpassword
25 18
 
26
-huginn_threaded:
27
-  image: cantino/huginn-single-process
28
-  restart: always
29
-  extends:
30
-    file: environment.yml
31
-    service: huginn_base
32
-  links:
33
-    - mysql
34
-  command: /scripts/init bin/threaded.rb
19
+  huginn_web:
20
+    image: cantino/huginn-single-process
21
+    restart: always
22
+    extends:
23
+      file: environment.yml
24
+      service: huginn_base
25
+    ports:
26
+      - 3000:3000
27
+    links:
28
+      - mysql
29
+    environment:
30
+      MYSQL_PORT_3306_TCP_ADDR: mysql
31
+      MYSQL_PORT_3306_TCP_PORT: 3306
32
+
33
+  huginn_threaded:
34
+    image: cantino/huginn-single-process
35
+    restart: always
36
+    extends:
37
+      file: environment.yml
38
+      service: huginn_base
39
+    links:
40
+      - mysql
41
+    command: /scripts/init bin/threaded.rb
42
+    environment:
43
+      MYSQL_PORT_3306_TCP_ADDR: mysql
44
+      MYSQL_PORT_3306_TCP_PORT: 3306
35 45
 

+ 11 - 7
docker/single-process/environment.yml

@@ -1,7 +1,11 @@
1
-huginn_base:
2
-  environment:
3
-    DATABASE_ADAPTER: mysql2
4
-    DATABASE_NAME: huginn
5
-    DATABASE_USERNAME: huginn
6
-    DATABASE_PASSWORD: myhuginnpassword
7
-    APP_SECRET_TOKEN: 3bd139f9186b31a85336bb89cd1a1337078921134b2f48e022fd09c234d764d3e19b018b2ab789c6e0e04a1ac9e3365116368049660234c2038dc9990513d49c
1
+# This needs at least compose 1.6.0
2
+version: '2'
3
+
4
+services:
5
+  huginn_base:
6
+    environment:
7
+      DATABASE_ADAPTER: mysql2
8
+      DATABASE_NAME: huginn
9
+      DATABASE_USERNAME: huginn
10
+      DATABASE_PASSWORD: myhuginnpassword
11
+      APP_SECRET_TOKEN: 3bd139f9186b31a85336bb89cd1a1337078921134b2f48e022fd09c234d764d3e19b018b2ab789c6e0e04a1ac9e3365116368049660234c2038dc9990513d49c

+ 77 - 63
docker/single-process/postgresql.yml

@@ -1,72 +1,86 @@
1
-postgresdata:
2
-  image: postgres:9.5
3
-  command: /bin/true
1
+# This needs at least compose 1.6.0
2
+version: '2'
4 3
 
5
-postgres:
6
-  image: postgres:9.5
7
-  volumes_from:
8
-    - postgresdata
9
-  environment:
10
-    POSTGRES_PASSWORD: myhuginnpassword
11
-    POSTGRES_USER: huginn
4
+services:
5
+  postgresdata:
6
+    image: postgres:9.5
7
+    command: /bin/true
12 8
 
13
-huginn_web:
14
-  image: cantino/huginn-single-process
15
-  restart: always
16
-  extends:
17
-    file: environment.yml
18
-    service: huginn_base
19
-  environment:
20
-    DATABASE_ADAPTER: postgresql
21
-  ports:
22
-    - 3000:3000
23
-  links:
24
-    - postgres
9
+  postgres:
10
+    image: postgres:9.5
11
+    volumes_from:
12
+      - postgresdata
13
+    environment:
14
+      POSTGRES_PASSWORD: myhuginnpassword
15
+      POSTGRES_USER: huginn
25 16
 
26
-huginn_threaded:
27
-  image: cantino/huginn-single-process
28
-  restart: always
29
-  extends:
30
-    file: environment.yml
31
-    service: huginn_base
32
-  environment:
33
-    DATABASE_ADAPTER: postgresql
34
-  links:
35
-    - postgres
36
-  command: /scripts/init bin/threaded.rb
17
+  huginn_web:
18
+    image: cantino/huginn-single-process
19
+    restart: always
20
+    extends:
21
+      file: environment.yml
22
+      service: huginn_base
23
+    environment:
24
+      DATABASE_ADAPTER: postgresql
25
+      POSTGRES_PORT_5432_TCP_ADDR: postgres
26
+      POSTGRES_PORT_5432_TCP_PORT: 5432
27
+    ports:
28
+      - 3000:3000
29
+    links:
30
+      - postgres
37 31
 
38
-# huginn_schedule:
39
-#   image: cantino/huginn-single-process
40
-#   extends:
41
-#     file: environment.yml
42
-#     service: huginn_base
43
-#   environment:
44
-#     DATABASE_ADAPTER: postgresql
45
-#   links:
46
-#     - postgres
47
-#   command: /scripts/init bin/schedule.rb
32
+  huginn_threaded:
33
+    image: cantino/huginn-single-process
34
+    restart: always
35
+    extends:
36
+      file: environment.yml
37
+      service: huginn_base
38
+    environment:
39
+      DATABASE_ADAPTER: postgresql
40
+      POSTGRES_PORT_5432_TCP_ADDR: postgres
41
+      POSTGRES_PORT_5432_TCP_PORT: 5432
42
+    links:
43
+      - postgres
44
+    command: /scripts/init bin/threaded.rb
48 45
 
46
+  # huginn_schedule:
47
+  #   image: cantino/huginn-single-process
48
+  #   extends:
49
+  #     file: environment.yml
50
+  #     service: huginn_base
51
+  #   environment:
52
+  #     DATABASE_ADAPTER: postgresql
53
+  #     POSTGRES_PORT_5432_TCP_ADDR: postgres
54
+  #     POSTGRES_PORT_5432_TCP_PORT: 5432
55
+  #   links:
56
+  #     - postgres
57
+  #   command: /scripts/init bin/schedule.rb
49 58
 
50
-# huginn_twitter_stream:
51
-#   image: cantino/huginn-single-process
52
-#   extends:
53
-#     file: environment.yml
54
-#     service: huginn_base
55
-#   environment:
56
-#     DATABASE_ADAPTER: postgresql
57
-#   links:
58
-#     - postgres
59
-#   command: /scripts/init bin/twitter_stream.rb
60 59
 
60
+  # huginn_twitter_stream:
61
+  #   image: cantino/huginn-single-process
62
+  #   extends:
63
+  #     file: environment.yml
64
+  #     service: huginn_base
65
+  #   environment:
66
+  #     DATABASE_ADAPTER: postgresql
67
+  #     POSTGRES_PORT_5432_TCP_ADDR: postgres
68
+  #     POSTGRES_PORT_5432_TCP_PORT: 5432
69
+  #   links:
70
+  #     - postgres
71
+  #   command: /scripts/init bin/twitter_stream.rb
61 72
 
62
-# huginn_dj1:
63
-#   image: cantino/huginn-single-process
64
-#   extends:
65
-#     file: environment.yml
66
-#     service: huginn_base
67
-#   environment:
68
-#     DATABASE_ADAPTER: postgresql
69
-#   links:
70
-#     - postgres
71
-#   command: /scripts/init script/delayed_job run
73
+
74
+  # huginn_dj1:
75
+  #   image: cantino/huginn-single-process
76
+  #   extends:
77
+  #     file: environment.yml
78
+  #     service: huginn_base
79
+  #   environment:
80
+  #     DATABASE_ADAPTER: postgresql
81
+  #     POSTGRES_PORT_5432_TCP_ADDR: postgres
82
+  #     POSTGRES_PORT_5432_TCP_PORT: 5432
83
+  #   links:
84
+  #     - postgres
85
+  #   command: /scripts/init script/delayed_job run
72 86
 

+ 1 - 1
docker/single-process/scripts/init

@@ -29,7 +29,7 @@ grep = /app/.env.example | sed -e 's/^#\([^ ]\)/\1/' | grep -v -e '^#' | \
29 29
 
30 30
 eval "echo PORT=${PORT:-${PORT:-3000}}" >> .env
31 31
 eval "echo RAILS_ENV=${RAILS_ENV:-${RAILS_ENV:-production}}" >> .env
32
-eval "echo ON_HEROKU=true" >> .env
32
+eval "echo RAILS_LOG_TO_STDOUT=true" >> .env
33 33
 eval "echo RAILS_SERVE_STATIC_FILES=true" >> .env
34 34
 
35 35
 chmod ugo+r /app/.env

+ 0 - 13
lib/ar_mysql_column_charset.rb

@@ -1,13 +0,0 @@
1
-require 'active_support'
2
-
3
-ActiveSupport.on_load :active_record do
4
-  class << ActiveRecord::Base
5
-    def establish_connection(spec = nil)
6
-      super.tap { |ret|
7
-        if /mysql/i === connection.adapter_name
8
-          require 'ar_mysql_column_charset/main'
9
-        end
10
-      }
11
-    end
12
-  end
13
-end

+ 0 - 118
lib/ar_mysql_column_charset/main.rb

@@ -1,118 +0,0 @@
1
-raise "Do not directly load this library." unless defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter)
2
-
3
-module ActiveRecord::ConnectionAdapters
4
-  class ColumnDefinition
5
-    module CharsetSupport
6
-      attr_accessor :charset, :collation
7
-    end
8
-
9
-    prepend CharsetSupport
10
-  end
11
-
12
-  class TableDefinition
13
-    module CharsetSupport
14
-      def new_column_definition(name, type, options)
15
-        column = super
16
-        column.charset   = options[:charset]
17
-        column.collation = options[:collation]
18
-        column
19
-      end
20
-    end
21
-
22
-    prepend CharsetSupport
23
-  end
24
-
25
-  class AbstractMysqlAdapter
26
-    module CharsetSupport
27
-      def prepare_column_options(column, types)
28
-        spec = super
29
-        spec[:charset]   = column.charset.inspect if column.charset && column.charset != charset
30
-        spec[:collation] = column.collation.inspect if column.collation && column.collation != collation
31
-        spec
32
-      end
33
-
34
-      def migration_keys
35
-        super + [:charset, :collation]
36
-      end
37
-
38
-      def utf8mb4_supported?
39
-        if @utf8mb4_supported.nil?
40
-          @utf8mb4_supported = !select("show character set like 'utf8mb4'").empty?
41
-        else
42
-          @utf8mb4_supported
43
-        end
44
-      end
45
-
46
-      def charset_collation(charset, collation)
47
-        [charset, collation].map { |name|
48
-          case name
49
-          when nil
50
-            nil
51
-          when /\A(utf8mb4(_\w*)?)\z/
52
-            if utf8mb4_supported?
53
-              $1
54
-            else
55
-              "utf8#{$2}"
56
-            end
57
-          else
58
-            name.to_s
59
-          end
60
-        }
61
-      end
62
-
63
-      def create_database(name, options = {})
64
-        # utf8mb4 is used in column definitions; use utf8 for
65
-        # databases.
66
-        [:charset, :collation].each { |key|
67
-          case options[key]
68
-          when /\A(utf8mb4(_\w*)?)\z/
69
-            options = options.merge(key => "utf8#{$2}")
70
-          end
71
-        }
72
-        super(name, options)
73
-      end
74
-    end
75
-
76
-    prepend CharsetSupport
77
-
78
-    class SchemaCreation
79
-      module CharsetSupport
80
-        def column_options(o)
81
-          column_options = super
82
-          column_options[:charset]   = o.charset unless o.charset.nil?
83
-          column_options[:collation] = o.collation unless o.collation.nil?
84
-          column_options
85
-        end
86
-
87
-        def add_column_options!(sql, options)
88
-          charset, collation = @conn.charset_collation(options[:charset], options[:collation])
89
-
90
-          if charset
91
-            sql << " CHARACTER SET #{charset}"
92
-          end
93
-
94
-          if collation
95
-            sql << " COLLATE #{collation}"
96
-          end
97
-
98
-          super
99
-        end
100
-      end
101
-
102
-      prepend CharsetSupport
103
-    end
104
-
105
-    class Column
106
-      module CharsetSupport
107
-        attr_reader :charset
108
-
109
-        def initialize(*args)
110
-          super
111
-          @charset = @collation[/\A[^_]+/] unless @collation.nil?
112
-        end
113
-      end
114
-
115
-      prepend CharsetSupport
116
-    end
117
-  end
118
-end

+ 286 - 0
lib/feedjira_extension.rb

@@ -0,0 +1,286 @@
1
+require 'feedjira'
2
+require 'digest'
3
+require 'mail'
4
+
5
+module FeedjiraExtension
6
+  AUTHOR_ATTRS = %i[name email uri]
7
+  LINK_ATTRS = %i[href rel type hreflang title length]
8
+  ENCLOSURE_ATTRS = %i[url type length]
9
+
10
+  class Author < Struct.new(*AUTHOR_ATTRS)
11
+    def to_json(options = nil)
12
+      members.flat_map { |key|
13
+        if value = self[key].presence
14
+          case key
15
+          when :email
16
+            "<#{value}>"
17
+          when :uri
18
+            "(#{value})"
19
+          else
20
+            value
21
+          end
22
+        else
23
+          []
24
+        end
25
+      }.join(' ').to_json(options)
26
+    end
27
+  end
28
+
29
+  class AtomAuthor < Author
30
+    include SAXMachine
31
+
32
+    AUTHOR_ATTRS.each do |attr|
33
+      element attr
34
+    end
35
+  end
36
+
37
+  class RssAuthor < Author
38
+    include SAXMachine
39
+
40
+    def content=(content)
41
+      @content = content
42
+
43
+      begin
44
+        addr = Mail::Address.new(content)
45
+      rescue
46
+        self.name = content
47
+      else
48
+        self.name = addr.name
49
+        self.email = addr.address
50
+      end
51
+    end
52
+
53
+    value :content
54
+  end
55
+
56
+  class Enclosure
57
+    include SAXMachine
58
+
59
+    ENCLOSURE_ATTRS.each do |attr|
60
+      attribute attr
61
+    end
62
+
63
+    def to_json(options = nil)
64
+      ENCLOSURE_ATTRS.each_with_object({}) { |key, hash|
65
+        if value = __send__(key)
66
+          hash[key] = value
67
+        end
68
+      }.to_json(options)
69
+    end
70
+  end
71
+
72
+  class AtomLink
73
+    include SAXMachine
74
+
75
+    LINK_ATTRS.each do |attr|
76
+      attribute attr
77
+    end
78
+
79
+    def to_json(options = nil)
80
+      LINK_ATTRS.each_with_object({}) { |key, hash|
81
+        if value = __send__(key)
82
+          hash[key] = value
83
+        end
84
+      }.to_json(options)
85
+    end
86
+  end
87
+
88
+  class RssLinkElement
89
+    include SAXMachine
90
+
91
+    value :href
92
+
93
+    def to_json(options = nil)
94
+      {
95
+        href: href
96
+      }.to_json(options)
97
+    end
98
+  end
99
+
100
+  module HasAuthors
101
+    def self.included(mod)
102
+      mod.module_exec do
103
+        case name
104
+        when /RSS/
105
+          %w[
106
+            itunes:author
107
+            dc:creator
108
+            author
109
+            managingEditor
110
+          ].each do |name|
111
+            sax_config.top_level_elements[name].clear
112
+
113
+            elements name, class: RssAuthor, as: :authors
114
+          end
115
+        else
116
+          elements :author, class: AtomAuthor, as: :authors
117
+        end
118
+
119
+        def alternate_link
120
+          links.find { |link|
121
+            link.is_a?(AtomLink) &&
122
+              link.rel == 'alternate' &&
123
+              (link.type == 'text/html'|| link.type.nil?)
124
+          }
125
+        end
126
+
127
+        def url
128
+          @url ||= (alternate_link || links.first).try!(:href)
129
+        end
130
+      end
131
+    end
132
+  end
133
+
134
+  module HasEnclosure
135
+    def self.included(mod)
136
+      mod.module_exec do
137
+        sax_config.top_level_elements['enclosure'].clear
138
+
139
+        element :enclosure, class: Enclosure
140
+
141
+        def image_enclosure
142
+          case enclosure.try!(:type)
143
+          when %r{\Aimage/}
144
+            enclosure
145
+          end
146
+        end
147
+
148
+        def image
149
+          @image ||= image_enclosure.try!(:url)
150
+        end
151
+      end
152
+    end
153
+  end
154
+
155
+  module HasLinks
156
+    def self.included(mod)
157
+      mod.module_exec do
158
+        sax_config.top_level_elements['link'].clear
159
+        sax_config.collection_elements['link'].clear
160
+
161
+        case name
162
+        when /RSS/
163
+          elements :link, class: RssLinkElement, as: :rss_links
164
+
165
+          case name
166
+          when /FeedBurner/
167
+            elements :'atok10:link', class: AtomLink, as: :atom_links
168
+
169
+              def links
170
+                @links ||= [*rss_links, *atom_links]
171
+              end
172
+          else
173
+            alias_method :links, :rss_links
174
+          end
175
+        else
176
+          elements :link, class: AtomLink, as: :links
177
+        end
178
+
179
+        def alternate_link
180
+          links.find { |link|
181
+            link.is_a?(AtomLink) &&
182
+              link.rel == 'alternate' &&
183
+              (link.type == 'text/html'|| link.type.nil?)
184
+          }
185
+        end
186
+
187
+        def url
188
+          @url ||= (alternate_link || links.first).try!(:href)
189
+        end
190
+      end
191
+    end
192
+  end
193
+
194
+  module HasTimestamps
195
+    attr_reader :published, :updated
196
+
197
+    # Keep the "oldest" publish time found
198
+    def published=(value)
199
+      parsed = parse_datetime(value)
200
+      @published = parsed if !@published || parsed < @published
201
+    end
202
+
203
+    # Keep the most recent update time found
204
+    def updated=(value)
205
+      parsed = parse_datetime(value)
206
+      @updated = parsed if !@updated || parsed > @updated
207
+    end
208
+
209
+    def date_published
210
+      published.try(:iso8601)
211
+    end
212
+
213
+    def last_updated
214
+      (updated || published).try(:iso8601)
215
+    end
216
+
217
+    private
218
+
219
+    def parse_datetime(string)
220
+      DateTime.parse(string) rescue nil
221
+    end
222
+  end
223
+
224
+  module FeedEntryExtensions
225
+    def self.included(mod)
226
+      mod.module_exec do
227
+        include HasAuthors
228
+        include HasEnclosure
229
+        include HasLinks
230
+        include HasTimestamps
231
+      end
232
+    end
233
+
234
+    def id
235
+      entry_id || Digest::MD5.hexdigest(content || summary || '')
236
+    end
237
+  end
238
+
239
+  module FeedExtensions
240
+    def self.included(mod)
241
+      mod.module_exec do
242
+        include HasAuthors
243
+        include HasEnclosure
244
+        include HasLinks
245
+        include HasTimestamps
246
+
247
+        element  :id, as: :feed_id
248
+        element  :generator
249
+        elements :rights
250
+        element  :published
251
+        element  :updated
252
+        element  :icon
253
+
254
+        if /RSS/ === name
255
+          element :guid, as: :feed_id
256
+          element :copyright
257
+          element :pubDate, as: :published
258
+          element :'dc:date', as: :published
259
+          element :lastBuildDate, as: :updated
260
+          element :image, value: :url, as: :icon
261
+
262
+          def copyright
263
+            @copyright || super
264
+          end
265
+        end
266
+
267
+        sax_config.collection_elements.each_value do |collection_elements|
268
+          collection_elements.each do |collection_element|
269
+            collection_element.accessor == 'entries' &&
270
+              (entry_class = collection_element.data_class).is_a?(Class) or next
271
+
272
+            entry_class.send :include, FeedEntryExtensions
273
+          end
274
+        end
275
+      end
276
+    end
277
+
278
+    def copyright
279
+      rights.join("\n").presence
280
+    end
281
+  end
282
+
283
+  Feedjira::Feed.feed_classes.each do |feed_class|
284
+    feed_class.send :include, FeedExtensions
285
+  end
286
+end

+ 0 - 1
lib/tasks/ar_mysql_column_charset.rake

@@ -1 +0,0 @@
1
-require 'ar_mysql_column_charset'

+ 1 - 1
lib/tasks/production.rake

@@ -34,7 +34,7 @@ namespace :production do
34 34
   end
35 35
 
36 36
   task :start => :check do
37
-    puts "Startig huginn ..."
37
+    puts "Starting huginn ..."
38 38
     run_sv('start')
39 39
   end
40 40
 

+ 24 - 5
lib/utils.rb

@@ -1,5 +1,6 @@
1 1
 require 'jsonpath'
2 2
 require 'cgi'
3
+require 'addressable/uri'
3 4
 
4 5
 module Utils
5 6
   def self.unindent(s)
@@ -25,11 +26,29 @@ module Utils
25 26
     begin
26 27
       URI(uri)
27 28
     rescue URI::Error
28
-      URI(uri.to_s.gsub(/[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]+/) { |unsafe|
29
-            unsafe.bytes.each_with_object(String.new) { |uc, s|
30
-              s << sprintf('%%%02X', uc)
31
-            }
32
-          }.force_encoding(Encoding::US_ASCII))
29
+      begin
30
+        URI(uri.to_s.gsub(/[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]+/) { |unsafe|
31
+              unsafe.bytes.each_with_object(String.new) { |uc, s|
32
+                s << sprintf('%%%02X', uc)
33
+              }
34
+            }.force_encoding(Encoding::US_ASCII))
35
+      rescue URI::Error => e
36
+        begin
37
+          auri = Addressable::URI.parse(uri.to_s)
38
+        rescue
39
+          # Do not leak Addressable::URI::InvalidURIError which
40
+          # callers might not expect.
41
+          raise e
42
+        else
43
+          # Addressable::URI#normalize! modifies the query and
44
+          # fragment components beyond escaping unsafe characters, so
45
+          # avoid using it.  Otherwise `?a[]=%2F` would be normalized
46
+          # as `?a%5B%5D=/`, for example.
47
+          auri.site = auri.normalized_site
48
+          auri.path = auri.normalized_path
49
+          URI(auri.to_s)
50
+        end
51
+      end
33 52
     end
34 53
   end
35 54
 

+ 61 - 2
spec/concerns/liquid_interpolatable_spec.rb

@@ -106,6 +106,10 @@ describe LiquidInterpolatable::Filters do
106 106
       expect(@filter.to_uri(123, 'http://example.com/dir/1')).to eq(URI('http://example.com/dir/123'))
107 107
     end
108 108
 
109
+    it 'should normalize a URL' do
110
+      expect(@filter.to_uri('a[]', 'http://example.com/dir/1')).to eq(URI('http://example.com/dir/a%5B%5D'))
111
+    end
112
+
109 113
     it 'should return a URI value in interpolation' do
110 114
       expect(@agent.interpolated['foo']).to eq('/dir/1')
111 115
     end
@@ -140,8 +144,8 @@ describe LiquidInterpolatable::Filters do
140 144
       expect(@filter.uri_expand(nil)).to eq('')
141 145
       expect(@filter.uri_expand('')).to eq('')
142 146
       expect(@filter.uri_expand(5)).to eq('5')
143
-      expect(@filter.uri_expand([])).to eq('[]')
144
-      expect(@filter.uri_expand({})).to eq('{}')
147
+      expect(@filter.uri_expand([])).to eq('%5B%5D')
148
+      expect(@filter.uri_expand({})).to eq('%7B%7D')
145 149
       expect(@filter.uri_expand(URI('/'))).to eq('/')
146 150
       expect(@filter.uri_expand(URI('http:google.com'))).to eq('http:google.com')
147 151
       expect(@filter.uri_expand(URI('http:/google.com'))).to eq('http:/google.com')
@@ -264,4 +268,59 @@ describe LiquidInterpolatable::Filters do
264 268
       expect(agent.interpolated['cleaned']).to eq('FOObar ZOObar')
265 269
     end
266 270
   end
271
+
272
+  context 'as_object' do
273
+    let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
274
+
275
+    it 'returns an array that was splitted in liquid tags' do
276
+      agent.interpolation_context['something'] = 'test,string,abc'
277
+      agent.options['array'] = "{{something | split: ',' | as_object}}"
278
+      expect(agent.interpolated['array']).to eq(['test', 'string', 'abc'])
279
+    end
280
+
281
+    it 'returns an object that was not modified in liquid' do
282
+      agent.interpolation_context['something'] = {'nested' => {'abc' => 'test'}}
283
+      agent.options['object'] = "{{something.nested | as_object}}"
284
+      expect(agent.interpolated['object']).to eq({"abc" => 'test'})
285
+    end
286
+
287
+    context 'as_json' do
288
+      def ensure_safety(obj)
289
+        JSON.parse(JSON.dump(obj))
290
+      end
291
+
292
+      it 'it converts "complex" objects' do
293
+        agent.interpolation_context['something'] = {'nested' => Service.new}
294
+        agent.options['object'] = "{{something | as_object}}"
295
+        expect(agent.interpolated['object']).to eq({'nested'=> ensure_safety(Service.new.as_json)})
296
+      end
297
+
298
+      it 'works with AgentDrops' do
299
+        agent.interpolation_context['something'] = agent
300
+        agent.options['object'] = "{{something | as_object}}"
301
+        expect(agent.interpolated['object']).to eq(ensure_safety(agent.to_liquid.as_json.stringify_keys))
302
+      end
303
+
304
+      it 'works with EventDrops' do
305
+        event = Event.new(payload: {some: 'payload'}, agent: agent, created_at: Time.now)
306
+        agent.interpolation_context['something'] = event
307
+        agent.options['object'] = "{{something | as_object}}"
308
+        expect(agent.interpolated['object']).to eq(ensure_safety(event.to_liquid.as_json.stringify_keys))
309
+      end
310
+
311
+      it 'works with MatchDataDrops' do
312
+        match = "test string".match(/\A(?<word>\w+)\s(.+?)\z/)
313
+        agent.interpolation_context['something'] = match
314
+        agent.options['object'] = "{{something | as_object}}"
315
+        expect(agent.interpolated['object']).to eq(ensure_safety(match.to_liquid.as_json.stringify_keys))
316
+      end
317
+
318
+      it 'works with URIDrops' do
319
+        uri = URI.parse("https://google.com?q=test")
320
+        agent.interpolation_context['something'] = uri
321
+        agent.options['object'] = "{{something | as_object}}"
322
+        expect(agent.interpolated['object']).to eq(ensure_safety(uri.to_liquid.as_json.stringify_keys))
323
+      end
324
+    end
325
+  end
267 326
 end

+ 40 - 3
spec/controllers/admin/users_controller_spec.rb

@@ -6,8 +6,8 @@ describe Admin::UsersController do
6 6
       it 'imports the default scenario for the new user' do
7 7
         mock(DefaultScenarioImporter).import(is_a(User))
8 8
         sign_in users(:jane)
9
-        post :create, :user => {username: 'jdoe', email: 'jdoe@example.com',
10
-                             password: 's3cr3t55', password_confirmation: 's3cr3t55', admin: false }
9
+        post :create, params: {:user => {username: 'jdoe', email: 'jdoe@example.com',
10
+                                         password: 's3cr3t55', password_confirmation: 's3cr3t55', admin: false }}
11 11
       end
12 12
     end
13 13
     
@@ -15,8 +15,45 @@ describe Admin::UsersController do
15 15
       it 'does not import the default scenario' do
16 16
         stub(DefaultScenarioImporter).import(is_a(User)) { fail "Should not attempt import" }
17 17
         sign_in users(:jane)
18
-        post :create, :user => {}
18
+        post :create, params: {:user => {username: 'user'}}
19 19
       end
20 20
     end
21 21
   end
22
+
23
+  describe 'GET #switch_to_user' do
24
+    it "switches to another user" do
25
+      sign_in users(:jane)
26
+
27
+      get :switch_to_user, params: {:id => users(:bob).id}
28
+      expect(response).to redirect_to(agents_path)
29
+      expect(subject.session[:original_admin_user_id]).to eq(users(:jane).id)
30
+    end
31
+
32
+    it "does not switch if not admin" do
33
+      sign_in users(:bob)
34
+
35
+      get :switch_to_user, params: {:id => users(:jane).id}
36
+      expect(response).to redirect_to(root_path)
37
+    end
38
+  end
39
+
40
+  describe 'GET #switch_back' do
41
+    it "switches to another user and back" do
42
+      sign_in users(:jane)
43
+
44
+      get :switch_to_user, params: {:id => users(:bob).id}
45
+      expect(response).to redirect_to(agents_path)
46
+      expect(subject.session[:original_admin_user_id]).to eq(users(:jane).id)
47
+
48
+      get :switch_back
49
+      expect(response).to redirect_to(admin_users_path)
50
+      expect(subject.session[:original_admin_user_id]).to be_nil
51
+    end
52
+
53
+    it "does not switch_back without having switched" do
54
+      sign_in users(:bob)
55
+      get :switch_back
56
+      expect(response).to redirect_to(root_path)
57
+    end
58
+  end
22 59
 end

+ 9 - 9
spec/controllers/agents/dry_runs_controller_spec.rb

@@ -16,7 +16,7 @@ describe Agents::DryRunsController do
16 16
 
17 17
   describe "GET index" do
18 18
     it "does not load any events without specifing sources" do
19
-      get :index, type: 'Agents::WebsiteAgent', source_ids: []
19
+      get :index, params: {type: 'Agents::WebsiteAgent', source_ids: []}
20 20
       expect(assigns(:events)).to eq([])
21 21
     end
22 22
 
@@ -29,13 +29,13 @@ describe Agents::DryRunsController do
29 29
       end
30 30
 
31 31
       it "for new agents" do
32
-        get :index, type: 'Agents::WebsiteAgent', source_ids: [@agent.id]
32
+        get :index, params: {type: 'Agents::WebsiteAgent', source_ids: [@agent.id]}
33 33
         expect(assigns(:events)).to eq([])
34 34
       end
35 35
 
36 36
       it "for existing agents" do
37 37
         expect(@agent.events.count).not_to be(0)
38
-        expect { get :index, agent_id: @agent }.to raise_error(NoMethodError)
38
+        expect { get :index, params: {agent_id: @agent} }.to raise_error(NoMethodError)
39 39
       end
40 40
     end
41 41
 
@@ -47,12 +47,12 @@ describe Agents::DryRunsController do
47 47
       end
48 48
 
49 49
       it "load the most recent events when providing source ids" do
50
-        get :index, type: 'Agents::WebsiteAgent', source_ids: [@agent.id]
50
+        get :index, params: {type: 'Agents::WebsiteAgent', source_ids: [@agent.id]}
51 51
         expect(assigns(:events)).to eq([@agent.events.first])
52 52
       end
53 53
 
54 54
       it "loads the most recent events for a saved agent" do
55
-        get :index, agent_id: @agent
55
+        get :index, params: {agent_id: @agent}
56 56
         expect(assigns(:events)).to eq([@agent.events.first])
57 57
       end
58 58
     end
@@ -65,7 +65,7 @@ describe Agents::DryRunsController do
65 65
 
66 66
     it "does not actually create any agent, event or log" do
67 67
       expect {
68
-        post :create, agent: valid_attributes
68
+        post :create, params: {agent: valid_attributes}
69 69
       }.not_to change {
70 70
         [users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count]
71 71
       }
@@ -81,7 +81,7 @@ describe Agents::DryRunsController do
81 81
     it "does not actually update an agent" do
82 82
       agent = agents(:bob_weather_agent)
83 83
       expect {
84
-        post :create, agent_id: agent, agent: valid_attributes(name: 'New Name')
84
+        post :create, params: {agent_id: agent, agent: valid_attributes(name: 'New Name')}
85 85
       }.not_to change {
86 86
         [users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count, agent.name, agent.updated_at]
87 87
       }
@@ -93,7 +93,7 @@ describe Agents::DryRunsController do
93 93
       agent.save!
94 94
       url_from_event = "http://xkcd.com/?from_event=1".freeze
95 95
       expect {
96
-        post :create, agent_id: agent, event: { url: url_from_event }
96
+        post :create, params: {agent_id: agent, event: { url: url_from_event }.to_json}
97 97
       }.not_to change {
98 98
         [users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count, agent.name, agent.updated_at]
99 99
       }
@@ -112,7 +112,7 @@ describe Agents::DryRunsController do
112 112
       agent.memory = {fu: "bar"}
113 113
       agent.user = users(:bob)
114 114
       agent.save!
115
-      post :create, agent_id: agent, agent: valid_params
115
+      post :create, params: {agent_id: agent, agent: valid_params}
116 116
       results = assigns(:results)
117 117
       expect(results[:events][0]).to eql({"message" => "bar"})
118 118
     end

+ 69 - 48
spec/controllers/agents_controller_spec.rb

@@ -29,7 +29,7 @@ describe AgentsController do
29 29
   describe "POST handle_details_post" do
30 30
     it "passes control to handle_details_post on the agent" do
31 31
       sign_in users(:bob)
32
-      post :handle_details_post, :id => agents(:bob_manual_event_agent).to_param, :payload => { :foo => "bar" }.to_json
32
+      post :handle_details_post, params: {:id => agents(:bob_manual_event_agent).to_param, :payload => { :foo => "bar" }.to_json}
33 33
       expect(JSON.parse(response.body)).to eq({ "success" => true })
34 34
       expect(agents(:bob_manual_event_agent).events.last.payload).to eq({ 'foo' => "bar" })
35 35
     end
@@ -37,7 +37,7 @@ describe AgentsController do
37 37
     it "can only be accessed by the Agent's owner" do
38 38
       sign_in users(:jane)
39 39
       expect {
40
-        post :handle_details_post, :id => agents(:bob_manual_event_agent).to_param, :payload => { :foo => :bar }.to_json
40
+        post :handle_details_post, params: {:id => agents(:bob_manual_event_agent).to_param, :payload => { :foo => :bar }.to_json}
41 41
       }.to raise_error(ActiveRecord::RecordNotFound)
42 42
     end
43 43
   end
@@ -46,13 +46,13 @@ describe AgentsController do
46 46
     it "triggers Agent.async_check with the Agent's ID" do
47 47
       sign_in users(:bob)
48 48
       mock(Agent).async_check(agents(:bob_manual_event_agent).id)
49
-      post :run, :id => agents(:bob_manual_event_agent).to_param
49
+      post :run, params: {:id => agents(:bob_manual_event_agent).to_param}
50 50
     end
51 51
 
52 52
     it "can only be accessed by the Agent's owner" do
53 53
       sign_in users(:jane)
54 54
       expect {
55
-        post :run, :id => agents(:bob_manual_event_agent).to_param
55
+        post :run, params: {:id => agents(:bob_manual_event_agent).to_param}
56 56
       }.to raise_error(ActiveRecord::RecordNotFound)
57 57
     end
58 58
   end
@@ -62,7 +62,7 @@ describe AgentsController do
62 62
       sign_in users(:bob)
63 63
       agent_event = events(:bob_website_agent_event).id
64 64
       other_event = events(:jane_website_agent_event).id
65
-      post :remove_events, :id => agents(:bob_website_agent).to_param
65
+      post :remove_events, params: {:id => agents(:bob_website_agent).to_param}
66 66
       expect(Event.where(:id => agent_event).count).to eq(0)
67 67
       expect(Event.where(:id => other_event).count).to eq(1)
68 68
     end
@@ -70,7 +70,7 @@ describe AgentsController do
70 70
     it "can only be accessed by the Agent's owner" do
71 71
       sign_in users(:jane)
72 72
       expect {
73
-        post :remove_events, :id => agents(:bob_website_agent).to_param
73
+        post :remove_events, params: {:id => agents(:bob_website_agent).to_param}
74 74
       }.to raise_error(ActiveRecord::RecordNotFound)
75 75
     end
76 76
   end
@@ -110,11 +110,11 @@ describe AgentsController do
110 110
   describe "GET show" do
111 111
     it "only shows Agents for the current user" do
112 112
       sign_in users(:bob)
113
-      get :show, :id => agents(:bob_website_agent).to_param
113
+      get :show, params: {:id => agents(:bob_website_agent).to_param}
114 114
       expect(assigns(:agent)).to eq(agents(:bob_website_agent))
115 115
 
116 116
       expect {
117
-        get :show, :id => agents(:jane_website_agent).to_param
117
+        get :show, params: {:id => agents(:jane_website_agent).to_param}
118 118
       }.to raise_error(ActiveRecord::RecordNotFound)
119 119
     end
120 120
   end
@@ -123,7 +123,7 @@ describe AgentsController do
123 123
     describe "with :id" do
124 124
       it "opens a clone of a given Agent" do
125 125
         sign_in users(:bob)
126
-        get :new, :id => agents(:bob_website_agent).to_param
126
+        get :new, params: {:id => agents(:bob_website_agent).to_param}
127 127
         expect(assigns(:agent).attributes).to eq(users(:bob).agents.build_clone(agents(:bob_website_agent)).attributes)
128 128
       end
129 129
 
@@ -131,7 +131,7 @@ describe AgentsController do
131 131
         sign_in users(:bob)
132 132
 
133 133
         expect {
134
-          get :new, :id => agents(:jane_website_agent).to_param
134
+          get :new, params: {:id => agents(:jane_website_agent).to_param}
135 135
         }.to raise_error(ActiveRecord::RecordNotFound)
136 136
       end
137 137
     end
@@ -139,13 +139,13 @@ describe AgentsController do
139 139
     describe "with a scenario_id" do
140 140
       it 'populates the assigned agent with the scenario' do
141 141
         sign_in users(:bob)
142
-        get :new, :scenario_id => scenarios(:bob_weather).id
142
+        get :new, params: {:scenario_id => scenarios(:bob_weather).id}
143 143
         expect(assigns(:agent).scenario_ids).to eq([scenarios(:bob_weather).id])
144 144
       end
145 145
 
146 146
       it "does not see other user's scenarios" do
147 147
         sign_in users(:bob)
148
-        get :new, :scenario_id => scenarios(:jane_weather).id
148
+        get :new, params: {:scenario_id => scenarios(:jane_weather).id}
149 149
         expect(assigns(:agent).scenario_ids).to eq([])
150 150
       end
151 151
     end
@@ -154,11 +154,11 @@ describe AgentsController do
154 154
   describe "GET edit" do
155 155
     it "only shows Agents for the current user" do
156 156
       sign_in users(:bob)
157
-      get :edit, :id => agents(:bob_website_agent).to_param
157
+      get :edit, params: {:id => agents(:bob_website_agent).to_param}
158 158
       expect(assigns(:agent)).to eq(agents(:bob_website_agent))
159 159
 
160 160
       expect {
161
-        get :edit, :id => agents(:jane_website_agent).to_param
161
+        get :edit, params: {:id => agents(:jane_website_agent).to_param}
162 162
       }.to raise_error(ActiveRecord::RecordNotFound)
163 163
     end
164 164
   end
@@ -167,27 +167,27 @@ describe AgentsController do
167 167
     it "errors on bad types" do
168 168
       sign_in users(:bob)
169 169
       expect {
170
-        post :create, :agent => valid_attributes(:type => "Agents::ThisIsFake")
170
+        post :create, params: {:agent => valid_attributes(:type => "Agents::ThisIsFake")}
171 171
       }.not_to change { users(:bob).agents.count }
172 172
       expect(assigns(:agent)).to be_a(Agent)
173 173
       expect(assigns(:agent)).to have(1).error_on(:type)
174 174
 
175 175
       sign_in users(:bob)
176 176
       expect {
177
-        post :create, :agent => valid_attributes(:type => "Object")
177
+        post :create, params: {:agent => valid_attributes(:type => "Object")}
178 178
       }.not_to change { users(:bob).agents.count }
179 179
       expect(assigns(:agent)).to be_a(Agent)
180 180
       expect(assigns(:agent)).to have(1).error_on(:type)
181 181
       sign_in users(:bob)
182 182
 
183 183
       expect {
184
-        post :create, :agent => valid_attributes(:type => "Agent")
184
+        post :create, params: {:agent => valid_attributes(:type => "Agent")}
185 185
       }.not_to change { users(:bob).agents.count }
186 186
       expect(assigns(:agent)).to be_a(Agent)
187 187
       expect(assigns(:agent)).to have(1).error_on(:type)
188 188
 
189 189
       expect {
190
-        post :create, :agent => valid_attributes(:type => "User")
190
+        post :create, params: {:agent => valid_attributes(:type => "User")}
191 191
       }.not_to change { users(:bob).agents.count }
192 192
       expect(assigns(:agent)).to be_a(Agent)
193 193
       expect(assigns(:agent)).to have(1).error_on(:type)
@@ -197,7 +197,7 @@ describe AgentsController do
197 197
       sign_in users(:bob)
198 198
       expect {
199 199
         expect {
200
-          post :create, :agent => valid_attributes
200
+          post :create, params: {:agent => valid_attributes}
201 201
         }.to change { users(:bob).agents.count }.by(1)
202 202
       }.to change { Link.count }.by(1)
203 203
       expect(assigns(:agent)).to be_a(Agents::WebsiteAgent)
@@ -205,11 +205,11 @@ describe AgentsController do
205 205
 
206 206
     it "creates Agents and accepts specifing a target agent" do
207 207
       sign_in users(:bob)
208
-      attributes = valid_attributes
208
+      attributes = valid_attributes(service_id: 1)
209 209
       attributes[:receiver_ids] = attributes[:source_ids]
210 210
       expect {
211 211
         expect {
212
-          post :create, :agent => attributes
212
+          post :create, params: {:agent => attributes}
213 213
         }.to change { users(:bob).agents.count }.by(1)
214 214
       }.to change { Link.count }.by(2)
215 215
       expect(assigns(:agent)).to be_a(Agents::WebsiteAgent)
@@ -218,7 +218,7 @@ describe AgentsController do
218 218
     it "shows errors" do
219 219
       sign_in users(:bob)
220 220
       expect {
221
-        post :create, :agent => valid_attributes(:name => "")
221
+        post :create, params: {:agent => valid_attributes(:name => "")}
222 222
       }.not_to change { users(:bob).agents.count }
223 223
       expect(assigns(:agent)).to have(1).errors_on(:name)
224 224
       expect(response).to render_template("new")
@@ -228,7 +228,7 @@ describe AgentsController do
228 228
       sign_in users(:bob)
229 229
       expect {
230 230
         expect {
231
-          post :create, :agent => valid_attributes(:source_ids => [agents(:jane_weather_agent).id])
231
+          post :create, params: {:agent => valid_attributes(:source_ids => [agents(:jane_weather_agent).id])}
232 232
         }.not_to change { users(:bob).agents.count }
233 233
       }.not_to change { Link.count }
234 234
     end
@@ -237,25 +237,25 @@ describe AgentsController do
237 237
   describe "PUT update" do
238 238
     it "does not allow changing types" do
239 239
       sign_in users(:bob)
240
-      post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:type => "Agents::WeatherAgent")
240
+      post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:type => "Agents::WeatherAgent")}
241 241
       expect(assigns(:agent)).to have(1).errors_on(:type)
242 242
       expect(response).to render_template("edit")
243 243
     end
244 244
 
245 245
     it "updates attributes on Agents for the current user" do
246 246
       sign_in users(:bob)
247
-      post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name")
247
+      post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name")}
248 248
       expect(response).to redirect_to(agents_path)
249 249
       expect(agents(:bob_website_agent).reload.name).to eq("New name")
250 250
 
251 251
       expect {
252
-        post :update, :id => agents(:jane_website_agent).to_param, :agent => valid_attributes(:name => "New name")
252
+        post :update, params: {:id => agents(:jane_website_agent).to_param, :agent => valid_attributes(:name => "New name")}
253 253
       }.to raise_error(ActiveRecord::RecordNotFound)
254 254
     end
255 255
 
256 256
     it "accepts JSON requests" do
257 257
       sign_in users(:bob)
258
-      post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :format => :json
258
+      post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name")}, :format => :json
259 259
       expect(agents(:bob_website_agent).reload.name).to eq("New name")
260 260
       expect(JSON.parse(response.body)['name']).to eq("New name")
261 261
       expect(response).to be_success
@@ -263,51 +263,58 @@ describe AgentsController do
263 263
 
264 264
     it "will not accept Agent sources owned by other users" do
265 265
       sign_in users(:bob)
266
-      post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:source_ids => [agents(:jane_weather_agent).id])
266
+      post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:source_ids => [agents(:jane_weather_agent).id])}
267 267
       expect(assigns(:agent)).to have(1).errors_on(:sources)
268 268
     end
269 269
 
270 270
     it "will not accept Scenarios owned by other users" do
271 271
       sign_in users(:bob)
272
-      post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:scenario_ids => [scenarios(:jane_weather).id])
272
+      post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:scenario_ids => [scenarios(:jane_weather).id])}
273 273
       expect(assigns(:agent)).to have(1).errors_on(:scenarios)
274 274
     end
275 275
 
276 276
     it "shows errors" do
277 277
       sign_in users(:bob)
278
-      post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "")
278
+      post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "")}
279 279
       expect(assigns(:agent)).to have(1).errors_on(:name)
280 280
       expect(response).to render_template("edit")
281 281
     end
282 282
 
283
+    it 'does not allow to modify the agents user_id' do
284
+      sign_in users(:bob)
285
+      expect {
286
+        post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:user_id => users(:jane).id)}
287
+      }.to raise_error(ActionController::UnpermittedParameters)
288
+    end
289
+
283 290
     describe "redirecting back" do
284 291
       before do
285 292
         sign_in users(:bob)
286 293
       end
287 294
 
288 295
       it "can redirect back to the show path" do
289
-        post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "show"
296
+        post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "show"}
290 297
         expect(response).to redirect_to(agent_path(agents(:bob_website_agent)))
291 298
       end
292 299
 
293 300
       it "redirect back to the index path by default" do
294
-        post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name")
301
+        post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name")}
295 302
         expect(response).to redirect_to(agents_path)
296 303
       end
297 304
 
298 305
       it "accepts return paths to scenarios" do
299
-        post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenarios/2"
306
+        post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenarios/2"}
300 307
         expect(response).to redirect_to("/scenarios/2")
301 308
       end
302 309
 
303 310
       it "sanitizes return paths" do
304
-        post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenar"
311
+        post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenar"}
305 312
         expect(response).to redirect_to(agents_path)
306 313
 
307
-        post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "http://google.com"
314
+        post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "http://google.com"}
308 315
         expect(response).to redirect_to(agents_path)
309 316
 
310
-        post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "javascript:alert(1)"
317
+        post :update, params: {:id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "javascript:alert(1)"}
311 318
         expect(response).to redirect_to(agents_path)
312 319
       end
313 320
     end
@@ -318,7 +325,7 @@ describe AgentsController do
318 325
       agent.disabled = true
319 326
       agent.last_checked_event_id = nil
320 327
       agent.save!
321
-      post :update, id: agents(:bob_website_agent).to_param, agent: { disabled: 'false', drop_pending_events: 'true' }
328
+      post :update, params: {id: agents(:bob_website_agent).to_param, agent: { disabled: 'false', drop_pending_events: 'true' }}
322 329
       agent.reload
323 330
       expect(agent.disabled).to eq(false)
324 331
       expect(agent.last_checked_event_id).to eq(Event.maximum(:id))
@@ -330,13 +337,13 @@ describe AgentsController do
330 337
       sign_in users(:bob)
331 338
 
332 339
       expect(agents(:bob_weather_agent).scenarios).to include(scenarios(:bob_weather))
333
-      put :leave_scenario, :id => agents(:bob_weather_agent).to_param, :scenario_id => scenarios(:bob_weather).to_param
340
+      put :leave_scenario, params: {:id => agents(:bob_weather_agent).to_param, :scenario_id => scenarios(:bob_weather).to_param}
334 341
       expect(agents(:bob_weather_agent).scenarios).not_to include(scenarios(:bob_weather))
335 342
 
336 343
       expect(Scenario.where(:id => scenarios(:bob_weather).id)).to exist
337 344
 
338 345
       expect {
339
-        put :leave_scenario, :id => agents(:jane_weather_agent).to_param, :scenario_id => scenarios(:jane_weather).to_param
346
+        put :leave_scenario, params: {:id => agents(:jane_weather_agent).to_param, :scenario_id => scenarios(:jane_weather).to_param}
340 347
       }.to raise_error(ActiveRecord::RecordNotFound)
341 348
     end
342 349
   end
@@ -345,25 +352,25 @@ describe AgentsController do
345 352
     it "destroys only Agents owned by the current user" do
346 353
       sign_in users(:bob)
347 354
       expect {
348
-        delete :destroy, :id => agents(:bob_website_agent).to_param
355
+        delete :destroy, params: {:id => agents(:bob_website_agent).to_param}
349 356
       }.to change(Agent, :count).by(-1)
350 357
 
351 358
       expect {
352
-        delete :destroy, :id => agents(:jane_website_agent).to_param
359
+        delete :destroy, params: {:id => agents(:jane_website_agent).to_param}
353 360
       }.to raise_error(ActiveRecord::RecordNotFound)
354 361
     end
355 362
 
356 363
     it "redirects correctly when the Agent is deleted from the Agent itself" do
357 364
       sign_in users(:bob)
358 365
 
359
-      delete :destroy, :id => agents(:bob_website_agent).to_param
366
+      delete :destroy, params: {:id => agents(:bob_website_agent).to_param}
360 367
       expect(response).to redirect_to agents_path
361 368
     end
362 369
 
363 370
     it "redirects correctly when the Agent is deleted from a Scenario" do
364 371
       sign_in users(:bob)
365 372
 
366
-      delete :destroy, :id => agents(:bob_weather_agent).to_param, :return => scenario_path(scenarios(:bob_weather)).to_param
373
+      delete :destroy, params: {:id => agents(:bob_weather_agent).to_param, :return => scenario_path(scenarios(:bob_weather)).to_param}
367 374
       expect(response).to redirect_to scenario_path(scenarios(:bob_weather))
368 375
     end
369 376
   end
@@ -380,7 +387,7 @@ describe AgentsController do
380 387
           stub(klass).validate_option { true }
381 388
         end
382 389
 
383
-        post :validate, @params
390
+        post :validate, params: @params
384 391
         expect(response.status).to eq 200
385 392
       end
386 393
 
@@ -389,7 +396,7 @@ describe AgentsController do
389 396
           stub(klass).validate_option { false }
390 397
         end
391 398
 
392
-        post :validate, @params
399
+        post :validate, params: @params
393 400
         expect(response.status).to eq 403
394 401
       end
395 402
     end
@@ -400,7 +407,7 @@ describe AgentsController do
400 407
           stub(klass).complete_option { [{name: 'test', value: 1}] }
401 408
         end
402 409
 
403
-        post :complete, @params
410
+        post :complete, params: @params
404 411
         expect(response.status).to eq 200
405 412
         expect(response.header['Content-Type']).to include('application/json')
406 413
 
@@ -413,7 +420,7 @@ describe AgentsController do
413 420
       agent = agents(:bob_website_agent)
414 421
       agent.update!(memory: { "test" => 42 })
415 422
       sign_in users(:bob)
416
-      delete :destroy_memory, id: agent.to_param
423
+      delete :destroy_memory, params: {id: agent.to_param}
417 424
       expect(agent.reload.memory).to eq({})
418 425
     end
419 426
 
@@ -422,9 +429,23 @@ describe AgentsController do
422 429
       agent.update!(memory: { "test" => 42 })
423 430
       sign_in users(:bob)
424 431
       expect {
425
-        delete :destroy_memory, id: agent.to_param
432
+        delete :destroy_memory, params: {id: agent.to_param}
426 433
       }.to raise_error(ActiveRecord::RecordNotFound)
427 434
       expect(agent.reload.memory).to eq({ "test" => 42})
428 435
     end
429 436
   end
437
+
438
+  describe 'DELETE undefined' do
439
+    it 'removes an undefined agent from the database' do
440
+      sign_in users(:bob)
441
+      agent = agents(:bob_website_agent)
442
+      agent.update_attribute(:type, 'Agents::UndefinedAgent')
443
+      agent2 = agents(:jane_website_agent)
444
+      agent2.update_attribute(:type, 'Agents::UndefinedAgent')
445
+
446
+      expect {
447
+        delete :destroy_undefined
448
+      }.to change { Agent.count }.by(-1)
449
+    end
450
+  end
430 451
 end

+ 8 - 8
spec/controllers/events_controller_spec.rb

@@ -15,12 +15,12 @@ describe EventsController do
15 15
 
16 16
     it "can filter by Agent" do
17 17
       sign_in users(:bob)
18
-      get :index, :agent_id => agents(:bob_website_agent)
18
+      get :index, params: {:agent_id => agents(:bob_website_agent)}
19 19
       expect(assigns(:events).length).to eq(agents(:bob_website_agent).events.length)
20 20
       expect(assigns(:events).all? {|i| expect(i.agent).to eq(agents(:bob_website_agent)) }).to be_truthy
21 21
 
22 22
       expect {
23
-        get :index, :agent_id => agents(:jane_website_agent)
23
+        get :index, params: {:agent_id => agents(:jane_website_agent)}
24 24
       }.to raise_error(ActiveRecord::RecordNotFound)
25 25
     end
26 26
   end
@@ -28,11 +28,11 @@ describe EventsController do
28 28
   describe "GET show" do
29 29
     it "only shows Events for the current user" do
30 30
       sign_in users(:bob)
31
-      get :show, :id => events(:bob_website_agent_event).to_param
31
+      get :show, params: {:id => events(:bob_website_agent_event).to_param}
32 32
       expect(assigns(:event)).to eq(events(:bob_website_agent_event))
33 33
 
34 34
       expect {
35
-        get :show, :id => events(:jane_website_agent_event).to_param
35
+        get :show, params: {:id => events(:jane_website_agent_event).to_param}
36 36
       }.to raise_error(ActiveRecord::RecordNotFound)
37 37
     end
38 38
   end
@@ -45,7 +45,7 @@ describe EventsController do
45 45
 
46 46
     it "clones and re-emits events" do
47 47
       expect {
48
-        post :reemit, :id => events(:bob_website_agent_event).to_param
48
+        post :reemit, params: {:id => events(:bob_website_agent_event).to_param}
49 49
       }.to change { Event.count }.by(1)
50 50
       expect(Event.last.payload).to eq(events(:bob_website_agent_event).payload)
51 51
       expect(Event.last.agent).to eq(events(:bob_website_agent_event).agent)
@@ -54,7 +54,7 @@ describe EventsController do
54 54
 
55 55
     it "can only re-emit Events for the current user" do
56 56
       expect {
57
-        post :reemit, :id => events(:jane_website_agent_event).to_param
57
+        post :reemit, params: {:id => events(:jane_website_agent_event).to_param}
58 58
       }.to raise_error(ActiveRecord::RecordNotFound)
59 59
     end
60 60
   end
@@ -63,11 +63,11 @@ describe EventsController do
63 63
     it "only deletes events for the current user" do
64 64
       sign_in users(:bob)
65 65
       expect {
66
-        delete :destroy, :id => events(:bob_website_agent_event).to_param
66
+        delete :destroy, params: {:id => events(:bob_website_agent_event).to_param}
67 67
       }.to change { Event.count }.by(-1)
68 68
 
69 69
       expect {
70
-        delete :destroy, :id => events(:jane_website_agent_event).to_param
70
+        delete :destroy, params: {:id => events(:jane_website_agent_event).to_param}
71 71
       }.to raise_error(ActiveRecord::RecordNotFound)
72 72
     end
73 73
   end

+ 5 - 5
spec/controllers/jobs_controller_spec.rb

@@ -37,11 +37,11 @@ describe JobsController do
37 37
     end
38 38
 
39 39
     it "destroy a job which is not running" do
40
-      expect { delete :destroy, id: @not_running.id }.to change(Delayed::Job, :count).by(-1)
40
+      expect { delete :destroy, params: {id: @not_running.id} }.to change(Delayed::Job, :count).by(-1)
41 41
     end
42 42
 
43 43
     it "does not destroy a running job" do
44
-      expect { delete :destroy, id: @running.id }.to change(Delayed::Job, :count).by(0)
44
+      expect { delete :destroy, params: {id: @running.id} }.to change(Delayed::Job, :count).by(0)
45 45
     end
46 46
   end
47 47
 
@@ -54,15 +54,15 @@ describe JobsController do
54 54
     end
55 55
 
56 56
     it "queue a job which is not running" do
57
-      expect { put :run, id: @not_running.id }.to change { @not_running.reload.run_at }
57
+      expect { put :run, params: {id: @not_running.id} }.to change { @not_running.reload.run_at }
58 58
     end
59 59
 
60 60
     it "queue a job that failed" do
61
-      expect { put :run, id: @failed.id }.to change { @failed.reload.run_at }
61
+      expect { put :run, params: {id: @failed.id} }.to change { @failed.reload.run_at }
62 62
     end
63 63
 
64 64
     it "not queue a running job" do
65
-      expect { put :run, id: @running.id }.not_to change { @not_running.reload.run_at }
65
+      expect { put :run, params: {id: @running.id} }.not_to change { @not_running.reload.run_at }
66 66
     end
67 67
   end
68 68
 

+ 4 - 4
spec/controllers/logs_controller_spec.rb

@@ -4,7 +4,7 @@ describe LogsController do
4 4
   describe "GET index" do
5 5
     it "can filter by Agent" do
6 6
       sign_in users(:bob)
7
-      get :index, :agent_id => agents(:bob_weather_agent).id
7
+      get :index, params: {:agent_id => agents(:bob_weather_agent).id}
8 8
       expect(assigns(:logs).length).to eq(agents(:bob_weather_agent).logs.length)
9 9
       expect(assigns(:logs).all? {|i| expect(i.agent).to eq(agents(:bob_weather_agent)) }).to be_truthy
10 10
     end
@@ -12,7 +12,7 @@ describe LogsController do
12 12
     it "only loads Agents owned by the current user" do
13 13
       sign_in users(:bob)
14 14
       expect {
15
-        get :index, :agent_id => agents(:jane_weather_agent).id
15
+        get :index, params: {:agent_id => agents(:jane_weather_agent).id}
16 16
       }.to raise_error(ActiveRecord::RecordNotFound)
17 17
     end
18 18
   end
@@ -22,7 +22,7 @@ describe LogsController do
22 22
       agents(:bob_weather_agent).last_error_log_at = 2.hours.ago
23 23
       sign_in users(:bob)
24 24
       expect {
25
-        delete :clear, :agent_id => agents(:bob_weather_agent).id
25
+        delete :clear, params: {:agent_id => agents(:bob_weather_agent).id}
26 26
       }.to change { AgentLog.count }.by(-1 * agents(:bob_weather_agent).logs.count)
27 27
       expect(assigns(:logs).length).to eq(0)
28 28
       expect(agents(:bob_weather_agent).reload.logs.count).to eq(0)
@@ -32,7 +32,7 @@ describe LogsController do
32 32
     it "only deletes logs for an Agent owned by the current user" do
33 33
       sign_in users(:bob)
34 34
       expect {
35
-        delete :clear, :agent_id => agents(:jane_weather_agent).id
35
+        delete :clear, params: {:agent_id => agents(:jane_weather_agent).id}
36 36
       }.to raise_error(ActiveRecord::RecordNotFound)
37 37
     end
38 38
   end

+ 9 - 8
spec/controllers/omniauth_callbacks_controller_spec.rb

@@ -5,22 +5,23 @@ describe OmniauthCallbacksController do
5 5
     sign_in users(:bob)
6 6
     OmniAuth.config.test_mode = true
7 7
     request.env["devise.mapping"] = Devise.mappings[:user]
8
-    request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
9 8
   end
10 9
 
11 10
   describe "accepting a callback url" do
12 11
     it "should update the user's credentials" do
12
+      request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
13 13
       expect {
14 14
         get :twitter
15 15
       }.to change { users(:bob).services.count }.by(1)
16 16
     end
17
+  end
17 18
 
18
-    # it "should work with an unknown provider (for now)" do
19
-    #   request.env["omniauth.auth"]['provider'] = 'unknown'
20
-    #   expect {
21
-    #     get :unknown
22
-    #   }.to change { users(:bob).services.count }.by(1)
23
-    #   expect(users(:bob).services.first.provider).to eq('unknown')
24
-    # end
19
+  describe "handling a provider with non-standard omniauth options" do
20
+    it "should update the user's credentials" do
21
+      request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/37signals.json')))
22
+      expect {
23
+        get "37signals"
24
+      }.to change { users(:bob).services.count }.by(1)
25
+    end
25 26
   end
26 27
 end

+ 1 - 1
spec/controllers/scenario_imports_controller_spec.rb

@@ -15,7 +15,7 @@ describe ScenarioImportsController do
15 15
 
16 16
   describe "POST create" do
17 17
     it "initializes a ScenarioImport for current_user, passing in params" do
18
-      post :create, :scenario_import => { :url => "bad url" }
18
+      post :create, params: {:scenario_import => { :url => "bad url" }}
19 19
       expect(assigns(:scenario_import).user).to eq(users(:bob))
20 20
       expect(assigns(:scenario_import).url).to eq("bad url")
21 21
       expect(assigns(:scenario_import)).not_to be_valid

+ 29 - 23
spec/controllers/scenarios_controller_spec.rb

@@ -18,34 +18,34 @@ describe ScenariosController do
18 18
 
19 19
   describe "GET show" do
20 20
     it "only shows Scenarios for the current user" do
21
-      get :show, :id => scenarios(:bob_weather).to_param
21
+      get :show, params: {:id => scenarios(:bob_weather).to_param}
22 22
       expect(assigns(:scenario)).to eq(scenarios(:bob_weather))
23 23
 
24 24
       expect {
25
-        get :show, :id => scenarios(:jane_weather).to_param
25
+        get :show, params: {:id => scenarios(:jane_weather).to_param}
26 26
       }.to raise_error(ActiveRecord::RecordNotFound)
27 27
     end
28 28
 
29 29
     it "loads Agents for the requested Scenario" do
30
-      get :show, :id => scenarios(:bob_weather).to_param
30
+      get :show, params: {:id => scenarios(:bob_weather).to_param}
31 31
       expect(assigns(:agents).pluck(:id).sort).to eq(scenarios(:bob_weather).agents.pluck(:id).sort)
32 32
     end
33 33
   end
34 34
 
35 35
   describe "GET share" do
36 36
     it "only displays Scenario share information for the current user" do
37
-      get :share, :id => scenarios(:bob_weather).to_param
37
+      get :share, params: {:id => scenarios(:bob_weather).to_param}
38 38
       expect(assigns(:scenario)).to eq(scenarios(:bob_weather))
39 39
 
40 40
       expect {
41
-        get :share, :id => scenarios(:jane_weather).to_param
41
+        get :share, params: {:id => scenarios(:jane_weather).to_param}
42 42
       }.to raise_error(ActiveRecord::RecordNotFound)
43 43
     end
44 44
   end
45 45
 
46 46
   describe "GET export" do
47 47
     it "returns a JSON file download from an instantiated AgentsExporter" do
48
-      get :export, :id => scenarios(:bob_weather).to_param
48
+      get :export, params: {:id => scenarios(:bob_weather).to_param}
49 49
       expect(assigns(:exporter).options[:name]).to eq(scenarios(:bob_weather).name)
50 50
       expect(assigns(:exporter).options[:description]).to eq(scenarios(:bob_weather).description)
51 51
       expect(assigns(:exporter).options[:agents]).to eq(scenarios(:bob_weather).agents)
@@ -59,11 +59,11 @@ describe ScenariosController do
59 59
     end
60 60
 
61 61
     it "only exports private Scenarios for the current user" do
62
-      get :export, :id => scenarios(:bob_weather).to_param
62
+      get :export, params: {:id => scenarios(:bob_weather).to_param}
63 63
       expect(assigns(:scenario)).to eq(scenarios(:bob_weather))
64 64
 
65 65
       expect {
66
-        get :export, :id => scenarios(:jane_weather).to_param
66
+        get :export, params: {:id => scenarios(:jane_weather).to_param}
67 67
       }.to raise_error(ActiveRecord::RecordNotFound)
68 68
     end
69 69
 
@@ -73,14 +73,14 @@ describe ScenariosController do
73 73
       end
74 74
 
75 75
       it "exports public scenarios for other users when logged in" do
76
-        get :export, :id => scenarios(:jane_weather).to_param
76
+        get :export, params: {:id => scenarios(:jane_weather).to_param}
77 77
         expect(assigns(:scenario)).to eq(scenarios(:jane_weather))
78 78
         expect(assigns(:exporter).options[:source_url]).to eq(export_scenario_url(scenarios(:jane_weather)))
79 79
       end
80 80
 
81 81
       it "exports public scenarios for other users when logged out" do
82 82
         sign_out :user
83
-        get :export, :id => scenarios(:jane_weather).to_param
83
+        get :export, params: {:id => scenarios(:jane_weather).to_param}
84 84
         expect(assigns(:scenario)).to eq(scenarios(:jane_weather))
85 85
         expect(assigns(:exporter).options[:source_url]).to eq(export_scenario_url(scenarios(:jane_weather)))
86 86
       end
@@ -89,11 +89,11 @@ describe ScenariosController do
89 89
 
90 90
   describe "GET edit" do
91 91
     it "only shows Scenarios for the current user" do
92
-      get :edit, :id => scenarios(:bob_weather).to_param
92
+      get :edit, params: {:id => scenarios(:bob_weather).to_param}
93 93
       expect(assigns(:scenario)).to eq(scenarios(:bob_weather))
94 94
 
95 95
       expect {
96
-        get :edit, :id => scenarios(:jane_weather).to_param
96
+        get :edit, params: {:id => scenarios(:jane_weather).to_param}
97 97
       }.to raise_error(ActiveRecord::RecordNotFound)
98 98
     end
99 99
   end
@@ -101,13 +101,13 @@ describe ScenariosController do
101 101
   describe "POST create" do
102 102
     it "creates Scenarios for the current user" do
103 103
       expect {
104
-        post :create, :scenario => valid_attributes
104
+        post :create, params: {:scenario => valid_attributes}
105 105
       }.to change { users(:bob).scenarios.count }.by(1)
106 106
     end
107 107
 
108 108
     it "shows errors" do
109 109
       expect {
110
-        post :create, :scenario => valid_attributes(:name => "")
110
+        post :create, params: {:scenario => valid_attributes(:name => "")}
111 111
       }.not_to change { users(:bob).scenarios.count }
112 112
       expect(assigns(:scenario)).to have(1).errors_on(:name)
113 113
       expect(response).to render_template("new")
@@ -115,35 +115,41 @@ describe ScenariosController do
115 115
 
116 116
     it "will not create Scenarios for other users" do
117 117
       expect {
118
-        post :create, :scenario => valid_attributes(:user_id => users(:jane).id)
119
-      }.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
118
+        post :create, params: {:scenario => valid_attributes(:user_id => users(:jane).id)}
119
+      }.to raise_error(ActionController::UnpermittedParameters)
120 120
     end
121 121
   end
122 122
 
123 123
   describe "PUT update" do
124 124
     it "updates attributes on Scenarios for the current user" do
125
-      post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name", :public => "1" }
125
+      post :update, params: {:id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name", :public => "1" }}
126 126
       expect(response).to redirect_to(scenario_path(scenarios(:bob_weather)))
127 127
       expect(scenarios(:bob_weather).reload.name).to eq("new_name")
128 128
       expect(scenarios(:bob_weather)).to be_public
129 129
 
130 130
       expect {
131
-        post :update, :id => scenarios(:jane_weather).to_param, :scenario => { :name => "new_name" }
131
+        post :update, params: {:id => scenarios(:jane_weather).to_param, :scenario => { :name => "new_name" }}
132 132
       }.to raise_error(ActiveRecord::RecordNotFound)
133 133
       expect(scenarios(:jane_weather).reload.name).not_to eq("new_name")
134 134
     end
135 135
 
136 136
     it "shows errors" do
137
-      post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "" }
137
+      post :update, params: {:id => scenarios(:bob_weather).to_param, :scenario => { :name => "" }}
138 138
       expect(assigns(:scenario)).to have(1).errors_on(:name)
139 139
       expect(response).to render_template("edit")
140 140
     end
141
+
142
+    it 'adds an agent to the scenario' do
143
+      expect {
144
+        post :update, params: {:id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name", :public => "1", agent_ids: scenarios(:bob_weather).agent_ids + [agents(:bob_website_agent).id] }}
145
+      }.to change { scenarios(:bob_weather).reload.agent_ids.length }.by(1)
146
+    end
141 147
   end
142 148
 
143 149
   describe 'PUT enable_or_disable_all_agents' do
144 150
     it 'updates disabled on all agents in a scenario for the current user' do
145 151
       @params = {"scenario"=>{"disabled"=>"true"}, "commit"=>"Yes", "id"=> scenarios(:bob_weather).id}
146
-      put :enable_or_disable_all_agents, @params
152
+      put :enable_or_disable_all_agents, params: @params
147 153
       expect(agents(:bob_rain_notifier_agent).disabled).to eq(true)
148 154
       expect(response).to redirect_to(scenario_path(scenarios(:bob_weather)))
149 155
     end
@@ -152,17 +158,17 @@ describe ScenariosController do
152 158
   describe "DELETE destroy" do
153 159
     it "destroys only Scenarios owned by the current user" do
154 160
       expect {
155
-        delete :destroy, :id => scenarios(:bob_weather).to_param
161
+        delete :destroy, params: {:id => scenarios(:bob_weather).to_param}
156 162
       }.to change(Scenario, :count).by(-1)
157 163
 
158 164
       expect {
159
-        delete :destroy, :id => scenarios(:jane_weather).to_param
165
+        delete :destroy, params: {:id => scenarios(:jane_weather).to_param}
160 166
       }.to raise_error(ActiveRecord::RecordNotFound)
161 167
     end
162 168
 
163 169
     it "passes the mode to the model" do
164 170
       expect {
165
-        delete :destroy, id: scenarios(:bob_weather).to_param, mode: 'all_agents'
171
+        delete :destroy, params: {id: scenarios(:bob_weather).to_param, mode: 'all_agents'}
166 172
       }.to change(Agent, :count).by(-2)
167 173
     end
168 174
   end

+ 4 - 4
spec/controllers/services_controller_spec.rb

@@ -14,14 +14,14 @@ describe ServicesController do
14 14
 
15 15
   describe "POST toggle_availability" do
16 16
     it "should work for service of the user" do
17
-      post :toggle_availability, :id => services(:generic).to_param
17
+      post :toggle_availability, params: {:id => services(:generic).to_param}
18 18
       expect(assigns(:service)).to eq(services(:generic))
19 19
       redirect_to(services_path)
20 20
     end
21 21
 
22 22
     it "should not work for a service of another user" do
23 23
       expect {
24
-        post :toggle_availability, :id => services(:global).to_param
24
+        post :toggle_availability, params: {:id => services(:global).to_param}
25 25
       }.to raise_error(ActiveRecord::RecordNotFound)
26 26
     end
27 27
   end
@@ -29,11 +29,11 @@ describe ServicesController do
29 29
   describe "DELETE destroy" do
30 30
     it "destroys only services owned by the current user" do
31 31
       expect {
32
-        delete :destroy, :id => services(:generic).to_param
32
+        delete :destroy, params: {:id => services(:generic).to_param}
33 33
       }.to change(Service, :count).by(-1)
34 34
 
35 35
       expect {
36
-        delete :destroy, :id => services(:global).to_param
36
+        delete :destroy, params: {:id => services(:global).to_param}
37 37
       }.to raise_error(ActiveRecord::RecordNotFound)
38 38
     end
39 39
   end

+ 14 - 14
spec/controllers/user_credentials_controller_spec.rb

@@ -22,30 +22,30 @@ describe UserCredentialsController do
22 22
 
23 23
   describe "GET edit" do
24 24
     it "only shows UserCredentials for the current user" do
25
-      get :edit, :id => user_credentials(:bob_aws_secret).to_param
25
+      get :edit, params: {:id => user_credentials(:bob_aws_secret).to_param}
26 26
       expect(assigns(:user_credential)).to eq(user_credentials(:bob_aws_secret))
27 27
 
28 28
       expect {
29
-        get :edit, :id => user_credentials(:jane_aws_secret).to_param
29
+        get :edit, params: {:id => user_credentials(:jane_aws_secret).to_param}
30 30
       }.to raise_error(ActiveRecord::RecordNotFound)
31 31
     end
32 32
   end
33 33
 
34 34
   describe "Post import" do
35 35
     it "asserts user credentials were created for current user only" do
36
-      post :import, :file => @file
36
+      post :import, params: {:file => @file}
37 37
       expect(controller.current_user.id).to eq(users(:bob).id)
38 38
       expect(controller.current_user.user_credentials).to eq(users(:bob).user_credentials)
39 39
     end
40 40
 
41 41
     it "asserts that primary id in json file is ignored" do
42
-      post :import, :file => @file
42
+      post :import, params: {:file => @file}
43 43
       expect(controller.current_user.user_credentials.last.id).not_to eq(24)
44 44
     end
45 45
 
46 46
     it "duplicate credential name shows an error that it is not saved" do
47 47
       file1 = fixture_file_upload('multiple_user_credentials.json')
48
-      post :import, :file => file1
48
+      post :import, params: {:file => file1}
49 49
       expect(flash[:notice]).to eq("One or more of the uploaded credentials was not imported due to an error. Perhaps an existing credential had the same name?")
50 50
       expect(response).to redirect_to(user_credentials_path)
51 51
     end
@@ -54,13 +54,13 @@ describe UserCredentialsController do
54 54
   describe "POST create" do
55 55
     it "creates UserCredentials for the current user" do
56 56
       expect {
57
-        post :create, :user_credential => valid_attributes
57
+        post :create, params: {:user_credential => valid_attributes}
58 58
       }.to change { users(:bob).user_credentials.count }.by(1)
59 59
     end
60 60
 
61 61
     it "shows errors" do
62 62
       expect {
63
-        post :create, :user_credential => valid_attributes(:credential_name => "")
63
+        post :create, params: {:user_credential => valid_attributes(:credential_name => "")}
64 64
       }.not_to change { users(:bob).user_credentials.count }
65 65
       expect(assigns(:user_credential)).to have(1).errors_on(:credential_name)
66 66
       expect(response).to render_template("new")
@@ -68,25 +68,25 @@ describe UserCredentialsController do
68 68
 
69 69
     it "will not create UserCredentials for other users" do
70 70
       expect {
71
-        post :create, :user_credential => valid_attributes(:user_id => users(:jane).id)
72
-      }.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
71
+        post :create, params: {:user_credential => valid_attributes(:user_id => users(:jane).id)}
72
+      }.to raise_error(ActionController::UnpermittedParameters)
73 73
     end
74 74
   end
75 75
 
76 76
   describe "PUT update" do
77 77
     it "updates attributes on UserCredentials for the current user" do
78
-      post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "new_name" }
78
+      post :update, params: {:id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "new_name" }}
79 79
       expect(response).to redirect_to(user_credentials_path)
80 80
       expect(user_credentials(:bob_aws_key).reload.credential_name).to eq("new_name")
81 81
 
82 82
       expect {
83
-        post :update, :id => user_credentials(:jane_aws_key).to_param, :user_credential => { :credential_name => "new_name" }
83
+        post :update, params: {:id => user_credentials(:jane_aws_key).to_param, :user_credential => { :credential_name => "new_name" }}
84 84
       }.to raise_error(ActiveRecord::RecordNotFound)
85 85
       expect(user_credentials(:jane_aws_key).reload.credential_name).not_to eq("new_name")
86 86
     end
87 87
 
88 88
     it "shows errors" do
89
-      post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "" }
89
+      post :update, params: {:id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "" }}
90 90
       expect(assigns(:user_credential)).to have(1).errors_on(:credential_name)
91 91
       expect(response).to render_template("edit")
92 92
     end
@@ -95,11 +95,11 @@ describe UserCredentialsController do
95 95
   describe "DELETE destroy" do
96 96
     it "destroys only UserCredentials owned by the current user" do
97 97
       expect {
98
-        delete :destroy, :id => user_credentials(:bob_aws_key).to_param
98
+        delete :destroy, params: {:id => user_credentials(:bob_aws_key).to_param}
99 99
       }.to change(UserCredential, :count).by(-1)
100 100
 
101 101
       expect {
102
-        delete :destroy, :id => user_credentials(:jane_aws_key).to_param
102
+        delete :destroy, params: {:id => user_credentials(:jane_aws_key).to_param}
103 103
       }.to raise_error(ActiveRecord::RecordNotFound)
104 104
     end
105 105
   end

+ 13 - 7
spec/controllers/users/registrations_controller_spec.rb

@@ -2,16 +2,19 @@ require 'rails_helper'
2 2
 
3 3
 module Users
4 4
   describe RegistrationsController do
5
-    include Devise::TestHelpers
6
-
7 5
     describe "POST create" do
6
+      before do
7
+        @request.env["devise.mapping"] = Devise.mappings[:user]
8
+      end
9
+
8 10
       context 'with valid params' do
9 11
         it "imports the default scenario for the new user" do
10 12
           mock(DefaultScenarioImporter).import(is_a(User))
11 13
 
12
-          @request.env["devise.mapping"] = Devise.mappings[:user]
13
-          post :create, :user => {username: 'jdoe', email: 'jdoe@example.com',
14
-            password: 's3cr3t55', password_confirmation: 's3cr3t55', admin: false, invitation_code: 'try-huginn'}
14
+          post :create, params: {
15
+            :user => {username: 'jdoe', email: 'jdoe@example.com',
16
+              password: 's3cr3t55', password_confirmation: 's3cr3t55', invitation_code: 'try-huginn'}
17
+          }
15 18
         end
16 19
       end
17 20
 
@@ -19,9 +22,12 @@ module Users
19 22
         it "does not import the default scenario" do
20 23
           stub(DefaultScenarioImporter).import(is_a(User)) { fail "Should not attempt import" }
21 24
 
22
-          @request.env["devise.mapping"] = Devise.mappings[:user]
23 25
           setup_controller_for_warden
24
-          post :create, :user => {}
26
+          post :create, params: {:user => {}}
27
+        end
28
+
29
+        it 'does not allow to set the admin flag' do
30
+          expect { post :create, params: {:user => {admin: 'true'}} }.to raise_error(ActionController::UnpermittedParameters)
25 31
         end
26 32
       end
27 33
     end

+ 14 - 14
spec/controllers/web_requests_controller_spec.rb

@@ -26,14 +26,14 @@ describe WebRequestsController do
26 26
 
27 27
   it "should not require login to receive a web request" do
28 28
     expect(@agent.last_web_request_at).to be_nil
29
-    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
29
+    post :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"}
30 30
     expect(@agent.reload.last_web_request_at).to be_within(2).of(Time.now)
31 31
     expect(response.body).to eq("success")
32 32
     expect(response).to be_success
33 33
   end
34 34
 
35 35
   it "should call receive_web_request" do
36
-    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
36
+    post :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"}
37 37
     @agent.reload
38 38
     expect(@agent.memory[:web_request_values]).to eq({ 'key' => "value", 'another_key' => "5" })
39 39
     expect(@agent.memory[:web_request_format]).to eq("text/html")
@@ -42,14 +42,14 @@ describe WebRequestsController do
42 42
     expect(response.headers['Content-Type']).to eq('text/plain; charset=utf-8')
43 43
     expect(response).to be_success
44 44
 
45
-    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go"
45
+    post :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go"}
46 46
     expect(@agent.reload.memory[:web_request_values]).not_to eq({ 'no' => "go" })
47 47
     expect(response.body).to eq("failure")
48 48
     expect(response).to be_missing
49 49
   end
50 50
 
51 51
   it "should accept gets" do
52
-    get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
52
+    get :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"}
53 53
     @agent.reload
54 54
     expect(@agent.memory[:web_request_values]).to eq({ 'key' => "value", 'another_key' => "5" })
55 55
     expect(@agent.memory[:web_request_format]).to eq("text/html")
@@ -59,19 +59,19 @@ describe WebRequestsController do
59 59
   end
60 60
 
61 61
   it "should pass through the received format" do
62
-    get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :json
62
+    get :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"}, :format => :json
63 63
     @agent.reload
64 64
     expect(@agent.memory[:web_request_values]).to eq({ 'key' => "value", 'another_key' => "5" })
65 65
     expect(@agent.memory[:web_request_format]).to eq("application/json")
66 66
     expect(@agent.memory[:web_request_method]).to eq("get")
67 67
 
68
-    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :xml
68
+    post :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"}, :format => :xml
69 69
     @agent.reload
70 70
     expect(@agent.memory[:web_request_values]).to eq({ 'key' => "value", 'another_key' => "5" })
71 71
     expect(@agent.memory[:web_request_format]).to eq("application/xml")
72 72
     expect(@agent.memory[:web_request_method]).to eq("post")
73 73
 
74
-    put :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :atom
74
+    put :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"}, :format => :atom
75 75
     @agent.reload
76 76
     expect(@agent.memory[:web_request_values]).to eq({ 'key' => "value", 'another_key' => "5" })
77 77
     expect(@agent.memory[:web_request_format]).to eq("application/atom+xml")
@@ -81,17 +81,17 @@ describe WebRequestsController do
81 81
   it "can accept a content-type to return" do
82 82
     @agent.memory['content_type'] = 'application/json'
83 83
     @agent.save!
84
-    get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
84
+    get :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"}
85 85
     expect(response.headers['Content-Type']).to eq('application/json; charset=utf-8')
86 86
   end
87 87
 
88 88
   it "should fail on incorrect users" do
89
-    post :handle_request, :user_id => users(:jane).to_param, :agent_id => @agent.id, :secret => "my_secret", :no => "go"
89
+    post :handle_request, params: {:user_id => users(:jane).to_param, :agent_id => @agent.id, :secret => "my_secret", :no => "go"}
90 90
     expect(response).to be_missing
91 91
   end
92 92
 
93 93
   it "should fail on incorrect agents" do
94
-    post :handle_request, :user_id => users(:bob).to_param, :agent_id => 454545, :secret => "my_secret", :no => "go"
94
+    post :handle_request, params: {:user_id => users(:bob).to_param, :agent_id => 454545, :secret => "my_secret", :no => "go"}
95 95
     expect(response).to be_missing
96 96
   end
97 97
 
@@ -102,7 +102,7 @@ describe WebRequestsController do
102 102
     end
103 103
 
104 104
     it "should create events without requiring login" do
105
-      post :update_location, user_id: users(:bob).to_param, secret: "my_secret", longitude: 123, latitude: 45, something: "else"
105
+      post :update_location, params: {user_id: users(:bob).to_param, secret: "my_secret", longitude: 123, latitude: 45, something: "else"}
106 106
       expect(@agent.events.last.payload).to eq({ 'longitude' => "123", 'latitude' => "45", 'something' => "else" })
107 107
       expect(@agent.events.last.lat).to eq(45)
108 108
       expect(@agent.events.last.lng).to eq(123)
@@ -112,13 +112,13 @@ describe WebRequestsController do
112 112
       @jane_agent = Agent.build_for_type("Agents::UserLocationAgent", users(:jane), name: "something", options: { secret: "my_secret" })
113 113
       @jane_agent.save!
114 114
 
115
-      post :update_location, user_id: users(:bob).to_param, secret: "my_secret", longitude: 123, latitude: 45, something: "else"
115
+      post :update_location, params: {user_id: users(:bob).to_param, secret: "my_secret", longitude: 123, latitude: 45, something: "else"}
116 116
       expect(@agent.events.last.payload).to eq({ 'longitude' => "123", 'latitude' => "45", 'something' => "else" })
117 117
       expect(@jane_agent.events).to be_empty
118 118
     end
119 119
 
120 120
     it "should raise a 404 error when given an invalid user id" do
121
-      post :update_location, user_id: "123", secret: "not_my_secret", longitude: 123, latitude: 45, something: "else"
121
+      post :update_location, params: {user_id: "123", secret: "not_my_secret", longitude: 123, latitude: 45, something: "else"}
122 122
       expect(response).to be_missing
123 123
     end
124 124
 
@@ -127,7 +127,7 @@ describe WebRequestsController do
127 127
       @agent2.save!
128 128
 
129 129
       expect {
130
-        post :update_location, user_id: users(:bob).to_param, secret: "my_secret2", longitude: 123, latitude: 45, something: "else"
130
+        post :update_location, params: {user_id: users(:bob).to_param, secret: "my_secret2", longitude: 123, latitude: 45, something: "else"}
131 131
         expect(@agent2.events.last.payload).to eq({ 'longitude' => "123", 'latitude' => "45", 'something' => "else" })
132 132
       }.not_to change { @agent.events.count }
133 133
     end

+ 2 - 2
spec/data_fixtures/onethingwell.atom

@@ -44,6 +44,7 @@
44 44
             <category>calendar</category>
45 45
             <category>menubar</category>
46 46
             <category>osx</category>
47
+            <enclosure url="http://c.1tw.org/images/2015/itsy.png" length="48249" type="image/png" />
47 48
         </item>
48 49
         <item>
49 50
             <title>Magic Wormhole</title>
@@ -208,8 +209,7 @@
208 209
         </item>
209 210
         <item>
210 211
             <title>Showgoers</title>
211
-            <description>&lt;a href="http://showgoers.tv/"&gt;Showgoers&lt;/a&gt;: &lt;blockquote&gt; &lt;p&gt;Showgoers is a Chrome browser extension to synchronize your Netflix player with someone else so that you can co-watch the same movie on different computers with no hassle. Syncing up your player is as easy as sharing a URL.&lt;/p&gt; &lt;/blockquote&gt;
212
-            </description>
212
+            <description>&lt;a href="http://showgoers.tv/" onmouseover="javascript:void(0)"&gt;Showgoers&lt;/a&gt;: &lt;blockquote&gt; &lt;p&gt;Showgoers is a Chrome browser extension to synchronize your Netflix player with someone else so that you can co-watch the same movie on different computers with no hassle. Syncing up your player is as easy as sharing a URL.&lt;/p&gt; &lt;/blockquote&gt;&lt;script&gt;some code&lt;/script&gt;</description>
213 213
             <link>http://onethingwell.org/post/125509667816</link>
214 214
             <guid>http://onethingwell.org/post/125509667816</guid>
215 215
             <pubDate>Fri, 31 Jul 2015 13:00:13 +0100</pubDate>

+ 2 - 1
spec/data_fixtures/urlTest.html

@@ -12,6 +12,7 @@
12 12
             <li><a href="https://www.google.ca/search?q=위키백과:대문">unicode param</a></li>
13 13
             <li><a href="http://ko.wikipedia.org/wiki/%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8">percent encoded url</a></li>
14 14
             <li><a href="https://www.google.ca/search?q=%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8">percent encoded param</a></li>
15
+            <li><a href="http://[::1]/path[]?query[]=foo">brackets</a></li>
15 16
         </ul>
16 17
     </body>
17
-</html>
18
+</html>

+ 76 - 0
spec/features/dry_running_spec.rb

@@ -0,0 +1,76 @@
1
+require 'rails_helper'
2
+
3
+describe "Dry running an Agent", js: true do
4
+  let(:agent)   { agents(:bob_website_agent) }
5
+  let(:formatting_agent) { agents(:bob_formatting_agent) }
6
+  let(:user)    { users(:bob) }
7
+  let(:emitter) { agents(:bob_weather_agent) }
8
+
9
+  before(:each) do
10
+    login_as(user)
11
+  end
12
+
13
+  def open_dry_run_modal(agent)
14
+    visit edit_agent_path(agent)
15
+    click_on("Dry Run")
16
+    expect(page).to have_text('Event to send')
17
+  end
18
+
19
+  context 'successful dry runs' do
20
+    before do
21
+      stub_request(:get, "http://xkcd.com/").
22
+        with(:headers => {'Accept-Encoding'=>'gzip,deflate', 'User-Agent'=>'Huginn - https://github.com/cantino/huginn'}).
23
+        to_return(:status => 200, :body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :headers => {})
24
+    end
25
+
26
+    it 'shows the dry run pop up without previous events and selects the events tab when a event was created' do
27
+      open_dry_run_modal(agent)
28
+      click_on("Dry Run")
29
+      expect(page).to have_text('Biologists play reverse')
30
+      expect(page).to have_selector(:css, 'li[role="presentation"].active a[href="#tabEvents"]')
31
+    end
32
+
33
+    it 'shows the dry run pop up with previous events and allows use previously received event' do
34
+      emitter.events << Event.new(payload: {url: "http://xkcd.com/"})
35
+      agent.sources << emitter
36
+      agent.options.merge!('url' => '', 'url_from_event' => '{{url}}')
37
+      agent.save!
38
+
39
+      open_dry_run_modal(agent)
40
+      find('.dry-run-event-sample').click
41
+      within(:css, '.modal .builder') do
42
+        expect(page).to have_text('http://xkcd.com/')
43
+      end
44
+      click_on("Dry Run")
45
+      expect(page).to have_text('Biologists play reverse')
46
+      expect(page).to have_selector(:css, 'li[role="presentation"].active a[href="#tabEvents"]')
47
+    end
48
+
49
+    it 'sends escape characters correctly to the backend' do
50
+      emitter.events << Event.new(payload: {data: "Line 1\nLine 2\nLine 3"})
51
+      formatting_agent.sources << emitter
52
+      formatting_agent.options.merge!('instructions' => {'data' => "{{data | newline_to_br | strip_newlines | split: '<br />' | join: ','}}"})
53
+      formatting_agent.save!
54
+
55
+      open_dry_run_modal(formatting_agent)
56
+      find('.dry-run-event-sample').click
57
+      within(:css, '.modal .builder') do
58
+        expect(page).to have_text('Line 1\nLine 2\nLine 3')
59
+      end
60
+      click_on("Dry Run")
61
+      expect(page).to have_text('Line 1,Line 2,Line 3')
62
+      expect(page).to have_selector(:css, 'li[role="presentation"].active a[href="#tabEvents"]')
63
+    end
64
+  end
65
+
66
+  it 'shows the dry run pop up without previous events and selects the log tab when no event was created' do
67
+    stub_request(:get, "http://xkcd.com/").
68
+      with(:headers => {'Accept-Encoding'=>'gzip,deflate', 'User-Agent'=>'Huginn - https://github.com/cantino/huginn'}).
69
+      to_return(:status => 200, :body => "", :headers => {})
70
+
71
+    open_dry_run_modal(agent)
72
+    click_on("Dry Run")
73
+    expect(page).to have_text('Dry Run started')
74
+    expect(page).to have_selector(:css, 'li[role="presentation"].active a[href="#tabLog"]')
75
+  end
76
+end

+ 10 - 0
spec/features/form_configurable_feature_spec.rb

@@ -0,0 +1,10 @@
1
+require 'capybara_helper'
2
+
3
+describe "form configuring agents", js: true do
4
+  it 'completes fields with predefined array values' do
5
+    login_as(users(:bob))
6
+    visit edit_agent_path(agents(:bob_csv_agent))
7
+    check('Propagate immediately')
8
+    select2("serialize", from: "Mode")
9
+  end
10
+end

+ 46 - 0
spec/features/scenario_import_spec.rb

@@ -0,0 +1,46 @@
1
+require 'rails_helper'
2
+
3
+describe ScenarioImportsController do
4
+  let(:user) { users(:bob) }
5
+
6
+  before do
7
+    login_as(user)
8
+  end
9
+
10
+  it 'renders the import form' do
11
+    visit new_scenario_imports_path
12
+    expect(page).to have_text('Import a Public Scenario')
13
+  end
14
+
15
+  it 'requires a URL or file uplaod' do
16
+    visit new_scenario_imports_path
17
+    click_on 'Start Import'
18
+    expect(page).to have_text('Please provide either a Scenario JSON File or a Public Scenario URL.')
19
+  end
20
+
21
+  it 'imports a scenario that does not exist yet' do
22
+    visit new_scenario_imports_path
23
+    attach_file('Option 2: Upload a Scenario JSON File', File.join(Rails.root, 'data/default_scenario.json'))
24
+    click_on 'Start Import'
25
+    expect(page).to have_text('This scenario has a few agents to get you started. Feel free to change them or delete them as you see fit!')
26
+    expect(page).not_to have_text('This Scenario already exists in your system.')
27
+    check('I confirm that I want to import these Agents.')
28
+    click_on 'Finish Import'
29
+    expect(page).to have_text('Import successful!')
30
+  end
31
+
32
+  it 'asks to accept conflicts when the scenario was modified' do
33
+    DefaultScenarioImporter.seed(user)
34
+    agent = user.agents.where(name: 'Rain Notifier').first
35
+    agent.options['expected_receive_period_in_days'] = 9001
36
+    agent.save!
37
+    visit new_scenario_imports_path
38
+    attach_file('Option 2: Upload a Scenario JSON File', File.join(Rails.root, 'data/default_scenario.json'))
39
+    click_on 'Start Import'
40
+    expect(page).to have_text('This Scenario already exists in your system.')
41
+    expect(page).to have_text('9001')
42
+    check('I confirm that I want to import these Agents.')
43
+    click_on 'Finish Import'
44
+    expect(page).to have_text('Import successful!')
45
+  end
46
+end

+ 21 - 0
spec/features/undefined_agents_spec.rb

@@ -0,0 +1,21 @@
1
+require 'capybara_helper'
2
+
3
+describe "handling undefined agents" do
4
+  before do
5
+    login_as(users(:bob))
6
+    agent = agents(:bob_website_agent)
7
+    agent.update_attribute(:type, 'Agents::UndefinedAgent')
8
+  end
9
+
10
+  it 'renders the error page' do
11
+    visit agents_path
12
+    expect(page).to have_text("Error: Agent(s) are 'missing in action'")
13
+    expect(page).to have_text('Undefined Agent')
14
+  end
15
+
16
+  it 'deletes all undefined agents' do
17
+    visit agents_path
18
+    click_on('Delete Missing Agents')
19
+    expect(page).to have_text('Your Agents')
20
+  end
21
+end

+ 22 - 0
spec/fixtures/agents.yml

@@ -60,6 +60,14 @@ bob_weather_agent:
60 60
   keep_events_for: <%= 45.days %>
61 61
   options: <%= { :location => 94102, :lat => 37.779329, :lng => -122.41915, :api_key => 'test' }.to_json.inspect %>
62 62
 
63
+bob_formatting_agent:
64
+  type: Agents::EventFormattingAgent
65
+  user: bob
66
+  name: "Formatting Agent"
67
+  guid: <%= SecureRandom.hex %>
68
+  keep_events_for: <%= 45.days %>
69
+  options: <%= { instructions: {}, mode: 'clean' }.to_json.inspect %>
70
+
63 71
 jane_weather_agent:
64 72
   type: Agents::WeatherAgent
65 73
   user: jane
@@ -136,14 +144,28 @@ bob_manual_event_agent:
136 144
 bob_basecamp_agent:
137 145
   type: Agents::BasecampAgent
138 146
   user: bob
147
+  name: "bob basecamp agent"
139 148
   service: generic
140 149
   guid: <%= SecureRandom.hex %>
150
+  options: <%= {
151
+      :project_id => "12345",
152
+    }.to_json.inspect %>
153
+
154
+bob_csv_agent:
155
+  type: Agents::CsvAgent
156
+  user: bob
157
+  name: "Bob's CsvAgent"
158
+  guid: <%= SecureRandom.hex %>
141 159
 
142 160
 jane_basecamp_agent:
143 161
   type: Agents::BasecampAgent
144 162
   user: jane
163
+  name: "jane basecamp agent"
145 164
   service: generic
146 165
   guid: <%= SecureRandom.hex %>
166
+  options: <%= {
167
+      :project_id => "12345",
168
+    }.to_json.inspect %>
147 169
 
148 170
 
149 171
 bob_data_output_agent:

+ 5 - 5
spec/models/agents/boxcar_agent_spec.rb

@@ -44,17 +44,17 @@ describe Agents::BoxcarAgent do
44 44
 
45 45
     it "should raise error when invalid response arrives" do
46 46
       stub(HTTParty).post { {"blah" => "blah"} }
47
-      expect{@checker.send_notification}.to raise_error
47
+      expect { @checker.send_notification({}) }.to raise_error(StandardError, /Invalid response from Boxcar:/)
48 48
     end
49 49
 
50 50
     it "should raise error when response says unauthorized" do
51
-      stub(HTTParty).post '{"Response":"Not authorized"}'
52
-      expect{@checker.send_notification}.to raise_error
51
+      stub(HTTParty).post { {"Response" => "Not authorized"} }
52
+      expect { @checker.send_notification({}) }.to raise_error(StandardError, /Not authorized/)
53 53
     end
54 54
 
55 55
     it "should raise error when response has an error" do
56
-      stub(HTTParty).post '{"error": {"message": "Sample error"}}'
57
-      expect{@checker.send_notification}.to raise_error
56
+      stub(HTTParty).post { {"error" => {"message" => "Sample error"}} }
57
+      expect { @checker.send_notification({}) }.to raise_error(StandardError, /Sample error/)
58 58
     end
59 59
   end
60 60
 end

+ 52 - 6
spec/models/agents/data_output_agent_spec.rb

@@ -142,7 +142,7 @@ describe Agents::DataOutputAgent do
142 142
           "url" => "http://imgs.xkcd.com/comics/evolving0.png",
143 143
           "title" => "Evolving yet again with a past date",
144 144
           "date" => '2014/05/05',
145
-          "hovertext" => "Something else"
145
+          "hovertext" => "A small text"
146 146
         }
147 147
       end
148 148
 
@@ -166,7 +166,7 @@ describe Agents::DataOutputAgent do
166 166
 
167 167
            <item>
168 168
             <title>Evolving yet again with a past date</title>
169
-            <description>Secret hovertext: Something else</description>
169
+            <description>Secret hovertext: A small text</description>
170 170
             <link>http://imgs.xkcd.com/comics/evolving0.png</link>
171 171
             <pubDate>#{Time.zone.parse(event3.payload['date']).rfc2822}</pubDate>
172 172
             <guid isPermaLink="false">#{event3.id}</guid>
@@ -216,7 +216,7 @@ describe Agents::DataOutputAgent do
216 216
           'items' => [
217 217
             {
218 218
               'title' => 'Evolving yet again with a past date',
219
-              'description' => 'Secret hovertext: Something else',
219
+              'description' => 'Secret hovertext: A small text',
220 220
               'link' => 'http://imgs.xkcd.com/comics/evolving0.png',
221 221
               'guid' => {"contents" => event3.id, "isPermaLink" => "false"},
222 222
               'pubDate' => Time.zone.parse(event3.payload['date']).rfc2822,
@@ -242,16 +242,62 @@ describe Agents::DataOutputAgent do
242 242
         })
243 243
       end
244 244
 
245
+      context 'with more events' do
246
+        let!(:event4) do
247
+          agents(:bob_website_agent).create_event payload: {
248
+            'site_title' => 'XKCD',
249
+            'url' => 'http://imgs.xkcd.com/comics/comic1.png',
250
+            'title' => 'Comic 1',
251
+            'date' => '',
252
+            'hovertext' => 'Hovertext for Comic 1'
253
+          }
254
+        end
255
+
256
+        let!(:event5) do
257
+          agents(:bob_website_agent).create_event payload: {
258
+            'site_title' => 'XKCD',
259
+            'url' => 'http://imgs.xkcd.com/comics/comic2.png',
260
+            'title' => 'Comic 2',
261
+            'date' => '',
262
+            'hovertext' => 'Hovertext for Comic 2'
263
+          }
264
+        end
265
+
266
+        let!(:event6) do
267
+          agents(:bob_website_agent).create_event payload: {
268
+            'site_title' => 'XKCD',
269
+            'url' => 'http://imgs.xkcd.com/comics/comic3.png',
270
+            'title' => 'Comic 3',
271
+            'date' => '',
272
+            'hovertext' => 'Hovertext for Comic 3'
273
+          }
274
+        end
275
+
276
+        describe 'limiting' do
277
+          it 'can select the last `events_to_show` events' do
278
+            agent.options['events_to_show'] = 2
279
+            content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
280
+            expect(content['items'].map {|i| i["title"] }).to eq(["Comic 3", "Comic 2"])
281
+          end
282
+        end
283
+      end
284
+
245 285
       describe 'ordering' do
246 286
         before do
247
-          agent.options['events_order'] = ['{{title}}']
287
+          agent.options['events_order'] = ['{{hovertext}}']
288
+          agent.options['events_list_order'] = ['{{title}}']
248 289
         end
249 290
 
250
-        it 'can reorder the events_to_show last events based on a Liquid expression' do
291
+        it 'can reorder the last `events_to_show` events based on a Liquid expression' do
292
+          agent.options['events_to_show'] = 2
293
+          asc_content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
294
+          expect(asc_content['items'].map {|i| i["title"] }).to eq(["Evolving", "Evolving again"])
295
+
296
+          agent.options['events_to_show'] = 40
251 297
           asc_content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
252 298
           expect(asc_content['items'].map {|i| i["title"] }).to eq(["Evolving", "Evolving again", "Evolving yet again with a past date"])
253 299
 
254
-          agent.options['events_order'] = [['{{title}}', 'string', true]]
300
+          agent.options['events_list_order'] = [['{{title}}', 'string', true]]
255 301
 
256 302
           desc_content, _status, _content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
257 303
           expect(desc_content['items']).to eq(asc_content['items'].reverse)

+ 28 - 25
spec/models/agents/email_digest_agent_spec.rb

@@ -8,11 +8,11 @@ describe Agents::EmailDigestAgent do
8 8
   end
9 9
 
10 10
   before do
11
-    @checker = Agents::EmailDigestAgent.new(:name => "something", :options => { :expected_receive_period_in_days => "2", :subject => "something interesting" })
11
+    @checker = Agents::EmailDigestAgent.new(:name => "something", :options => {:expected_receive_period_in_days => "2", :subject => "something interesting"})
12 12
     @checker.user = users(:bob)
13 13
     @checker.save!
14 14
 
15
-    @checker1 = Agents::EmailDigestAgent.new(:name => "something", :options => { :expected_receive_period_in_days => "2", :subject => "something interesting", :content_type => "text/plain" })
15
+    @checker1 = Agents::EmailDigestAgent.new(:name => "something", :options => {:expected_receive_period_in_days => "2", :subject => "something interesting", :content_type => "text/plain"})
16 16
     @checker1.user = users(:bob)
17 17
     @checker1.save!
18 18
   end
@@ -25,16 +25,16 @@ describe Agents::EmailDigestAgent do
25 25
     it "queues any payloads it receives" do
26 26
       event1 = Event.new
27 27
       event1.agent = agents(:bob_rain_notifier_agent)
28
-      event1.payload = { :data => "Something you should know about" }
28
+      event1.payload = {:data => "Something you should know about"}
29 29
       event1.save!
30 30
 
31 31
       event2 = Event.new
32 32
       event2.agent = agents(:bob_weather_agent)
33
-      event2.payload = { :data => "Something else you should know about" }
33
+      event2.payload = {:data => "Something else you should know about"}
34 34
       event2.save!
35 35
 
36 36
       Agents::EmailDigestAgent.async_receive(@checker.id, [event1.id, event2.id])
37
-      expect(@checker.reload.memory[:queue]).to eq([{ 'data' => "Something you should know about" }, { 'data' => "Something else you should know about" }])
37
+      expect(@checker.reload.memory['events']).to match([event1.id, event2.id])
38 38
     end
39 39
   end
40 40
 
@@ -44,25 +44,34 @@ describe Agents::EmailDigestAgent do
44 44
       Agents::EmailDigestAgent.async_check(@checker.id)
45 45
       expect(ActionMailer::Base.deliveries).to eq([])
46 46
 
47
-      @checker.memory[:queue] = [{ :data => "Something you should know about" },
48
-                                 { :title => "Foo", :url => "http://google.com", :bar => 2 },
49
-                                 { "message" => "hi", :woah => "there" },
50
-                                 { "test" => 2 }]
51
-      @checker.memory[:events] = [1,2,3,4]
52
-      @checker.save!
53
-
54
-      Agents::EmailDigestAgent.async_check(@checker.id)
47
+      payloads = [
48
+        {:data => "Something you should know about"},
49
+        {:title => "Foo", :url => "http://google.com", :bar => 2},
50
+        {"message" => "hi", :woah => "there"},
51
+        {"test" => 2}
52
+      ]
53
+
54
+      events = payloads.map do |payload|
55
+        Event.new.tap do |event|
56
+          event.agent = agents(:bob_weather_agent)
57
+          event.payload = payload
58
+          event.save!
59
+        end
60
+      end
61
+
62
+      Agents::DigestAgent.async_receive(@checker.id, events.map(&:id))
63
+      @checker.sources << agents(:bob_weather_agent)
64
+      Agents::DigestAgent.async_check(@checker.id)
55 65
 
56 66
       expect(ActionMailer::Base.deliveries.last.to).to eq(["bob@example.com"])
57 67
       expect(ActionMailer::Base.deliveries.last.subject).to eq("something interesting")
58 68
       expect(get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip).to eq("Event\n  data: Something you should know about\n\nFoo\n  bar: 2\n  url: http://google.com\n\nhi\n  woah: there\n\nEvent\n  test: 2")
59
-      expect(@checker.reload.memory[:queue]).to be_empty
69
+      expect(@checker.reload.memory[:events]).to be_empty
60 70
     end
61 71
 
62 72
     it "logs and re-raises mailer errors" do
63 73
       mock(SystemMailer).send_message(anything) { raise Net::SMTPAuthenticationError.new("Wrong password") }
64 74
 
65
-      @checker.memory[:queue] = [{ :data => "Something you should know about" }]
66 75
       @checker.memory[:events] = [1]
67 76
       @checker.save!
68 77
 
@@ -71,8 +80,6 @@ describe Agents::EmailDigestAgent do
71 80
       }.to raise_error(/Wrong password/)
72 81
 
73 82
       expect(@checker.reload.memory[:events]).not_to be_empty
74
-      expect(@checker.reload.memory[:queue]).not_to be_empty
75
-
76 83
       expect(@checker.logs.last.message).to match(/Error sending digest mail .* Wrong password/)
77 84
     end
78 85
 
@@ -84,7 +91,7 @@ describe Agents::EmailDigestAgent do
84 91
       Agent.async_check(agents(:bob_weather_agent).id)
85 92
 
86 93
       Agent.receive!
87
-      expect(@checker.reload.memory[:queue]).not_to be_empty
94
+      expect(@checker.reload.memory[:events]).not_to be_empty
88 95
 
89 96
       Agents::EmailDigestAgent.async_check(@checker.id)
90 97
 
@@ -94,18 +101,14 @@ describe Agents::EmailDigestAgent do
94 101
       expect(plain_email_text).to match(/avehumidity/)
95 102
       expect(html_email_text).to match(/avehumidity/)
96 103
 
97
-      expect(@checker.reload.memory[:queue]).to be_empty
104
+      expect(@checker.reload.memory[:events]).to be_empty
98 105
     end
99
-    
106
+
100 107
     it "should send email with correct content type" do
101 108
       Agents::EmailDigestAgent.async_check(@checker1.id)
102 109
       expect(ActionMailer::Base.deliveries).to eq([])
103 110
 
104
-      @checker1.memory[:queue] = [{ :data => "Something you should know about" },
105
-                                 { :title => "Foo", :url => "http://google.com", :bar => 2 },
106
-                                 { "message" => "hi", :woah => "there" },
107
-                                 { "test" => 2 }]
108
-      @checker1.memory[:events] = [1,2,3,4]
111
+      @checker1.memory[:events] = [1, 2, 3, 4]
109 112
       @checker1.save!
110 113
 
111 114
       Agents::EmailDigestAgent.async_check(@checker1.id)

+ 18 - 0
spec/models/agents/post_agent_spec.rb

@@ -57,6 +57,11 @@ describe Agents::PostAgent do
57 57
   end
58 58
 
59 59
   it_behaves_like WebRequestConcern
60
+  it_behaves_like 'FileHandlingConsumer'
61
+
62
+  it 'renders the description markdown without errors' do
63
+    expect { @checker.description }.not_to raise_error
64
+  end
60 65
 
61 66
   describe "making requests" do
62 67
     it "can make requests of each type" do
@@ -149,6 +154,19 @@ describe Agents::PostAgent do
149 154
       headers = @sent_requests[:post].first.headers
150 155
       expect(headers["Foo"]).to eq("a_variable")
151 156
     end
157
+
158
+    it 'makes a multipart request when receiving a file_pointer' do
159
+      WebMock.reset!
160
+      stub_request(:post, "http://www.example.com/").
161
+        with(:body => "-------------RubyMultipartPost\r\nContent-Disposition: form-data; name=\"default\"\r\n\r\nvalue\r\n-------------RubyMultipartPost\r\nContent-Disposition: form-data; name=\"file\"; filename=\"local.path\"\r\nContent-Length: 8\r\nContent-Type: \r\nContent-Transfer-Encoding: binary\r\n\r\ntestdata\r\n-------------RubyMultipartPost--\r\n\r\n",
162
+             :headers => {'Accept-Encoding'=>'gzip,deflate', 'Content-Length'=>'307', 'Content-Type'=>'multipart/form-data; boundary=-----------RubyMultipartPost', 'User-Agent'=>'Huginn - https://github.com/cantino/huginn'}).
163
+        to_return(:status => 200, :body => "", :headers => {})
164
+      event = Event.new(payload: {file_pointer: {agent_id: 111, file: 'test'}})
165
+      io_mock = mock()
166
+      mock(@checker).get_io(event) { StringIO.new("testdata") }
167
+      @checker.options['no_merge'] = true
168
+      @checker.receive([event])
169
+    end
152 170
   end
153 171
 
154 172
   describe "#check" do

+ 34 - 32
spec/models/agents/pushover_agent_spec.rb

@@ -2,27 +2,29 @@ require 'rails_helper'
2 2
 
3 3
 describe Agents::PushoverAgent do
4 4
   before do
5
-    @checker = Agents::PushoverAgent.new(:name => 'Some Name',
6
-                                       :options => { :token => 'x',
7
-                                                :user => 'x',
8
-                                                :message => 'Some Message',
9
-                                                :device => 'Some Device',
10
-                                                :title => 'Some Message Title',
11
-                                                :url => 'http://someurl.com',
12
-                                                :url_title => 'Some Url Title',
13
-                                                :priority => 0,
14
-                                                :timestamp => 'false',
15
-                                                :sound => 'pushover',
16
-                                                :retry => 0,
17
-                                                :expire => 0,
18
-                                                :expected_receive_period_in_days => '1'})
19
-     
5
+    @checker = Agents::PushoverAgent.new(name: 'Some Name',
6
+                                         options: {
7
+                                           token: 'x',
8
+                                           user: 'x',
9
+                                           message: "{{ message | default: text | default: 'Some Message' }}",
10
+                                           device: "{{ device | default: 'Some Device' }}",
11
+                                           title: "{{ title | default: 'Some Message Title' }}",
12
+                                           url: "{{ url | default: 'http://someurl.com' }}",
13
+                                           url_title: "{{ url_title | default: 'Some Url Title' }}",
14
+                                           priority: "{{ priority | default: 0 }}",
15
+                                           timestamp: "{{ timestamp | default: 'false' }}",
16
+                                           sound: "{{ sound | default: 'pushover' }}",
17
+                                           retry: "{{ retry | default: 0 }}",
18
+                                           expire: "{{ expire | default: 0 }}",
19
+                                           expected_receive_period_in_days: '1'
20
+                                         })
21
+
20 22
     @checker.user = users(:bob)
21 23
     @checker.save!
22 24
 
23 25
     @event = Event.new
24 26
     @event.agent = agents(:bob_weather_agent)
25
-    @event.payload = { :message => 'Looks like its going to rain' }
27
+    @event.payload = { message: 'Looks like its going to rain' }
26 28
     @event.save!
27 29
 
28 30
     @sent_notifications = []
@@ -33,12 +35,12 @@ describe Agents::PushoverAgent do
33 35
     it 'should make sure multiple events are being received' do
34 36
       event1 = Event.new
35 37
       event1.agent = agents(:bob_rain_notifier_agent)
36
-      event1.payload = { :message => 'Some message' }
38
+      event1.payload = { message: 'Some message' }
37 39
       event1.save!
38 40
 
39 41
       event2 = Event.new
40 42
       event2.agent = agents(:bob_weather_agent)
41
-      event2.payload = { :message => 'Some other message' }
43
+      event2.payload = { message: 'Some other message' }
42 44
       event2.save!
43 45
 
44 46
       @checker.receive([@event,event1,event2])
@@ -50,7 +52,7 @@ describe Agents::PushoverAgent do
50 52
     it 'should make sure event message overrides default message' do
51 53
       event = Event.new
52 54
       event.agent = agents(:bob_rain_notifier_agent)
53
-      event.payload = { :message => 'Some new message'}
55
+      event.payload = { message: 'Some new message'}
54 56
       event.save!
55 57
 
56 58
       @checker.receive([event])
@@ -60,7 +62,7 @@ describe Agents::PushoverAgent do
60 62
     it 'should make sure event text overrides default message' do
61 63
       event = Event.new
62 64
       event.agent = agents(:bob_rain_notifier_agent)
63
-      event.payload = { :text => 'Some new text'}
65
+      event.payload = { text: 'Some new text'}
64 66
       event.save!
65 67
 
66 68
       @checker.receive([event])
@@ -70,7 +72,7 @@ describe Agents::PushoverAgent do
70 72
     it 'should make sure event title overrides default title' do
71 73
       event = Event.new
72 74
       event.agent = agents(:bob_rain_notifier_agent)
73
-      event.payload = { :message => 'Some message', :title => 'Some new title' }
75
+      event.payload = { message: 'Some message', title: 'Some new title' }
74 76
       event.save!
75 77
 
76 78
       @checker.receive([event])
@@ -80,7 +82,7 @@ describe Agents::PushoverAgent do
80 82
     it 'should make sure event url overrides default url' do
81 83
       event = Event.new
82 84
       event.agent = agents(:bob_rain_notifier_agent)
83
-      event.payload = { :message => 'Some message', :url => 'Some new url' }
85
+      event.payload = { message: 'Some message', url: 'Some new url' }
84 86
       event.save!
85 87
 
86 88
       @checker.receive([event])
@@ -90,7 +92,7 @@ describe Agents::PushoverAgent do
90 92
     it 'should make sure event url_title overrides default url_title' do
91 93
       event = Event.new
92 94
       event.agent = agents(:bob_rain_notifier_agent)
93
-      event.payload = { :message => 'Some message', :url_title => 'Some new url_title' }
95
+      event.payload = { message: 'Some message', url_title: 'Some new url_title' }
94 96
       event.save!
95 97
 
96 98
       @checker.receive([event])
@@ -100,17 +102,17 @@ describe Agents::PushoverAgent do
100 102
     it 'should make sure event priority overrides default priority' do
101 103
       event = Event.new
102 104
       event.agent = agents(:bob_rain_notifier_agent)
103
-      event.payload = { :message => 'Some message', :priority => 1 }
105
+      event.payload = { message: 'Some message', priority: '1' }
104 106
       event.save!
105 107
 
106 108
       @checker.receive([event])
107
-      expect(@sent_notifications[0]['priority']).to eq(1)
109
+      expect(@sent_notifications[0]['priority']).to eq('1')
108 110
     end
109 111
 
110 112
     it 'should make sure event timestamp overrides default timestamp' do
111 113
       event = Event.new
112 114
       event.agent = agents(:bob_rain_notifier_agent)
113
-      event.payload = { :message => 'Some message', :timestamp => 'false' }
115
+      event.payload = { message: 'Some message', timestamp: 'false' }
114 116
       event.save!
115 117
 
116 118
       @checker.receive([event])
@@ -120,7 +122,7 @@ describe Agents::PushoverAgent do
120 122
     it 'should make sure event sound overrides default sound' do
121 123
       event = Event.new
122 124
       event.agent = agents(:bob_rain_notifier_agent)
123
-      event.payload = { :message => 'Some message', :sound => 'Some new sound' }
125
+      event.payload = { message: 'Some message', sound: 'Some new sound' }
124 126
       event.save!
125 127
 
126 128
       @checker.receive([event])
@@ -130,21 +132,21 @@ describe Agents::PushoverAgent do
130 132
     it 'should make sure event retry overrides default retry' do
131 133
       event = Event.new
132 134
       event.agent = agents(:bob_rain_notifier_agent)
133
-      event.payload = { :message => 'Some message', :retry => 1 }
135
+      event.payload = { message: 'Some message', retry: 1 }
134 136
       event.save!
135 137
 
136 138
       @checker.receive([event])
137
-      expect(@sent_notifications[0]['retry']).to eq(1)
139
+      expect(@sent_notifications[0]['retry']).to eq('1')
138 140
     end
139 141
 
140 142
     it 'should make sure event expire overrides default expire' do
141 143
       event = Event.new
142 144
       event.agent = agents(:bob_rain_notifier_agent)
143
-      event.payload = { :message => 'Some message', :expire => 60 }
145
+      event.payload = { message: 'Some message', expire: '60' }
144 146
       event.save!
145 147
 
146 148
       @checker.receive([event])
147
-      expect(@sent_notifications[0]['expire']).to eq(60)
149
+      expect(@sent_notifications[0]['expire']).to eq('60')
148 150
     end
149 151
   end
150 152
 
@@ -158,7 +160,7 @@ describe Agents::PushoverAgent do
158 160
       expect(@checker.reload).to be_working
159 161
       two_days_from_now = 2.days.from_now
160 162
       stub(Time).now { two_days_from_now }
161
-      
163
+
162 164
       # More time has passed than the expected receive period without any new events
163 165
       expect(@checker.reload).not_to be_working
164 166
     end

+ 92 - 2
spec/models/agents/rss_agent_spec.rb

@@ -55,16 +55,57 @@ describe Agents::RssAgent do
55 55
   end
56 56
 
57 57
   describe "emitting RSS events" do
58
-    it "should emit items as events" do
58
+    it "should emit items as events for an Atom feed" do
59
+      agent.options['include_feed_info'] = true
60
+
59 61
       expect {
60 62
         agent.check
61 63
       }.to change { agent.events.count }.by(20)
62 64
 
63 65
       first, *, last = agent.events.last(20)
66
+      [first, last].each do |event|
67
+        expect(first.payload['feed']).to include({
68
+                                                   "type" => "atom",
69
+                                                   "title" => "Recent Commits to huginn:master",
70
+                                                   "url" => "https://github.com/cantino/huginn/commits/master",
71
+                                                   "links" => [
72
+                                                     {
73
+                                                       "type" => "text/html",
74
+                                                       "rel" => "alternate",
75
+                                                       "href" => "https://github.com/cantino/huginn/commits/master",
76
+                                                     },
77
+                                                     {
78
+                                                       "type" => "application/atom+xml",
79
+                                                       "rel" => "self",
80
+                                                       "href" => "https://github.com/cantino/huginn/commits/master.atom",
81
+                                                     },
82
+                                                   ],
83
+                                                 })
84
+      end
64 85
       expect(first.payload['url']).to eq("https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0")
65 86
       expect(first.payload['urls']).to eq(["https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0"])
87
+      expect(first.payload['links']).to eq([
88
+                                             {
89
+                                               "href" => "https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0",
90
+                                               "rel" => "alternate",
91
+                                               "type" => "text/html",
92
+                                             }
93
+                                          ])
94
+      expect(first.payload['authors']).to eq(["cantino (https://github.com/cantino)"])
95
+      expect(first.payload['date_published']).to be_nil
96
+      expect(first.payload['last_updated']).to eq("2014-07-16T22:26:22-07:00")
66 97
       expect(last.payload['url']).to eq("https://github.com/cantino/huginn/commit/d465158f77dcd9078697e6167b50abbfdfa8b1af")
67 98
       expect(last.payload['urls']).to eq(["https://github.com/cantino/huginn/commit/d465158f77dcd9078697e6167b50abbfdfa8b1af"])
99
+      expect(last.payload['links']).to eq([
100
+                                              {
101
+                                                "href" => "https://github.com/cantino/huginn/commit/d465158f77dcd9078697e6167b50abbfdfa8b1af",
102
+                                                "rel" => "alternate",
103
+                                                "type" => "text/html",
104
+                                              }
105
+                                          ])
106
+      expect(last.payload['authors']).to eq(["CloCkWeRX (https://github.com/CloCkWeRX)"])
107
+      expect(last.payload['date_published']).to be_nil
108
+      expect(last.payload['last_updated']).to eq("2014-07-01T16:37:47+09:30")
68 109
     end
69 110
 
70 111
     it "should emit items as events in the order specified in the events_order option" do
@@ -82,6 +123,33 @@ describe Agents::RssAgent do
82 123
       expect(last.payload['urls']).to eq(["https://github.com/cantino/huginn/commit/0e80f5341587aace2c023b06eb9265b776ac4535"])
83 124
     end
84 125
 
126
+    it "should emit items as events for a FeedBurner RSS 2.0 feed" do
127
+      agent.options['url'] = "http://feeds.feedburner.com/SlickdealsnetFP?format=atom" # This is actually RSS 2.0 w/ Atom extension
128
+      agent.options['include_feed_info'] = true
129
+      agent.save!
130
+
131
+      expect {
132
+        agent.check
133
+      }.to change { agent.events.count }.by(79)
134
+
135
+      first, *, last = agent.events.last(79)
136
+      expect(first.payload['feed']).to include({
137
+                                                 "type" => "rss",
138
+                                                 "title" => "SlickDeals.net",
139
+                                                 "description" => "Slick online shopping deals.",
140
+                                                 "url" => "http://slickdeals.net/",
141
+                                               })
142
+      # Feedjira extracts feedburner:origLink
143
+      expect(first.payload['url']).to eq("http://slickdeals.net/permadeal/130160/green-man-gaming---pc-games-tomb-raider-game-of-the-year-6-hitman-absolution-elite-edition")
144
+      expect(last.payload['feed']).to include({
145
+                                                "type" => "rss",
146
+                                                "title" => "SlickDeals.net",
147
+                                                "description" => "Slick online shopping deals.",
148
+                                                "url" => "http://slickdeals.net/",
149
+                                              })
150
+      expect(last.payload['url']).to eq("http://slickdeals.net/permadeal/129980/amazon---rearth-ringke-fusion-bumper-hybrid-case-for-iphone-6")
151
+    end
152
+
85 153
     it "should track ids and not re-emit the same item when seen again" do
86 154
       agent.check
87 155
       expect(agent.memory['seen_ids']).to eq(agent.events.map {|e| e.payload['id'] })
@@ -155,17 +223,39 @@ describe Agents::RssAgent do
155 223
       @valid_options['url'] = 'http://onethingwell.org/rss'
156 224
     end
157 225
 
226
+    it "captures timestamps normalized in the ISO 8601 format" do
227
+      agent.check
228
+      first, *, third = agent.events.take(3)
229
+      expect(first.payload['date_published']).to eq('2015-08-20T17:00:10+01:00')
230
+      expect(third.payload['date_published']).to eq('2015-08-20T13:00:07+01:00')
231
+    end
232
+
158 233
     it "captures multiple categories" do
159 234
       agent.check
160 235
       first, *, third = agent.events.take(3)
161 236
       expect(first.payload['categories']).to eq(["csv", "crossplatform", "utilities"])
162 237
       expect(third.payload['categories']).to eq(["web"])
163 238
     end
239
+
240
+    it "sanitizes HTML content" do
241
+      agent.options['clean'] = true
242
+      agent.check
243
+      event = agent.events.last
244
+      expect(event.payload['content']).to eq('<a href="http://showgoers.tv/">Showgoers</a>: <blockquote> <p>Showgoers is a Chrome browser extension to synchronize your Netflix player with someone else so that you can co-watch the same movie on different computers with no hassle. Syncing up your player is as easy as sharing a URL.</p> </blockquote>')
245
+      expect(event.payload['description']).to eq('<a href="http://showgoers.tv/">Showgoers</a>: <blockquote> <p>Showgoers is a Chrome browser extension to synchronize your Netflix player with someone else so that you can co-watch the same movie on different computers with no hassle. Syncing up your player is as easy as sharing a URL.</p> </blockquote>')
246
+    end
247
+
248
+    it "captures an enclosure" do
249
+      agent.check
250
+      event = agent.events.fourth
251
+      expect(event.payload['enclosure']).to eq({ "url" => "http://c.1tw.org/images/2015/itsy.png", "type" => "image/png", "length" => "48249" })
252
+      expect(event.payload['image']).to eq("http://c.1tw.org/images/2015/itsy.png")
253
+    end
164 254
   end
165 255
 
166 256
   describe 'logging errors with the feed url' do
167 257
     it 'includes the feed URL when an exception is raised' do
168
-      mock(FeedNormalizer::FeedNormalizer).parse(anything, loose: true) { raise StandardError.new("Some error!") }
258
+      mock(Feedjira::Feed).parse(anything) { raise StandardError.new("Some error!") }
169 259
       expect(lambda {
170 260
         agent.check
171 261
       }).not_to raise_error

+ 43 - 12
spec/models/agents/twitter_action_agent_spec.rb

@@ -24,11 +24,11 @@ describe Agents::TwitterActionAgent do
24 24
 
25 25
     context 'when set up to retweet' do
26 26
       before do
27
-        @agent = build_agent({
28
-          'expected_receive_period_in_days' => '2',
27
+        @agent = build_agent(
29 28
           'favorite' => 'false',
30 29
           'retweet' => 'true',
31
-        })
30
+          'emit_error_events' => 'true'
31
+        )
32 32
         @agent.save!
33 33
       end
34 34
 
@@ -68,9 +68,9 @@ describe Agents::TwitterActionAgent do
68 68
     context 'when set up to favorite' do
69 69
       before do
70 70
         @agent = build_agent(
71
-          'expected_receive_period_in_days' => '2',
72 71
           'favorite' => 'true',
73 72
           'retweet' => 'false',
73
+          'emit_error_events' => 'true'
74 74
         )
75 75
         @agent.save!
76 76
       end
@@ -107,13 +107,48 @@ describe Agents::TwitterActionAgent do
107 107
         end
108 108
       end
109 109
     end
110
+
111
+    context 'with emit_error_events set to false' do
112
+      it 'does re-raises the exception on failure' do
113
+        agent = build_agent
114
+
115
+        stub(agent.twitter).retweet(anything) {
116
+          raise Twitter::Error.new('uh oh')
117
+        }
118
+
119
+       expect { agent.receive([@event1]) }.to raise_error(StandardError, /uh oh/)
120
+
121
+      end
122
+    end
110 123
   end
111 124
 
112 125
   describe "#validate_options" do
126
+    it 'the default options are valid' do
127
+      agent = build_agent(described_class.new.default_options)
128
+
129
+      expect(agent).to be_valid
130
+    end
131
+
132
+    context 'emit_error_events' do
133
+      it 'can be set to true' do
134
+        agent = build_agent(described_class.new.default_options.merge('emit_error_events' => 'true'))
135
+        expect(agent).to be_valid
136
+      end
137
+
138
+      it 'must be a boolean' do
139
+        agent = build_agent(described_class.new.default_options.merge('emit_error_events' => 'notbolean'))
140
+        expect(agent).not_to be_valid
141
+      end
142
+    end
143
+
144
+    it 'expected_receive_period_in_days must be set' do
145
+      agent = build_agent(described_class.new.default_options.merge('expected_receive_period_in_days' => ''))
146
+      expect(agent).not_to be_valid
147
+    end
148
+
113 149
     context 'when set up to neither favorite or retweet' do
114 150
       it 'is invalid' do
115 151
         agent = build_agent(
116
-          'expected_receive_period_in_days' => '2',
117 152
           'favorite' => 'false',
118 153
           'retweet' => 'false',
119 154
         )
@@ -129,11 +164,7 @@ describe Agents::TwitterActionAgent do
129 164
     end
130 165
 
131 166
     it 'checks if events have been received within the expected time period' do
132
-      agent = build_agent(
133
-        'expected_receive_period_in_days' => '2',
134
-        'favorite' => 'false',
135
-        'retweet' => 'true',
136
-      )
167
+      agent = build_agent
137 168
       agent.save!
138 169
 
139 170
       expect(agent).not_to be_working # No events received
@@ -147,10 +178,10 @@ describe Agents::TwitterActionAgent do
147 178
     end
148 179
   end
149 180
 
150
-  def build_agent(options)
181
+  def build_agent(options = {})
151 182
     described_class.new do |agent|
152 183
       agent.name = 'twitter stuff'
153
-      agent.options = options
184
+      agent.options = agent.default_options.merge(options)
154 185
       agent.service = services(:generic)
155 186
       agent.user = users(:bob)
156 187
     end

+ 10 - 0
spec/models/agents/weather_agent_spec.rb

@@ -19,6 +19,16 @@ describe Agents::WeatherAgent do
19 19
   it "creates a valid agent" do
20 20
     expect(agent).to be_valid
21 21
   end
22
+
23
+  it "is valid with put-your-key-here or your-key" do
24
+    agent.options['api_key'] = 'put-your-key-here'
25
+    expect(agent).to be_valid
26
+    expect(agent.working?).to be_falsey
27
+
28
+    agent.options['api_key'] = 'your-key'
29
+    expect(agent).to be_valid
30
+    expect(agent.working?).to be_falsey
31
+  end
22 32
   
23 33
   describe "#service" do
24 34
     it "doesn't have a Service object attached" do

+ 26 - 0
spec/models/agents/webhook_agent_spec.rb

@@ -49,6 +49,12 @@ describe Agents::WebhookAgent do
49 49
       expect(out).to eq(['', 201])
50 50
     end
51 51
 
52
+    it 'should respond with interpolated response message if configured with `response` option' do
53
+      agent.options['response'] = '{{some_key.people[1].name}}'
54
+      out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
55
+      expect(out).to eq(['jon', 201])
56
+    end
57
+
52 58
     it 'should respond with `Event Created` if the response option is nil or missing' do
53 59
       agent.options['response'] = nil
54 60
       out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
@@ -59,6 +65,26 @@ describe Agents::WebhookAgent do
59 65
       expect(out).to eq(['Event Created', 201])
60 66
     end
61 67
 
68
+    it 'should respond with customized response code if configured with `code` option' do
69
+      agent.options['code'] = '200'
70
+      out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
71
+      expect(out).to eq(['Event Created', 200])
72
+    end
73
+
74
+    it 'should respond with `201` if the code option is empty, nil or missing' do
75
+      agent.options['code'] = ''
76
+      out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
77
+      expect(out).to eq(['Event Created', 201])
78
+      
79
+      agent.options['code'] = nil
80
+      out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
81
+      expect(out).to eq(['Event Created', 201])
82
+
83
+      agent.options.delete('code')
84
+      out = agent.receive_web_request({ 'secret' => 'foobar', 'some_key' => payload }, "post", "text/html")
85
+      expect(out).to eq(['Event Created', 201])
86
+    end
87
+
62 88
     describe "receiving events" do
63 89
 
64 90
       context "default settings" do

+ 7 - 2
spec/models/agents/website_agent_spec.rb

@@ -1105,8 +1105,8 @@ fire: hot
1105 1105
 
1106 1106
     describe "#check" do
1107 1107
       before do
1108
-        expect { @checker.check }.to change { Event.count }.by(7)
1109
-        @events = Event.last(7)
1108
+        expect { @checker.check }.to change { Event.count }.by(8)
1109
+        @events = Event.last(8)
1110 1110
       end
1111 1111
 
1112 1112
       it "should check hostname" do
@@ -1143,6 +1143,11 @@ fire: hot
1143 1143
         event = @events[6]
1144 1144
         expect(event.payload['url']).to eq("https://www.google.ca/search?q=%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8")
1145 1145
       end
1146
+
1147
+      it "should check url with unescaped brackets in the path component" do
1148
+        event = @events[7]
1149
+        expect(event.payload['url']).to eq("http://[::1]/path%5B%5D?query[]=foo")
1150
+      end
1146 1151
     end
1147 1152
   end
1148 1153
 end

+ 25 - 0
spec/models/service_spec.rb

@@ -98,6 +98,7 @@ describe Service do
98 98
       expect(service.token).to eq('a1b2c3d4...')
99 99
       expect(service.secret).to eq('abcdef1234')
100 100
     end
101
+
101 102
     it "should work with 37signals services" do
102 103
       signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/37signals.json')))
103 104
       expect {
@@ -113,6 +114,7 @@ describe Service do
113 114
       expect(service.options[:user_id]).to eq(12345)
114 115
       service.expires_at = Time.at(1401554352)
115 116
     end
117
+
116 118
     it "should work with github services" do
117 119
       signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/github.json')))
118 120
       expect {
@@ -126,4 +128,27 @@ describe Service do
126 128
       expect(service.token).to eq('agithubtoken')
127 129
     end
128 130
   end
131
+
132
+  describe 'omniauth options provider registry for non-conforming omniauth responses' do
133
+    describe '.register_options_provider' do
134
+      before do
135
+        Service.register_options_provider('test-omniauth-provider') do |omniauth|
136
+          { name: omniauth['special_field'] }
137
+        end
138
+      end
139
+
140
+      after do
141
+        Service.option_providers.delete('test-omniauth-provider')
142
+      end
143
+
144
+      it 'allows gem developers to add their own options provider to the registry' do
145
+        actual_options = Service.get_options({
146
+          'provider' => 'test-omniauth-provider',
147
+          'special_field' => 'A Great Name'
148
+        })
149
+
150
+        expect(actual_options[:name]).to eq('A Great Name')
151
+      end
152
+    end
153
+  end
129 154
 end

+ 0 - 8
spec/models/user_credential_spec.rb

@@ -8,14 +8,6 @@ describe UserCredential do
8 8
     it { should validate_presence_of(:user_id) }
9 9
   end
10 10
 
11
-  describe "mass assignment" do
12
-    it { should allow_mass_assignment_of :credential_name }
13
-
14
-    it { should allow_mass_assignment_of :credential_value }
15
-
16
-    it { should_not allow_mass_assignment_of :user_id }
17
-  end
18
-
19 11
   describe "cleaning fields" do
20 12
     it "should trim whitespace" do
21 13
       user_credential = user_credentials(:bob_aws_key)

+ 27 - 0
spec/models/users_spec.rb

@@ -1,6 +1,8 @@
1 1
 require 'rails_helper'
2 2
 
3 3
 describe User do
4
+  let(:bob) { users(:bob) }
5
+
4 6
   describe "validations" do
5 7
     describe "invitation_code" do
6 8
       context "when configured to use invitation codes" do
@@ -64,4 +66,29 @@ describe User do
64 66
       expect(users(:bob).deactivated_at).to be_nil
65 67
     end
66 68
   end
69
+
70
+  context '#undefined_agent_types' do
71
+    it 'returns an empty array when no agents are undefined' do
72
+      expect(bob.undefined_agent_types).to be_empty
73
+    end
74
+
75
+    it 'returns the undefined agent types' do
76
+      agent = agents(:bob_website_agent)
77
+      agent.update_attribute(:type, 'Agents::UndefinedAgent')
78
+      expect(bob.undefined_agent_types).to match_array(['Agents::UndefinedAgent'])
79
+    end
80
+  end
81
+
82
+  context '#undefined_agents' do
83
+    it 'returns an empty array when no agents are undefined' do
84
+      expect(bob.undefined_agents).to be_empty
85
+    end
86
+
87
+    it 'returns the undefined agent types' do
88
+      agent = agents(:bob_website_agent)
89
+      agent.update_attribute(:type, 'Agents::UndefinedAgent')
90
+      expect(bob.undefined_agents).not_to be_empty
91
+      expect(bob.undefined_agents.first).to be_a(Agent)
92
+    end
93
+  end
67 94
 end

+ 2 - 2
spec/rails_helper.rb

@@ -13,7 +13,7 @@ require 'rspec/rails'
13 13
 require 'rr'
14 14
 require 'webmock/rspec'
15 15
 
16
-WebMock.disable_net_connect!
16
+WebMock.disable_net_connect!(allow_localhost: true)
17 17
 
18 18
 # Requires supporting ruby files with custom matchers and macros, etc,
19 19
 # in spec/support/ and its subdirectories.
@@ -66,7 +66,7 @@ RSpec.configure do |config|
66 66
 
67 67
   config.render_views
68 68
 
69
-  config.include Devise::TestHelpers, type: :controller
69
+  config.include Devise::Test::ControllerHelpers, type: :controller
70 70
   config.include SpecHelpers
71 71
   config.include Delorean
72 72
 end

+ 23 - 3
spec/support/shared_examples/file_handling_consumer.rb

@@ -1,6 +1,8 @@
1 1
 require 'rails_helper'
2 2
 
3 3
 shared_examples_for 'FileHandlingConsumer' do
4
+  let(:event) { Event.new(user: @checker.user, payload: {'file_pointer' => {'file' => 'text.txt', 'agent_id' => @checker.id}}) }
5
+
4 6
   it 'returns a file pointer' do
5 7
     expect(@checker.get_file_pointer('testfile')).to eq(file_pointer: { file: "testfile", agent_id: @checker.id})
6 8
   end
@@ -9,8 +11,26 @@ shared_examples_for 'FileHandlingConsumer' do
9 11
     @checker2 = @checker.dup
10 12
     @checker2.user = users(:bob)
11 13
     @checker2.save!
12
-    expect(@checker2.user.id).not_to eq(@checker.user.id)
13
-    event = Event.new(user: @checker.user, payload: {'file_pointer' => {'file' => 'test', 'agent_id' => @checker2.id}})
14
+    event.payload['file_pointer']['agent_id'] = @checker2.id
14 15
     expect { @checker.get_io(event) }.to raise_error(ActiveRecord::RecordNotFound)
15 16
   end
16
-end
17
+
18
+  context '#has_file_pointer?' do
19
+    it 'returns true if the event contains a file pointer' do
20
+      expect(@checker.has_file_pointer?(event)).to be_truthy
21
+    end
22
+
23
+    it 'returns false if the event does not contain a file pointer' do
24
+      expect(@checker.has_file_pointer?(Event.new)).to be_falsy
25
+    end
26
+  end
27
+
28
+  it '#get_upload_io returns a Faraday::UploadIO instance' do
29
+    io_mock = mock()
30
+    mock(@checker).get_io(event) { StringIO.new("testdata") }
31
+
32
+    upload_io = @checker.get_upload_io(event)
33
+    expect(upload_io).to be_a(Faraday::UploadIO)
34
+    expect(upload_io.content_type).to eq('text/plain')
35
+  end
36
+end

+ 142 - 36
vendor/assets/javascripts/jquery.serializeObject.js

@@ -1,40 +1,146 @@
1
-//
2
-// Use internal $.serializeArray to get list of form elements which is
3
-// consistent with $.serialize
4
-//
5
-// From version 2.0.0, $.serializeObject will stop converting [name] values
6
-// to camelCase format. This is *consistent* with other serialize methods:
7
-//
8
-//   - $.serialize
9
-//   - $.serializeArray
10
-//
11
-// If you require camel casing, you can either download version 1.0.4 or map
12
-// them yourself.
13
-//
14
-
15
-(function($){
16
-  $.fn.serializeObject = function () {
17
-    "use strict";
18
-
19
-    var result = {};
20
-    var extend = function (i, element) {
21
-      var node = result[element.name];
22
-
23
-  // If node with same name exists already, need to convert it to an array as it
24
-  // is a multi-value field (i.e., checkboxes)
25
-
26
-      if ('undefined' !== typeof node && node !== null) {
27
-        if ($.isArray(node)) {
28
-          node.push(element.value);
29
-        } else {
30
-          result[element.name] = [node, element.value];
1
+/**
2
+ * jQuery serializeObject
3
+ * @copyright 2014, macek <paulmacek@gmail.com>
4
+ * @link https://github.com/macek/jquery-serialize-object
5
+ * @license BSD
6
+ * @version 2.5.0
7
+ */
8
+(function(root, factory) {
9
+
10
+  // AMD
11
+  if (typeof define === "function" && define.amd) {
12
+    define(["exports", "jquery"], function(exports, $) {
13
+      return factory(exports, $);
14
+    });
15
+  }
16
+
17
+  // CommonJS
18
+  else if (typeof exports !== "undefined") {
19
+    var $ = require("jquery");
20
+    factory(exports, $);
21
+  }
22
+
23
+  // Browser
24
+  else {
25
+    factory(root, (root.jQuery || root.Zepto || root.ender || root.$));
26
+  }
27
+
28
+}(this, function(exports, $) {
29
+
30
+  var patterns = {
31
+    validate: /^[a-z_][a-z0-9_]*(?:\[(?:\d*|[a-z0-9_]+)\])*$/i,
32
+    key:      /[a-z0-9_]+|(?=\[\])/gi,
33
+    push:     /^$/,
34
+    fixed:    /^\d+$/,
35
+    named:    /^[a-z0-9_]+$/i
36
+  };
37
+
38
+  function FormSerializer(helper, $form) {
39
+
40
+    // private variables
41
+    var data     = {},
42
+        pushes   = {};
43
+
44
+    // private API
45
+    function build(base, key, value) {
46
+      base[key] = value;
47
+      return base;
48
+    }
49
+
50
+    function makeObject(root, value) {
51
+
52
+      var keys = root.match(patterns.key), k;
53
+
54
+      // nest, nest, ..., nest
55
+      while ((k = keys.pop()) !== undefined) {
56
+        // foo[]
57
+        if (patterns.push.test(k)) {
58
+          var idx = incrementPush(root.replace(/\[\]$/, ''));
59
+          value = build([], idx, value);
60
+        }
61
+
62
+        // foo[n]
63
+        else if (patterns.fixed.test(k)) {
64
+          value = build([], k, value);
65
+        }
66
+
67
+        // foo; foo[bar]
68
+        else if (patterns.named.test(k)) {
69
+          value = build({}, k, value);
31 70
         }
32
-      } else {
33
-        result[element.name] = element.value;
34 71
       }
35
-    };
36 72
 
37
-    $.each(this.serializeArray(), extend);
38
-    return result;
73
+      return value;
74
+    }
75
+
76
+    function incrementPush(key) {
77
+      if (pushes[key] === undefined) {
78
+        pushes[key] = 0;
79
+      }
80
+      return pushes[key]++;
81
+    }
82
+
83
+    function encode(pair) {
84
+      switch ($('[name="' + pair.name + '"]', $form).attr("type")) {
85
+        case "checkbox":
86
+          return pair.value === "on" ? true : pair.value;
87
+        default:
88
+          return pair.value;
89
+      }
90
+    }
91
+
92
+    function addPair(pair) {
93
+      if (!patterns.validate.test(pair.name)) return this;
94
+      var obj = makeObject(pair.name, encode(pair));
95
+      data = helper.extend(true, data, obj);
96
+      return this;
97
+    }
98
+
99
+    function addPairs(pairs) {
100
+      if (!helper.isArray(pairs)) {
101
+        throw new Error("formSerializer.addPairs expects an Array");
102
+      }
103
+      for (var i=0, len=pairs.length; i<len; i++) {
104
+        this.addPair(pairs[i]);
105
+      }
106
+      return this;
107
+    }
108
+
109
+    function serialize() {
110
+      return data;
111
+    }
112
+
113
+    function serializeJSON() {
114
+      return JSON.stringify(serialize());
115
+    }
116
+
117
+    // public API
118
+    this.addPair = addPair;
119
+    this.addPairs = addPairs;
120
+    this.serialize = serialize;
121
+    this.serializeJSON = serializeJSON;
122
+  }
123
+
124
+  FormSerializer.patterns = patterns;
125
+
126
+  FormSerializer.serializeObject = function serializeObject() {
127
+    return new FormSerializer($, this).
128
+      addPairs(this.serializeArray()).
129
+      serialize();
130
+  };
131
+
132
+  FormSerializer.serializeJSON = function serializeJSON() {
133
+    return new FormSerializer($, this).
134
+      addPairs(this.serializeArray()).
135
+      serializeJSON();
39 136
   };
40
-})(jQuery);
137
+
138
+  if (typeof $.fn !== "undefined") {
139
+    $.fn.serializeObject = FormSerializer.serializeObject;
140
+    $.fn.serializeJSON   = FormSerializer.serializeJSON;
141
+  }
142
+
143
+  exports.FormSerializer = FormSerializer;
144
+
145
+  return FormSerializer;
146
+}));