Merge pull request #1330 from kreuzwerker/feature/user-admin-interface

Admin user management
This commit is contained in:
Dominik Sander 2016-03-17 21:02:27 +01:00
commit bf7c2feba4
25 changed files with 617 additions and 49 deletions

View file

@ -40,9 +40,9 @@ DATABASE_PASSWORD=""
# Should Rails force all requests to use SSL?
FORCE_SSL=false
############################
# Allowing Signups #
############################
################################################
# User authentication and registration #
################################################
# This invitation code will be required for users to signup with your Huginn installation.
# You can see its use in user.rb. PLEASE CHANGE THIS!
@ -51,6 +51,36 @@ INVITATION_CODE=try-huginn
# If you don't want to require new users to have an invitation code in order to sign up, set this to true.
SKIP_INVITATION_CODE=false
# If you'd like to require new users to confirm their email address after sign up, set this to true.
REQUIRE_CONFIRMED_EMAIL=false
# If REQUIRE_CONFIRMED_EMAIL is true, set this to the duration in which a user needs to confirm their email address.
ALLOW_UNCONFIRMED_ACCESS_FOR=2.days
# Duration for which the above confirmation token is valid
CONFIRM_WITHIN=3.days
# Minimum password length
MIN_PASSWORD_LENGTH=8
# Duration for which the reset password token is valid
RESET_PASSWORD_WITHIN=6.hours
# Set to 'failed_attempts' to lock user accounts for the UNLOCK_AFTER period they fail MAX_FAILED_LOGIN_ATTEMPTS login attempts. Set to 'none' to allow unlimited failed login attempts.
LOCK_STRATEGY=failed_attempts
# After how many failed login attempts the account is locked when LOCK_STRATEGY is set to failed_attempts.
MAX_FAILED_LOGIN_ATTEMPTS=10
# Can be set to 'email', 'time', 'both' or 'none'. 'none' requires manual unlocking of your users!
UNLOCK_STRATEGY=both
# Duration after which the user is unlocked when UNLOCK_STRATEGY is 'both' or 'time' and LOCK_STRATEGY is 'failed_attempts'
UNLOCK_AFTER=1.hour
# Duration for which the user will be remembered without asking for credentials again.
REMEMBER_FOR=4.weeks
#############################
# Email Configuration #
#############################

View file

@ -0,0 +1,94 @@
class Admin::UsersController < ApplicationController
before_action :authenticate_admin!
before_action :find_user, only: [:edit, :destroy, :update, :deactivate, :activate]
helper_method :resource
def index
@users = User.reorder('created_at DESC').page(params[:page])
respond_to do |format|
format.html
format.json { render json: @users }
end
end
def new
@user = User.new
end
def create
admin = params[:user].delete(:admin)
@user = User.new(params[:user])
@user.requires_no_invitation_code!
@user.admin = admin
respond_to do |format|
if @user.save
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was successfully created." }
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
else
format.html { render action: 'new' }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
def edit
end
def update
admin = params[:user].delete(:admin)
params[:user].except!(:password, :password_confirmation) if params[:user][:password].blank?
@user.assign_attributes(params[:user])
@user.admin = admin
respond_to do |format|
if @user.save
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was successfully updated." }
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
else
format.html { render action: 'edit' }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
def destroy
@user.destroy
respond_to do |format|
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was deleted." }
format.json { head :no_content }
end
end
def deactivate
@user.deactivate!
respond_to do |format|
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was deactivated." }
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
end
end
def activate
@user.activate!
respond_to do |format|
format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was activated." }
format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
end
end
private
def find_user
@user = User.find(params[:id])
end
def resource
@user
end
end

View file

@ -0,0 +1,13 @@
module UsersHelper
def user_account_state(user)
if !user.active?
content_tag :span, 'inactive', class: 'label label-danger'
elsif user.access_locked?
content_tag :span, 'locked', class: 'label label-danger'
elsif ENV['REQUIRE_CONFIRMED_EMAIL'] == 'true' && !user.confirmed?
content_tag :span, 'unconfirmed', class: 'label label-warning'
else
content_tag :span, 'active', class: 'label label-success'
end
end
end

View file

@ -61,8 +61,8 @@ class Agent < ActiveRecord::Base
has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :agent
has_many :scenarios, :through => :scenario_memberships, :inverse_of => :agents
scope :active, -> { where(disabled: false) }
scope :inactive, -> { where(disabled: true) }
scope :active, -> { where(disabled: false, deactivated: false) }
scope :inactive, -> { where(['disabled = ? OR deactivated = ?', true, true]) }
scope :of_type, lambda { |type|
type = case type
@ -381,7 +381,7 @@ class Agent < ActiveRecord::Base
joins("JOIN links ON (links.receiver_id = agents.id)").
joins("JOIN agents AS sources ON (links.source_id = sources.id)").
joins("JOIN events ON (events.agent_id = sources.id AND events.id > links.event_id_at_creation)").
where("NOT agents.disabled AND (agents.last_checked_event_id IS NULL OR events.id > agents.last_checked_event_id)")
where("NOT agents.disabled AND NOT agents.deactivated AND (agents.last_checked_event_id IS NULL OR events.id > agents.last_checked_event_id)")
if options[:only_receivers].present?
scope = scope.where("agents.id in (?)", options[:only_receivers])
end
@ -432,7 +432,7 @@ class Agent < ActiveRecord::Base
# per type of agent, so you can override this to define custom bulk check behavior for your custom Agent type.
def bulk_check(schedule)
raise "Call #bulk_check on the appropriate subclass of Agent" if self == Agent
where("agents.schedule = ? and disabled = false", schedule).pluck("agents.id").each do |agent_id|
where("NOT disabled AND NOT deactivated AND schedule = ?", schedule).pluck("agents.id").each do |agent_id|
async_check(agent_id)
end
end

View file

@ -1,8 +1,10 @@
# Huginn is designed to be a multi-User system. Users have many Agents (and Events created by those Agents).
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :lockable,
:omniauthable
DEVISE_MODULES = [:database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable,
:validatable, :lockable, :omniauthable,
(ENV['REQUIRE_CONFIRMED_EMAIL'] == 'true' ? :confirmable : nil)].compact
devise *DEVISE_MODULES
INVITATION_CODES = [ENV['INVITATION_CODE'] || 'try-huginn']
@ -16,9 +18,9 @@ class User < ActiveRecord::Base
attr_accessible *(ACCESSIBLE_ATTRIBUTES + [:admin]), :as => :admin
validates_presence_of :username
validates_uniqueness_of :username
validates :username, uniqueness: { case_sensitive: false }
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", if: ->{ User.using_invitation_code? }
validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid", if: -> { !requires_no_invitation_code? && User.using_invitation_code? }
has_many :user_credentials, :dependent => :destroy, :inverse_of => :user
has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user
@ -41,7 +43,41 @@ class User < ActiveRecord::Base
end
end
def active?
!deactivated_at
end
def deactivate!
User.transaction do
agents.update_all(deactivated: true)
update_attribute(:deactivated_at, Time.now)
end
end
def activate!
User.transaction do
agents.update_all(deactivated: false)
update_attribute(:deactivated_at, nil)
end
end
def active_for_authentication?
super && active?
end
def inactive_message
active? ? super : :deactivated_account
end
def self.using_invitation_code?
ENV['SKIP_INVITATION_CODE'] != 'true'
end
def requires_no_invitation_code!
@requires_no_invitation_code = true
end
def requires_no_invitation_code?
!!@requires_no_invitation_code
end
end

View file

@ -0,0 +1,26 @@
<%= form_for([:admin, @user], html: { class: 'form-horizontal' }) do |f| %>
<%= devise_error_messages! %>
<%= render partial: '/devise/registrations/common_registration_fields', locals: { f: f } %>
<div class="form-group">
<div class="col-md-offset-4 col-md-10">
<%= f.label :admin do %>
<%= f.check_box :admin %> Admin
<% end %>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-4 col-md-10">
<%= f.submit class: "btn btn-primary" %>
</div>
</div>
<% end %>
<hr>
<div class="row">
<div class="col-md-12">
<%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, admin_users_path, class: "btn btn-default" %>
</div>
</div>

View file

@ -0,0 +1,9 @@
<div class='container'>
<div class='row'>
<div class='col-md-12'>
<h2>Edit User</h2>
<%= render partial: 'form' %>
</div>
</div>
</div>

View file

@ -0,0 +1,55 @@
<div class='container'>
<div class='row'>
<div class='col-md-12'>
<div class="page-header">
<h2>
Users
</h2>
</div>
<div class='table-responsive'>
<table class='table table-striped events'>
<tr>
<th>Username</th>
<th>Email</th>
<th>State</th>
<th>Active agents</th>
<th>Deactivated agents</th>
<th>Registered since</th>
<th>Options</th>
</tr>
<% @users.each do |user| %>
<tr>
<td><%= link_to user.username, edit_admin_user_path(user) %></td>
<td><%= user.email %></td>
<td><%= user_account_state(user) %></td>
<td><%= user.agents.active.count %></td>
<td><%= user.agents.inactive.count %></td>
<td title='<%= user.created_at %>'><%= time_ago_in_words user.created_at %> ago</td>
<td>
<div class="btn-group btn-group-xs">
<% if user != current_user %>
<% if user.active? %>
<%= link_to 'Deactivate', deactivate_admin_user_path(user), method: :put, class: "btn btn-default" %>
<% else %>
<%= link_to 'Activate', activate_admin_user_path(user), method: :put, class: "btn btn-default" %>
<% end %>
<%= link_to 'Delete', admin_user_path(user), method: :delete, data: { confirm: 'Are you sure? This can not be undone.' }, class: "btn btn-default" %>
<% end %>
</div>
</td>
</tr>
<% end %>
</table>
</div>
<%= paginate @users, theme: 'twitter-bootstrap-3' %>
<div class="btn-group">
<%= link_to icon_tag('glyphicon-plus') + ' New User', new_admin_user_path, class: "btn btn-default" %>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,9 @@
<div class='container'>
<div class='row'>
<div class='col-md-12'>
<h2>Create new User</h2>
<%= render partial: 'form' %>
</div>
</div>
</div>

View file

@ -0,0 +1,28 @@
<div class="form-group">
<%= f.label :email, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.email_field :email, autofocus: true, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<%= f.label :username, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.text_field :username, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<%= f.label :password, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
<% if @validatable %><span class="help-inline"><%= @minimum_password_length %> characters minimum.</span><% end %>
</div>
</div>
<div class="form-group">
<%= f.label :password_confirmation, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
</div>
</div>

View file

@ -41,34 +41,7 @@ bin/setup_heroku
</div>
<% end %>
<div class="form-group">
<%= f.label :email, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.email_field :email, autofocus: true, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<%= f.label :username, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.text_field :username, class: 'form-control' %>
</div>
</div>
<div class="form-group">
<%= f.label :password, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
<% if @validatable %><span class="help-inline"><%= @minimum_password_length %> characters minimum.</span><% end %>
</div>
</div>
<div class="form-group">
<%= f.label :password_confirmation, class: 'col-md-4 control-label' %>
<div class="col-md-6">
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
</div>
</div>
<%= render partial: 'common_registration_fields', locals: { f: f } %>
<div class="form-group">
<div class="col-md-offset-4 col-md-10">

View file

@ -74,6 +74,9 @@
<li>
<%= link_to 'Job Management', jobs_path, :tabindex => '-1' %>
</li>
<li>
<%= link_to 'User Management', admin_users_path, tabindex: '-1' %>
</li>
<% end %>
<li>
<%= link_to 'About', 'https://github.com/cantino/huginn', :tabindex => "-1" %>

View file

@ -103,7 +103,7 @@ Devise.setup do |config|
# able to access the website for two days without confirming their account,
# access will be blocked just in the third day. Default is 0.days, meaning
# the user cannot access the website without confirming their account.
# config.allow_unconfirmed_access_for = 2.days
config.allow_unconfirmed_access_for = Utils.parse_duration(ENV['ALLOW_UNCONFIRMED_ACCESS_FOR']).presence || 2.days
# A period that the user is allowed to confirm their account before their
# token becomes invalid. For example, if set to 3.days, the user can confirm
@ -111,7 +111,7 @@ Devise.setup do |config|
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
# config.confirm_within = 3.days
config.confirm_within = Utils.parse_duration(ENV['CONFIRM_WITHIN']).presence || 3.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
@ -124,7 +124,7 @@ Devise.setup do |config|
# ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again.
config.remember_for = 4.weeks
config.remember_for = Utils.parse_duration(ENV['REMEMBER_FOR']).presence || 4.weeks
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true
@ -142,7 +142,7 @@ Devise.setup do |config|
# ==> Configuration for :validatable
# Range for password length.
config.password_length = 8..128
config.password_length = (Utils.if_present(ENV['MIN_PASSWORD_LENGTH'], :to_i) || 8)..128
# Email regex used to validate email formats. It simply asserts that
# one (and only one) @ exists in the given string. This is mainly
@ -158,7 +158,7 @@ Devise.setup do |config|
# Defines which strategy will be used to lock an account.
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
# :none = No lock strategy. You should handle locking by yourself.
config.lock_strategy = :failed_attempts
config.lock_strategy = Utils.if_present(ENV['LOCK_STRATEGY'], :to_sym) || :failed_attempts
# Defines which key will be used when locking and unlocking an account
config.unlock_keys = [ :email ]
@ -168,14 +168,14 @@ Devise.setup do |config|
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
# :both = Enables both strategies
# :none = No unlock strategy. You should handle unlocking by yourself.
config.unlock_strategy = :both
config.unlock_strategy = Utils.if_present(ENV['UNLOCK_STRATEGY'], :to_sym) || :both
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
config.maximum_attempts = 10
config.maximum_attempts = Utils.if_present(ENV['MAX_FAILED_LOGIN_ATTEMPTS'], :to_i) || 10
# Time interval to unlock the account if :time is enabled as unlock_strategy.
config.unlock_in = 1.hour
config.unlock_in = Utils.parse_duration(ENV['UNLOCK_AFTER']).presence || 1.hour
# Warn on the last attempt before the account is locked.
# config.last_attempt_warning = true
@ -188,7 +188,7 @@ Devise.setup do |config|
# Time interval you can reset your password with a reset password key.
# Don't put a too small interval or your users won't have the time to
# change their passwords.
config.reset_password_within = 6.hours
config.reset_password_within = Utils.parse_duration(ENV['RESET_PASSWORD_WITHIN']).presence || 6.hours
# ==> Configuration for :encryptable
# Allow you to use another encryption algorithm besides bcrypt (default). You can use

View file

@ -2,6 +2,9 @@
# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
en:
devise:
failure:
deactivated_account: "Your account has been deactivated by an administrator."
datetime:
distance_in_words:
half_a_minute: "half a minute"

View file

@ -66,6 +66,15 @@ Huginn::Application.routes.draw do
end
end
namespace :admin do
resources :users, except: :show do
member do
put :deactivate
put :activate
end
end
end
get "/worker_status" => "worker_status#show"
match "/users/:user_id/web_requests/:agent_id/:secret" => "web_requests#handle_request", :as => :web_requests, :via => [:get, :post, :put, :delete]

View file

@ -0,0 +1,17 @@
class AddConfirmableAttributesToUsers < ActiveRecord::Migration
def change
change_table(:users) do |t|
## Confirmable
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email # Only if using reconfirmable
end
add_index :users, :confirmation_token, unique: true
if ENV['REQUIRE_CONFIRMED_EMAIL'] != 'true' && ActiveRecord::Base.connection.column_exists?(:users, :confirmed_at)
User.update_all('confirmed_at = NOW()')
end
end
end

View file

@ -0,0 +1,7 @@
class AddDeactivatedAtToUsers < ActiveRecord::Migration
def change
add_column :users, :deactivated_at, :datetime
add_index :users, :deactivated_at
end
end

View file

@ -0,0 +1,6 @@
class AddDeactivatedToAgents < ActiveRecord::Migration
def change
add_column :agents, :deactivated, :boolean, default: false
add_index :agents, [:disabled, :deactivated]
end
end

View file

@ -0,0 +1,21 @@
class WarnAboutDuplicateUsernames < ActiveRecord::Migration
def up
names = User.group('LOWER(username)').having('count(*) > 1').pluck('LOWER(username)')
if names.length > 0
puts "-----------------------------------------------------"
puts "--------------------- WARNiNG -----------------------"
puts "-------- Found users with duplicate usernames -------"
puts "-----------------------------------------------------"
puts "For the users to log in using their username they have to change it to a unique name"
names.each do |name|
puts
puts "'#{name}' is used multiple times:"
User.where(['LOWER(username) = ?', name]).each do |u|
puts "#{u.id}\t#{u.email}"
end
end
puts
puts
end
end
end

View file

@ -130,4 +130,25 @@ module Utils
def self.sort_tuples!(array, orders = [])
TupleSorter.sort!(array, orders)
end
def self.parse_duration(string)
return nil if string.blank?
case string.strip
when /\A(\d+)\.(\w+)\z/
$1.to_i.send($2.to_s)
when /\A(\d+)\z/
$1.to_i
else
STDERR.puts "WARNING: Invalid duration format: '#{string.strip}'"
nil
end
end
def self.if_present(string, method)
if string.present?
string.send(method)
else
nil
end
end
end

View file

@ -11,3 +11,4 @@ WUNDERLIST_OAUTH_KEY=wunderoauthkey
EVERNOTE_OAUTH_KEY=evernoteoauthkey
EVERNOTE_OAUTH_SECRET=evernoteoauthsecret
FAILED_JOBS_TO_KEEP=2
REQUIRE_CONFIRMED_EMAIL=false

View file

@ -0,0 +1,107 @@
require 'capybara_helper'
describe Admin::UsersController do
it "requires to be signed in as an admin" do
login_as(users(:bob))
visit admin_users_path
expect(page).to have_text('Admin access required to view that page.')
end
context "as an admin" do
before :each do
login_as(users(:jane))
end
it "lists all users" do
visit admin_users_path
expect(page).to have_text('bob')
expect(page).to have_text('jane')
end
it "allows to delete a user" do
visit admin_users_path
find(:css, "a[href='/admin/users/#{users(:bob).id}']").click
expect(page).to have_text("User 'bob' was deleted.")
expect(page).not_to have_text('bob@example.com')
end
context "creating new users" do
it "follow the 'new user' link" do
visit admin_users_path
click_on('New User')
expect(page).to have_text('Create new User')
end
it "creates a new user" do
visit new_admin_user_path
fill_in 'Email', with: 'test@test.com'
fill_in 'Username', with: 'usertest'
fill_in 'Password', with: '12345678'
fill_in 'Password confirmation', with: '12345678'
click_on 'Create User'
expect(page).to have_text("User 'usertest' was successfully created.")
expect(page).to have_text('test@test.com')
end
it "requires the passwords to match" do
visit new_admin_user_path
fill_in 'Email', with: 'test@test.com'
fill_in 'Username', with: 'usertest'
fill_in 'Password', with: '12345678'
fill_in 'Password confirmation', with: 'no_match'
click_on 'Create User'
expect(page).to have_text("Password confirmation doesn't match")
end
end
context "updating existing users" do
it "follows the edit link" do
visit admin_users_path
click_on('bob')
expect(page).to have_text('Edit User')
end
it "updates an existing user" do
visit edit_admin_user_path(users(:bob))
check 'Admin'
click_on 'Update User'
expect(page).to have_text("User 'bob' was successfully updated.")
visit edit_admin_user_path(users(:bob))
expect(page).to have_checked_field('Admin')
end
it "requires the passwords to match when changing them" do
visit edit_admin_user_path(users(:bob))
fill_in 'Password', with: '12345678'
fill_in 'Password confirmation', with: 'no_match'
click_on 'Update User'
expect(page).to have_text("Password confirmation doesn't match")
end
end
context "(de)activating users" do
it "does not show deactivation buttons for the current user" do
visit admin_users_path
expect(page).not_to have_css("a[href='/admin/users/#{users(:jane).id}/deactivate']")
end
it "deactivates an existing user" do
visit admin_users_path
expect(page).not_to have_text('inactive')
find(:css, "a[href='/admin/users/#{users(:bob).id}/deactivate']").click
expect(page).to have_text('inactive')
users(:bob).reload
expect(users(:bob)).not_to be_active
end
it "deactivates an existing user" do
users(:bob).deactivate!
visit admin_users_path
find(:css, "a[href='/admin/users/#{users(:bob).id}/activate']").click
expect(page).not_to have_text('inactive')
users(:bob).reload
expect(users(:bob)).to be_active
end
end
end
end

View file

@ -172,4 +172,33 @@ describe Utils do
expect(tuples).to eq expected
end
end
context "#parse_duration" do
it "works with correct arguments" do
expect(Utils.parse_duration('2.days')).to eq(2.days)
expect(Utils.parse_duration('2.seconds')).to eq(2)
expect(Utils.parse_duration('2')).to eq(2)
end
it "returns nil when passed nil" do
expect(Utils.parse_duration(nil)).to be_nil
end
it "warns and returns nil when not parseable" do
mock(STDERR).puts("WARNING: Invalid duration format: 'bogus'")
expect(Utils.parse_duration('bogus')).to be_nil
end
end
context "#if_present" do
it "returns nil when passed nil" do
expect(Utils.if_present(nil, :to_i)).to be_nil
end
it "calls the specified method when the argument is present" do
argument = mock()
mock(argument).to_i { 1 }
expect(Utils.if_present(argument, :to_i)).to eq(1)
end
end
end

View file

@ -3,6 +3,34 @@ require 'rails_helper'
describe Agent do
it_behaves_like WorkingHelpers
describe '.active/inactive' do
let(:agent) { agents(:jane_website_agent) }
it 'is active per default' do
expect(Agent.active).to include(agent)
expect(Agent.inactive).not_to include(agent)
end
it 'is not active when disabled' do
agent.update_attribute(:disabled, true)
expect(Agent.active).not_to include(agent)
expect(Agent.inactive).to include(agent)
end
it 'is not active when deactivated' do
agent.update_attribute(:deactivated, true)
expect(Agent.active).not_to include(agent)
expect(Agent.inactive).to include(agent)
end
it 'is not active when disabled and deactivated' do
agent.update_attribute(:disabled, true)
agent.update_attribute(:deactivated, true)
expect(Agent.active).not_to include(agent)
expect(Agent.inactive).to include(agent)
end
end
describe ".bulk_check" do
before do
@weather_agent_count = Agents::WeatherAgent.where(:schedule => "midnight", :disabled => false).count
@ -18,6 +46,12 @@ describe Agent do
mock(Agents::WeatherAgent).async_check(anything).times(@weather_agent_count - 1)
Agents::WeatherAgent.bulk_check("midnight")
end
it "should skip agents of deactivated accounts" do
agents(:bob_weather_agent).user.deactivate!
mock(Agents::WeatherAgent).async_check(anything).times(@weather_agent_count - 1)
Agents::WeatherAgent.bulk_check("midnight")
end
end
describe ".run_schedule" do
@ -335,6 +369,13 @@ describe Agent do
Agent.receive! # and we receive it
}.to change { agents(:bob_rain_notifier_agent).reload.last_checked_event_id }
end
it "should not run agents of deactivated accounts" do
agents(:bob_weather_agent).user.deactivate!
Agent.async_check(agents(:bob_weather_agent).id)
mock(Agent).async_receive(agents(:bob_rain_notifier_agent).id, anything).times(0)
Agent.receive!
end
end
describe ".async_receive" do

View file

@ -19,6 +19,12 @@ describe User do
should_not allow_value(v).for(:invitation_code)
end
end
it "requires no authentication code when requires_no_invitation_code! is called" do
u = User.new(username: 'test', email: 'test@test.com', password: '12345678', password_confirmation: '12345678')
u.requires_no_invitation_code!
expect(u).to be_valid
end
end
context "when configured not to use invitation codes" do
@ -34,4 +40,28 @@ describe User do
end
end
end
context '#deactivate!' do
it "deactivates the user and all her agents" do
agent = agents(:jane_website_agent)
users(:jane).deactivate!
agent.reload
expect(agent.deactivated).to be_truthy
expect(users(:jane).deactivated_at).not_to be_nil
end
end
context '#activate!' do
before do
users(:bob).deactivate!
end
it 'activates the user and all his agents' do
agent = agents(:bob_website_agent)
users(:bob).activate!
agent.reload
expect(agent.deactivated).to be_falsy
expect(users(:bob).deactivated_at).to be_nil
end
end
end