From ff758cc679280ba61bff39ff3ae113d1d05cee9f Mon Sep 17 00:00:00 2001 From: Andrew Cantino Date: Sat, 1 Feb 2014 14:01:27 -0800 Subject: [PATCH] move user credentials to a primary route, update twitter agents and background job --- .../javascripts/application.js.coffee.erb | 1 - app/assets/javascripts/users.js | 13 --- app/concerns/twitter_concern.rb | 41 ++++++--- .../user_credentials_controller.rb | 61 +++++++++++++ app/helpers/application_helper.rb | 14 --- app/models/agent.rb | 18 +++- app/models/agents/twitter_publish_agent.rb | 20 ++--- app/models/agents/twitter_stream_agent.rb | 18 ++-- app/models/agents/twitter_user_agent.rb | 16 ++-- app/models/contact.rb | 1 + app/models/user.rb | 5 +- app/models/user_credential.rb | 15 +++- .../_user_credential_fields.html.erb | 9 -- app/views/devise/registrations/edit.html.erb | 6 -- app/views/layouts/_navigation.html.erb | 1 + app/views/user_credentials/_form.html.erb | 30 +++++++ app/views/user_credentials/edit.html.erb | 17 ++++ app/views/user_credentials/index.html.erb | 44 ++++++++++ app/views/user_credentials/new.html.erb | 17 ++++ bin/twitter_stream.rb | 20 ++--- config/routes.rb | 2 + .../20140121075418_create_user_credentials.rb | 6 +- db/schema.rb | 52 +++++++----- .../user_credentials_controller_spec.rb | 85 +++++++++++++++++++ spec/fixtures/user_credentials.yml | 12 ++- spec/models/agent_spec.rb | 9 ++ spec/models/user_credential_spec.rb | 23 ++++- spec/models/users_spec.rb | 20 ++--- 28 files changed, 423 insertions(+), 153 deletions(-) delete mode 100644 app/assets/javascripts/users.js create mode 100644 app/controllers/user_credentials_controller.rb delete mode 100644 app/views/devise/registrations/_user_credential_fields.html.erb create mode 100644 app/views/user_credentials/_form.html.erb create mode 100644 app/views/user_credentials/edit.html.erb create mode 100644 app/views/user_credentials/index.html.erb create mode 100644 app/views/user_credentials/new.html.erb create mode 100644 spec/controllers/user_credentials_controller_spec.rb diff --git a/app/assets/javascripts/application.js.coffee.erb b/app/assets/javascripts/application.js.coffee.erb index d9270998..15b69c98 100644 --- a/app/assets/javascripts/application.js.coffee.erb +++ b/app/assets/javascripts/application.js.coffee.erb @@ -6,7 +6,6 @@ #= require jquery.json-editor #= require latlon_and_geo #= require ./worker-checker -#= require ./users #= require_self window.setupJsonEditor = ($editor = $(".live-json-editor")) -> diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js deleted file mode 100644 index 96b0c09d..00000000 --- a/app/assets/javascripts/users.js +++ /dev/null @@ -1,13 +0,0 @@ -//alert("i get included"); -function remove_fields(link){ - $(link).prev().val("1"); - $(link).parent(".fields").hide(); -} - -function add_fields(link, association, content) { - var new_id = new Date().getTime(); - var regexp = new RegExp("new_" + association, "g") - $(link).parent().before(content.replace(regexp, new_id)); -} - - diff --git a/app/concerns/twitter_concern.rb b/app/concerns/twitter_concern.rb index aedc351a..6d1f6e5d 100644 --- a/app/concerns/twitter_concern.rb +++ b/app/concerns/twitter_concern.rb @@ -2,28 +2,41 @@ module TwitterConcern extend ActiveSupport::Concern included do - self.validate :validate_twitter_options - self.after_initialize :configure_twitter + validate :validate_twitter_options + after_initialize :configure_twitter end def validate_twitter_options - unless options['consumer_key'].present? && - options['consumer_secret'].present? && - options['oauth_token'].present? && - options['oauth_token_secret'].present? - errors.add(:base, "consumer_key, consumer_secret, oauth_token and oauth_token_secret are required to authenticate with the Twitter API") + unless twitter_consumer_key.present? && + twitter_consumer_secret.present? && + twitter_oauth_token.present? && + twitter_oauth_token_secret.present? + errors.add(:base, "Twitter consumer_key, consumer_secret, oauth_token, and oauth_token_secret are required to authenticate with the Twitter API. You can provide these as options to this Agent, or as Credentials with the same names, but starting with 'twitter_'.") end end + def twitter_consumer_key + options['consumer_key'].presence || credential('twitter_consumer_key') + end + + def twitter_consumer_secret + options['consumer_secret'].presence || credential('twitter_consumer_secret') + end + + def twitter_oauth_token + options['oauth_token'].presence || options['access_key'].presence || credential('twitter_oauth_token') + end + + def twitter_oauth_token_secret + options['oauth_token_secret'].presence || options['access_secret'].presence || credential('twitter_oauth_token_secret') + end + def configure_twitter Twitter.configure do |config| - config.consumer_key = options['consumer_key'] - config.consumer_secret = options['consumer_secret'] - config.oauth_token = options['oauth_token'] || options['access_key'] - config.oauth_token_secret = options['oauth_token_secret'] || options['access_secret'] + config.consumer_key = twitter_consumer_key + config.consumer_secret = twitter_consumer_secret + config.oauth_token = twitter_oauth_token + config.oauth_token_secret = twitter_oauth_token_secret end end - - module ClassMethods - end end \ No newline at end of file diff --git a/app/controllers/user_credentials_controller.rb b/app/controllers/user_credentials_controller.rb new file mode 100644 index 00000000..86b2f74f --- /dev/null +++ b/app/controllers/user_credentials_controller.rb @@ -0,0 +1,61 @@ +class UserCredentialsController < ApplicationController + def index + @user_credentials = current_user.user_credentials.page(params[:page]) + + respond_to do |format| + format.html + format.json { render json: @user_credentials } + end + end + + def new + @user_credential = current_user.user_credentials.build + + respond_to do |format| + format.html + format.json { render json: @user_credential } + end + end + + def edit + @user_credential = current_user.user_credentials.find(params[:id]) + end + + def create + @user_credential = current_user.user_credentials.build(params[:user_credential]) + + respond_to do |format| + if @user_credential.save + format.html { redirect_to user_credentials_path, notice: 'Your credential was successfully created.' } + format.json { render json: @user_credential, status: :created, location: @user_credential } + else + format.html { render action: "new" } + format.json { render json: @user_credential.errors, status: :unprocessable_entity } + end + end + end + + def update + @user_credential = current_user.user_credentials.find(params[:id]) + + respond_to do |format| + if @user_credential.update_attributes(params[:user_credential]) + format.html { redirect_to user_credentials_path, notice: 'Your credential was successfully updated.' } + format.json { head :no_content } + else + format.html { render action: "edit" } + format.json { render json: @user_credential.errors, status: :unprocessable_entity } + end + end + end + + def destroy + @user_credential = current_user.user_credentials.find(params[:id]) + @user_credential.destroy + + respond_to do |format| + format.html { redirect_to user_credentials_path } + format.json { head :no_content } + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8c7356b1..7c7581b2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -14,18 +14,4 @@ module ApplicationHelper link_to 'No'.html_safe, agent_path(agent, :tab => (agent.recent_error_logs? ? 'logs' : 'details')) end end - - def link_to_remove_fields(name, f, options = {}) - f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)", options) - end - - def link_to_add_fields(name, f, options = {}) - association = options[:association] - new_object = f.object.class.reflect_on_association(association).klass.new - fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder| - render(association.to_s.singularize + "_fields", :f => builder) - end - link_to_function(name, "add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}\")") - end - end diff --git a/app/models/agent.rb b/app/models/agent.rb index 354bdf43..f7e6bbd1 100644 --- a/app/models/agent.rb +++ b/app/models/agent.rb @@ -59,10 +59,6 @@ class Agent < ActiveRecord::Base where(:type => type) } - def credential(name) - user.user_credentials.where(:credential_name => name).first.try(:credential_value) || nil - end - def check # Implement me in your subclass of Agent. end @@ -106,6 +102,20 @@ class Agent < ActiveRecord::Base end end + def credential(name) + @credential_cache ||= {} + if @credential_cache.has_key?(name) + @credential_cache[name] + else + @credential_cache[name] = user.user_credentials.where(:credential_name => name).first.try(:credential_value) + end + end + + def reload + @credential_cache = {} + super + end + def new_event_expiration_date keep_events_for > 0 ? keep_events_for.days.from_now : nil end diff --git a/app/models/agents/twitter_publish_agent.rb b/app/models/agents/twitter_publish_agent.rb index 0576c469..fe98bfe1 100644 --- a/app/models/agents/twitter_publish_agent.rb +++ b/app/models/agents/twitter_publish_agent.rb @@ -8,10 +8,11 @@ module Agents description <<-MD The TwitterPublishAgent publishes tweets from the events it receives. - You [must set up a Twitter app](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token) and provide it's `consumer_key`, `consumer_secret`, `oauth_token` and `oauth_token_secret`, - (also knows as "Access token" on the Twitter developer's site), along with the `username` of the Twitter user to publish as. + Twitter credentials must be supplied as either [credentials](/user_credentials) called + `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`, + or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`. - The `oauth_token` and `oauth_token_secret` determine which user the tweet will be sent as. + To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). You must also specify a `message_path` parameter: a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value to tweet. @@ -19,10 +20,7 @@ module Agents MD def validate_options - unless options['username'].present? && - options['expected_update_period_in_days'].present? - errors.add(:base, "username and expected_update_period_in_days are required") - end + errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present? end def working? @@ -31,12 +29,7 @@ module Agents def default_options { - 'username' => "", 'expected_update_period_in_days' => "10", - 'consumer_key' => "---", - 'consumer_secret' => "---", - 'oauth_token' => "---", - 'oauth_token_secret' => "---", 'message_path' => "text" } end @@ -68,9 +61,8 @@ module Agents end end - def publish_tweet text + def publish_tweet(text) Twitter.update(text) end - end end \ No newline at end of file diff --git a/app/models/agents/twitter_stream_agent.rb b/app/models/agents/twitter_stream_agent.rb index eb0bbd26..89f72f4c 100644 --- a/app/models/agents/twitter_stream_agent.rb +++ b/app/models/agents/twitter_stream_agent.rb @@ -6,11 +6,13 @@ module Agents description <<-MD The TwitterStreamAgent follows the Twitter stream in real time, watching for certain keywords, or filters, that you provide. - You must provide an oAuth `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`, as well as an array of `filters`. Multiple words in a filter - must all show up in a tweet, but are independent of order. - + To follow the Twitter stream, provide an array of `filters`. Multiple words in a filter must all show up in a tweet, but are independent of order. If you provide an array instead of a filter, the first entry will be considered primary and any additional values will be treated as aliases. + Twitter credentials must be supplied as either [credentials](/user_credentials) called + `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`, + or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`. + To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). 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. @@ -67,13 +69,9 @@ module Agents def default_options { - 'consumer_key' => "---", - 'consumer_secret' => "---", - 'oauth_token' => "---", - 'oauth_token_secret' => "---", - 'filters' => %w[keyword1 keyword2], - 'expected_update_period_in_days' => "2", - 'generate' => "events" + 'filters' => %w[keyword1 keyword2], + 'expected_update_period_in_days' => "2", + 'generate' => "events" } end diff --git a/app/models/agents/twitter_user_agent.rb b/app/models/agents/twitter_user_agent.rb index 0d7f57d2..718f149a 100644 --- a/app/models/agents/twitter_user_agent.rb +++ b/app/models/agents/twitter_user_agent.rb @@ -9,7 +9,13 @@ module Agents description <<-MD The TwitterUserAgent follows the timeline of a specified Twitter user. - You [must set up a Twitter app](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token) and provide it's `consumer_key`, `consumer_secret`, `oauth_token` and `oauth_token_secret`, (Also shown as "Access token" on the Twitter developer's site.) along with the `username` of the Twitter user to monitor. + Twitter credentials must be supplied as either [credentials](/user_credentials) called + `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`, + or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`. + + To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token). + + You must also provide the `username` of the Twitter user to monitor. 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. MD @@ -53,12 +59,8 @@ module Agents def default_options { - 'username' => "tectonic", - 'expected_update_period_in_days' => "2", - 'consumer_key' => "---", - 'consumer_secret' => "---", - 'oauth_token' => "---", - 'oauth_token_secret' => "---" + 'username' => "tectonic", + 'expected_update_period_in_days' => "2" } end diff --git a/app/models/contact.rb b/app/models/contact.rb index 6adeafb6..5928ec70 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -1,5 +1,6 @@ # Contacts are used only for the contact form on the Huginn website. If you host a public Huginn instance, you can use # these to receive messages from visitors. + class Contact < ActiveRecord::Base attr_accessible :email, :message, :name diff --git a/app/models/user.rb b/app/models/user.rb index 1e2c0a8a..b6970c65 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,10 +22,7 @@ class User < ActiveRecord::Base 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." validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid" - has_many :user_credentials, :dependent => :destroy - accepts_nested_attributes_for :user_credentials, - :allow_destroy => true - attr_accessible :user_credentials_attributes + has_many :user_credentials, :dependent => :destroy, :inverse_of => :user has_many :events, :order => "events.created_at desc", :dependent => :delete_all, :inverse_of => :user has_many :agents, :order => "agents.created_at desc", :dependent => :destroy, :inverse_of => :user has_many :logs, :through => :agents, :class_name => "AgentLog" diff --git a/app/models/user_credential.rb b/app/models/user_credential.rb index 7fa60209..7cf78d9f 100644 --- a/app/models/user_credential.rb +++ b/app/models/user_credential.rb @@ -1,6 +1,19 @@ class UserCredential < ActiveRecord::Base - attr_accessible :credential_name, :credential_value, :user_id + attr_accessible :credential_name, :credential_value + belongs_to :user + validates_presence_of :credential_name + validates_presence_of :credential_value + validates_presence_of :user_id validates_uniqueness_of :credential_name, :scope => :user_id + + before_save :trim_fields + + protected + + def trim_fields + credential_name.strip! + credential_value.strip! + end end diff --git a/app/views/devise/registrations/_user_credential_fields.html.erb b/app/views/devise/registrations/_user_credential_fields.html.erb deleted file mode 100644 index 0cd514b3..00000000 --- a/app/views/devise/registrations/_user_credential_fields.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -

- <%= f.label :credential_name, "Name" %> - <%= f.text_field :credential_name %> - <%= f.label :credential_value, "Value" %> - <%= f.text_field :credential_value %> - <%= f.hidden_field :_destroy %> - <%= link_to_remove_fields("remove", f) %> -

- diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 271a98b5..241e1cb3 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -48,12 +48,6 @@
<%= f.submit "Update", :class => "btn btn-primary" %>
-
- <%= f.fields_for(:user_credentials) do |uc| %> - <%= render 'user_credential_fields', :f => uc %> - <% end %> -
-

<%= link_to_add_fields "Add A Credential", f, :association => :user_credentials %>

<% end %>

Cancel my account

diff --git a/app/views/layouts/_navigation.html.erb b/app/views/layouts/_navigation.html.erb index e8a95513..8c9fe5ba 100644 --- a/app/views/layouts/_navigation.html.erb +++ b/app/views/layouts/_navigation.html.erb @@ -4,6 +4,7 @@ <% end %> diff --git a/app/views/user_credentials/_form.html.erb b/app/views/user_credentials/_form.html.erb new file mode 100644 index 00000000..2074de01 --- /dev/null +++ b/app/views/user_credentials/_form.html.erb @@ -0,0 +1,30 @@ +<%= form_for(@user_credential, :method => @user_credential.new_record? ? "POST" : "PUT") do |f| %> + <% if @user_credential.errors.any? %> +
+

<%= pluralize(@user_credential.errors.count, "error") %> prohibited this Credential from being saved:

+ +
+ <% end %> + +
+ <%= f.label :credential_name, :class => 'control-label' %> +
+ <%= f.text_field :credential_name, :class => 'span4' %> +
+
+ +
+ <%= f.label :credential_value, :class => 'control-label' %> +
+ <%= f.text_area :credential_value, :class => 'span8', :rows => 10 %> +
+
+ +
+ <%= f.submit "Save Credential", :class => "btn btn-primary" %> +
+<% end %> diff --git a/app/views/user_credentials/edit.html.erb b/app/views/user_credentials/edit.html.erb new file mode 100644 index 00000000..5d1a5c1e --- /dev/null +++ b/app/views/user_credentials/edit.html.erb @@ -0,0 +1,17 @@ +
+
+
+ + + <%= render 'form' %> + +
+ <%= link_to ' Back'.html_safe, user_credentials_path, class: "btn" %> +
+
+
+
\ No newline at end of file diff --git a/app/views/user_credentials/index.html.erb b/app/views/user_credentials/index.html.erb new file mode 100644 index 00000000..dbafee06 --- /dev/null +++ b/app/views/user_credentials/index.html.erb @@ -0,0 +1,44 @@ +
+
+
+ + +
+ Credentials are used to store values used by many Agents. Examples might include "twitter_consumer_secret", + "user_full_name", or "user_birthday". +
+ + + + + + + + <% @user_credentials.each do |user_credential| %> + + + + + <% end %> +
NameValue
<%= user_credential.credential_name %> + <%= truncate user_credential.credential_value %> +
+ <%= link_to 'Edit', edit_user_credential_path(user_credential), class: "btn btn-mini" %> + <%= link_to 'Delete', user_credential_path(user_credential), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-mini" %> +
+
+ + <%= paginate @user_credentials, :theme => 'twitter-bootstrap' %> + +
+ +
+ <%= link_to ' New Credential'.html_safe, new_user_credential_path, class: "btn" %> +
+
+
+
\ No newline at end of file diff --git a/app/views/user_credentials/new.html.erb b/app/views/user_credentials/new.html.erb new file mode 100644 index 00000000..aa0f697e --- /dev/null +++ b/app/views/user_credentials/new.html.erb @@ -0,0 +1,17 @@ +
+
+
+ + + <%= render 'form' %> + +
+ <%= link_to ' Back'.html_safe, user_credentials_path, class: "btn" %> +
+
+
+
\ No newline at end of file diff --git a/bin/twitter_stream.rb b/bin/twitter_stream.rb index 474639c0..c83b8687 100755 --- a/bin/twitter_stream.rb +++ b/bin/twitter_stream.rb @@ -18,16 +18,16 @@ require 'twitter/json_stream' require 'em-http-request' require 'pp' -def stream!(filters, options = {}, &block) +def stream!(filters, agent, &block) stream = Twitter::JSONStream.connect( :path => "/1/statuses/#{(filters && filters.length > 0) ? 'filter' : 'sample'}.json#{"?track=#{filters.map {|f| CGI::escape(f) }.join(",")}" if filters && filters.length > 0}", + :ssl => true, :oauth => { - :consumer_key => options[:consumer_key], - :consumer_secret => options[:consumer_secret], - :access_key => options[:oauth_token] || options[:access_key], - :access_secret => options[:oauth_token_secret] || options[:access_secret] - }, - :ssl => true + :consumer_key => agent.twitter_consumer_key, + :consumer_secret => agent.twitter_consumer_secret, + :access_key => agent.twitter_oauth_token, + :access_secret => agent.twitter_oauth_token_secret + } ) stream.each_item do |status| @@ -55,7 +55,7 @@ def stream!(filters, options = {}, &block) end def load_and_run(agents) - agents.group_by { |agent| agent.options[:twitter_username] }.each do |twitter_username, agents| + agents.group_by { |agent| agent.twitter_oauth_token }.each do |oauth_token, agents| filter_to_agent_map = agents.map { |agent| agent.options[:filters] }.flatten.uniq.compact.map(&:strip).inject({}) { |m, f| m[f] = []; m } agents.each do |agent| @@ -64,11 +64,9 @@ def load_and_run(agents) end end - options = agents.first.options.slice(:consumer_key, :consumer_secret, :access_key, :oauth_token, :access_secret, :oauth_token_secret) - recent_tweets = [] - stream!(filter_to_agent_map.keys, options) do |status| + stream!(filter_to_agent_map.keys, agents.first) do |status| if status["retweeted_status"].present? && status["retweeted_status"].is_a?(Hash) puts "Skipping retweet: #{status["text"]}" elsif recent_tweets.include?(status["id_str"]) diff --git a/config/routes.rb b/config/routes.rb index 18101711..072a03b6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,8 @@ Huginn::Application.routes.draw do end end + resources :user_credentials, :except => :show + match "/worker_status" => "worker_status#show" post "/users/:user_id/update_location/:secret" => "user_location_updates#create" diff --git a/db/migrate/20140121075418_create_user_credentials.rb b/db/migrate/20140121075418_create_user_credentials.rb index 86c87a1e..2a493eff 100644 --- a/db/migrate/20140121075418_create_user_credentials.rb +++ b/db/migrate/20140121075418_create_user_credentials.rb @@ -1,9 +1,9 @@ class CreateUserCredentials < ActiveRecord::Migration def change create_table :user_credentials do |t| - t.integer :user_id - t.string :credential_name - t.string :credential_value + t.integer :user_id, :null => false + t.string :credential_name, :null => false + t.text :credential_value, :null => false t.timestamps end diff --git a/db/schema.rb b/db/schema.rb index d751e8a0..d265a3a6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,21 +11,21 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140121075418) do +ActiveRecord::Schema.define(:version => 20140127164931) do create_table "agent_logs", :force => true do |t| - t.integer "agent_id", :null => false - t.text "message", :null => false - t.integer "level", :default => 3, :null => false + t.integer "agent_id", :null => false + t.text "message", :limit => 16777215, :null => false + t.integer "level", :default => 3, :null => false t.integer "inbound_event_id" t.integer "outbound_event_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end create_table "agents", :force => true do |t| t.integer "user_id" - t.text "options" + t.text "options", :limit => 16777215 t.string "type" t.string "name" t.string "schedule" @@ -37,27 +37,35 @@ ActiveRecord::Schema.define(:version => 20140121075418) do t.datetime "updated_at", :null => false t.text "memory", :limit => 2147483647 t.datetime "last_webhook_at" - t.integer "keep_events_for", :default => 0, :null => false t.datetime "last_event_at" t.datetime "last_error_log_at" + t.integer "keep_events_for", :default => 0, :null => false end add_index "agents", ["schedule"], :name => "index_agents_on_schedule" add_index "agents", ["type"], :name => "index_agents_on_type" add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at" + create_table "contacts", :force => true do |t| + t.text "message" + t.string "name" + t.string "email" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "delayed_jobs", :force => true do |t| - t.integer "priority", :default => 0 - t.integer "attempts", :default => 0 - t.text "handler" - t.text "last_error" + t.integer "priority", :default => 0 + t.integer "attempts", :default => 0 + t.text "handler", :limit => 16777215 + t.text "last_error", :limit => 16777215 t.datetime "run_at" t.datetime "locked_at" t.datetime "failed_at" t.string "locked_by" t.string "queue" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false end add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" @@ -65,11 +73,11 @@ ActiveRecord::Schema.define(:version => 20140121075418) do create_table "events", :force => true do |t| t.integer "user_id" t.integer "agent_id" - t.decimal "lat", :precision => 15, :scale => 10 - t.decimal "lng", :precision => 15, :scale => 10 - t.text "payload", :limit => 16777215 - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.decimal "lat", :precision => 15, :scale => 10 + t.decimal "lng", :precision => 15, :scale => 10 + t.text "payload", :limit => 2147483647 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.datetime "expires_at" end @@ -88,9 +96,9 @@ ActiveRecord::Schema.define(:version => 20140121075418) do add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id" create_table "user_credentials", :force => true do |t| - t.integer "user_id" - t.string "credential_name" - t.string "credential_value" + t.integer "user_id", :null => false + t.string "credential_name", :null => false + t.text "credential_value", :null => false t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end diff --git a/spec/controllers/user_credentials_controller_spec.rb b/spec/controllers/user_credentials_controller_spec.rb new file mode 100644 index 00000000..b5b46ce3 --- /dev/null +++ b/spec/controllers/user_credentials_controller_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe UserCredentialsController do + def valid_attributes(options = {}) + { + :credential_name => "some_name", + :credential_value => "some_value" + }.merge(options) + end + + before do + sign_in users(:bob) + end + + describe "GET index" do + it "only returns UserCredentials for the current user" do + get :index + assigns(:user_credentials).all? {|i| i.user.should == users(:bob) }.should be_true + end + end + + describe "GET edit" do + it "only shows UserCredentials for the current user" do + get :edit, :id => user_credentials(:bob_aws_secret).to_param + assigns(:user_credential).should eq(user_credentials(:bob_aws_secret)) + + lambda { + get :edit, :id => user_credentials(:jane_aws_secret).to_param + }.should raise_error(ActiveRecord::RecordNotFound) + end + end + + describe "POST create" do + it "creates UserCredentials for the current user" do + expect { + post :create, :user_credential => valid_attributes + }.to change { users(:bob).user_credentials.count }.by(1) + end + + it "shows errors" do + expect { + post :create, :user_credential => valid_attributes(:credential_name => "") + }.not_to change { users(:bob).user_credentials.count } + assigns(:user_credential).should have(1).errors_on(:credential_name) + response.should render_template("new") + end + + it "will not create UserCredentials for other users" do + expect { + post :create, :user_credential => valid_attributes(:user_id => users(:jane).id) + }.to raise_error(ActiveModel::MassAssignmentSecurity::Error) + end + end + + describe "PUT update" do + it "updates attributes on UserCredentials for the current user" do + post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "new_name" } + response.should redirect_to(user_credentials_path) + user_credentials(:bob_aws_key).reload.credential_name.should == "new_name" + + lambda { + post :update, :id => user_credentials(:jane_aws_key).to_param, :user_credential => { :credential_name => "new_name" } + }.should raise_error(ActiveRecord::RecordNotFound) + user_credentials(:jane_aws_key).reload.credential_name.should_not == "new_name" + end + + it "shows errors" do + post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "" } + assigns(:user_credential).should have(1).errors_on(:credential_name) + response.should render_template("edit") + end + end + + describe "DELETE destroy" do + it "destroys only UserCredentials owned by the current user" do + expect { + delete :destroy, :id => user_credentials(:bob_aws_key).to_param + }.to change(UserCredential, :count).by(-1) + + lambda { + delete :destroy, :id => user_credentials(:jane_aws_key).to_param + }.should raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/fixtures/user_credentials.yml b/spec/fixtures/user_credentials.yml index eabb1165..4f250315 100644 --- a/spec/fixtures/user_credentials.yml +++ b/spec/fixtures/user_credentials.yml @@ -1,8 +1,16 @@ bob_aws_key: user: bob credential_name: aws_key - credential_value: 2222222222 + credential_value: 2222222222-bob bob_aws_secret: user: bob credential_name: aws_secret - credential_value: 1111111111 + credential_value: 1111111111-bob +jane_aws_key: + user: jane + credential_name: aws_key + credential_value: 2222222222-jane +jane_aws_secret: + user: jane + credential_name: aws_secret + credential_value: 1111111111-jabe diff --git a/spec/models/agent_spec.rb b/spec/models/agent_spec.rb index eec931af..01925142 100644 --- a/spec/models/agent_spec.rb +++ b/spec/models/agent_spec.rb @@ -35,6 +35,15 @@ describe Agent do it "should return nil when credential is not present" do agents(:bob_weather_agent).credential("non_existing_credential").should == nil end + + it "should memoize the load" do + mock.any_instance_of(UserCredential).credential_value.twice { "foo" } + agents(:bob_weather_agent).credential("aws_secret").should == "foo" + agents(:bob_weather_agent).credential("aws_secret").should == "foo" + agents(:bob_weather_agent).reload + agents(:bob_weather_agent).credential("aws_secret").should == "foo" + agents(:bob_weather_agent).credential("aws_secret").should == "foo" + end end describe "changes to type" do diff --git a/spec/models/user_credential_spec.rb b/spec/models/user_credential_spec.rb index a6f11354..9b210cdf 100644 --- a/spec/models/user_credential_spec.rb +++ b/spec/models/user_credential_spec.rb @@ -2,13 +2,28 @@ require 'spec_helper' describe UserCredential do describe "validation" do - it {should validate_uniqueness_of(:credential_name).scoped_to(:user_id)} + it { should validate_uniqueness_of(:credential_name).scoped_to(:user_id) } + it { should validate_presence_of(:credential_name) } + it { should validate_presence_of(:credential_value) } + it { should validate_presence_of(:user_id) } end + describe "mass assignment" do - it {should allow_mass_assignment_of :credential_name} + it { should allow_mass_assignment_of :credential_name } - it {should allow_mass_assignment_of :credential_value} + it { should allow_mass_assignment_of :credential_value } - it {should allow_mass_assignment_of :user_id} + it { should_not allow_mass_assignment_of :user_id } + end + + describe "cleaning fields" do + it "should trim whitespace" do + user_credential = user_credentials(:bob_aws_key) + user_credential.credential_name = " new name " + user_credential.credential_value = " new value " + user_credential.save! + user_credential.credential_name.should == "new name" + user_credential.credential_value.should == "new value" + end end end diff --git a/spec/models/users_spec.rb b/spec/models/users_spec.rb index f587ab74..0ba1bad5 100644 --- a/spec/models/users_spec.rb +++ b/spec/models/users_spec.rb @@ -4,24 +4,16 @@ describe User do describe "validations" do describe "invitation_code" do it "only accepts valid invitation codes" do - User::INVITATION_CODES.each do |v| - should allow_value(v).for(:invitation_code) - end + User::INVITATION_CODES.each do |v| + should allow_value(v).for(:invitation_code) + end end it "can reject invalid invitation codes" do - %w['foo', 'bar'].each do |v| - should_not allow_value(v).for(:invitation_code) - end + %w['foo', 'bar'].each do |v| + should_not allow_value(v).for(:invitation_code) + end end end end - - describe "nested attributes" do - it { should accept_nested_attributes_for(:user_credentials).allow_destroy(true) } - end - - describe "mass assignment" do - it {should allow_mass_assignment_of :user_credentials_attributes} - end end