Project

General

Profile

Feature #19851 » 20150515_sudo_mode.diff

patch against current trunk (r14266) - Jens Krämer, 2015-05-15 10:52

View differences:

app/controllers/application_controller.rb
59 59
  include Redmine::MenuManager::MenuController
60 60
  helper Redmine::MenuManager::MenuHelper
61 61

  
62
  include Redmine::SudoMode::Controller
63

  
62 64
  def session_expiration
63 65
    if session[:user_id]
64 66
      if session_expired? && !try_to_autologin
app/controllers/auth_sources_controller.rb
21 21

  
22 22
  before_filter :require_admin
23 23
  before_filter :find_auth_source, :only => [:edit, :update, :test_connection, :destroy]
24
  require_sudo_mode :update, :destroy
24 25

  
25 26
  def index
26 27
    @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 25
app/controllers/email_addresses_controller.rb
18 18
class EmailAddressesController < ApplicationController
19 19
  before_filter :find_user, :require_admin_or_current_user
20 20
  before_filter :find_email_address, :only => [:update, :destroy]
21
  require_sudo_mode :create, :update, :destroy
21 22

  
22 23
  def index
23 24
    @addresses = @user.email_addresses.order(:id).where(:is_default => false).to_a
app/controllers/groups_controller.rb
22 22
  before_filter :find_group, :except => [:index, :new, :create]
23 23
  accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user
24 24

  
25
  require_sudo_mode :add_users, :remove_user, :create, :update, :destroy, :edit_membership, :destroy_membership
26

  
25 27
  helper :custom_fields
26 28
  helper :principal_memberships
27 29

  
app/controllers/members_controller.rb
23 23
  before_filter :authorize
24 24
  accept_api_auth :index, :show, :create, :update, :destroy
25 25

  
26
  require_sudo_mode :create, :update, :destroy
27

  
26 28
  def index
27 29
    @offset, @limit = api_offset_and_limit
28 30
    @member_count = @project.member_principals.count
app/controllers/my_controller.rb
20 20
  # let user change user's password when user has to
21 21
  skip_before_filter :check_password_change, :only => :password
22 22

  
23
  require_sudo_mode :account, only: :post
24
  require_sudo_mode :reset_rss_key, :reset_api_key, :show_api_key, :destroy
25

  
23 26
  helper :issues
24 27
  helper :users
25 28
  helper :custom_fields
......
123 126
    redirect_to my_account_path
124 127
  end
125 128

  
129
  def show_api_key
130
    @user = User.current
131
  end
132

  
126 133
  # Create a new API key
127 134
  def reset_api_key
128 135
    if request.post?
app/controllers/projects_controller.rb
25 25
  before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
26 26
  accept_rss_auth :index
27 27
  accept_api_auth :index, :show, :create, :update, :destroy
28
  require_sudo_mode :destroy
28 29

  
29 30
  after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 31
    if controller.request.post?
app/controllers/roles_controller.rb
23 23
  before_filter :find_role, :only => [:show, :edit, :update, :destroy]
24 24
  accept_api_auth :index, :show
25 25

  
26
  require_sudo_mode :create, :update, :destroy
27

  
26 28
  def index
27 29
    respond_to do |format|
28 30
      format.html {
app/controllers/settings_controller.rb
23 23

  
24 24
  before_filter :require_admin
25 25

  
26
  require_sudo_mode :index, :edit, :plugin
27

  
26 28
  def index
27 29
    edit
28 30
    render :action => 'edit'
app/controllers/users_controller.rb
28 28
  include CustomFieldsHelper
29 29
  helper :principal_memberships
30 30

  
31
  require_sudo_mode :create, :update, :destroy
32

  
31 33
  def index
32 34
    sort_init 'login', 'asc'
33 35
    sort_update %w(login firstname lastname admin created_on last_login_on)
app/helpers/application_helper.rb
25 25
  include Redmine::I18n
26 26
  include GravatarHelper::PublicMethods
27 27
  include Redmine::Pagination::Helper
28
  include Redmine::SudoMode::Helper
28 29

  
29 30
  extend Forwardable
30 31
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
app/views/my/_sidebar.html.erb
21 21
<% if Setting.rest_api_enabled? %>
22 22
<h4><%= l(:label_api_access_key) %></h4>
23 23
<div>
24
  <%= link_to_function(l(:button_show), "$('#api-access-key').toggle();")%>
25
  <pre id='api-access-key' class='autoscroll'><%= @user.api_key %></pre>
24
  <%= link_to l(:button_show), {:action => 'show_api_key'}, :remote => true %>
25
  <pre id='api-access-key' class='autoscroll'></pre>
26 26
</div>
27 27
<%= javascript_tag("$('#api-access-key').hide();") %>
28 28
<p>
app/views/my/show_api_key.html.erb
1
<h2><%= l :label_api_access_key %></h2>
2

  
3
<div class="box">
4
  <pre><%= @user.api_key %></pre>
5
</div>
6

  
7
<p><%= link_to l(:button_back), action: 'account' %></p>
8

  
9

  
10

  
app/views/my/show_api_key.js.erb
1
$('#api-access-key').html('<%= escape_javascript @user.api_key %>').toggle();
app/views/sudo_mode/_new_modal.html.erb
1
<h3 class="title"><%= l(:label_password_required) %></h3>
2
<%= form_tag({}, remote: true) do %>
3

  
4
  <%= hidden_field_tag '_method', request.request_method %>
5
  <%= hash_to_hidden_fields @sudo_form.original_fields %>
6
  <%= render_flash_messages %>
7
  <div class="box tabular">
8
    <p>
9
      <label for="sudo_password"><%= l :field_password %><span class="required">*</span></label>
10
      <%= password_field_tag :sudo_password, nil, size: 25 %>
11
    </p>
12
  </div>
13

  
14
  <p class="buttons">
15
    <%= submit_tag l(:button_confirm_password), onclick: "hideModal(this);"  %>
16
    <%= submit_tag l(:button_cancel), name: nil, onclick: "hideModal(this);", type: 'button' %>
17
  </p>
18
<% end %>
19

  
app/views/sudo_mode/new.html.erb
1
<h2><%= l :label_password_required %></h2>
2
<%= form_tag({}, class: 'tabular') do %>
3

  
4
  <%= hidden_field_tag '_method', request.request_method %>
5
  <%= hash_to_hidden_fields @sudo_form.original_fields %>
6

  
7
  <div class="box">
8
    <p>
9
      <label for="sudo_password"><%= l :field_password %><span class="required">*</span></label>
10
      <%= password_field_tag :sudo_password, nil, size: 25 %>
11
    </p>
12
  </div>
13
  <%= submit_tag l(:button_confirm_password) %>
14
<% end %>
15
<%= javascript_tag "$('#sudo_password').focus();" %>
16

  
17

  
app/views/sudo_mode/new.js.erb
1
$('#ajax-modal').html('<%= escape_javascript render partial: 'sudo_mode/new_modal' %>');
2
showModal('ajax-modal', '400px');
3
$('#sudo_password').focus();
4

  
config/locales/de.yml
163 163
  button_close: Schließen
164 164
  button_collapse_all: Alle einklappen
165 165
  button_configure: Konfigurieren
166
  button_confirm_password: Kennwort bestätigen
166 167
  button_copy: Kopieren
167 168
  button_copy_and_follow: Kopieren und Ticket anzeigen
168 169
  button_create: Anlegen
......
670 671
  label_overview: Übersicht
671 672
  label_parent_revision: Vorgänger
672 673
  label_password_lost: Kennwort vergessen
674
  label_password_required: Bitte geben Sie Ihr Kennwort ein
673 675
  label_permissions: Berechtigungen
674 676
  label_permissions_report: Berechtigungsübersicht
675 677
  label_personalize_page: Diese Seite anpassen
config/locales/en.yml
553 553
  label_register: Register
554 554
  label_login_with_open_id_option: or login with OpenID
555 555
  label_password_lost: Lost password
556
  label_password_required: Confirm your password to continue
556 557
  label_home: Home
557 558
  label_my_page: My page
558 559
  label_my_account: My account
......
980 981
  button_reset: Reset
981 982
  button_rename: Rename
982 983
  button_change_password: Change password
984
  button_confirm_password: Confirm password
983 985
  button_copy: Copy
984 986
  button_copy_and_follow: Copy and follow
985 987
  button_annotate: Annotate
config/routes.rb
67 67
  match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
68 68
  match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
69 69
  match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
70
  match 'my/show_api_key', :controller => 'my', :action => 'show_api_key', :via => :get
70 71
  match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
71 72
  match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
72 73
  match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
lib/redmine/sudo_mode.rb
1
require 'active_support/core_ext/object/to_query'
2
require 'rack/utils'
3

  
4
module Redmine
5
  module SudoMode
6

  
7
    # timespan after which sudo mode expires when unused.
8
    MAX_INACTIVITY = 15.minutes
9

  
10

  
11
    class SudoRequired < StandardError
12
    end
13

  
14

  
15
    class Form
16
      include ActiveModel::Validations
17

  
18
      attr_accessor :password, :original_fields
19
      validate :check_password
20

  
21
      def initialize(password = nil)
22
        self.password = password
23
      end
24

  
25
      def check_password
26
        unless password.present? && User.current.check_password?(password)
27
          errors[:password] << :invalid
28
        end
29
      end
30
    end
31

  
32

  
33
    module Helper
34
      # Represents params data from hash as hidden fields
35
      #
36
      # taken from https://github.com/brianhempel/hash_to_hidden_fields
37
      def hash_to_hidden_fields(hash)
38
        cleaned_hash = hash.reject { |k, v| v.nil? }
39
        pairs = cleaned_hash.to_query.split(Rack::Utils::DEFAULT_SEP)
40
        tags = pairs.map do |pair|
41
          key, value = pair.split('=', 2).map { |str| Rack::Utils.unescape(str) }
42
          hidden_field_tag(key, value)
43
        end
44
        tags.join("\n").html_safe
45
      end
46
    end
47

  
48

  
49
    module Controller
50
      extend ActiveSupport::Concern
51

  
52
      included do
53
        around_filter :sudo_mode
54
      end
55

  
56
      # Sudo mode Around Filter
57
      #
58
      # Checks the 'last used' timestamp from session and sets the
59
      # SudoMode::active? flag accordingly.
60
      #
61
      # After the request refreshes the timestamp if sudo mode was used during
62
      # this request.
63
      def sudo_mode
64
        if api_request?
65
          SudoMode.disable!
66
        elsif sudo_timestamp_valid?
67
          SudoMode.active!
68
        end
69
        yield
70
        update_sudo_timestamp! if SudoMode.was_used?
71
      end
72

  
73
      # This renders the sudo mode form / handles sudo form submission.
74
      #
75
      # Call this method in controller actions if sudo permissions are required
76
      # for processing this request. This approach is good in cases where the
77
      # action needs to be protected in any case or where the check is simple.
78
      #
79
      # In cases where this decision depends on complex conditions in the model,
80
      # consider the declarative approach using the require_sudo_mode class
81
      # method and a corresponding declaration in the model that causes it to throw
82
      # a SudoRequired Error when necessary.
83
      #
84
      # All parameter names given are included as hidden fields to be resubmitted
85
      # along with the password.
86
      #
87
      # Returns true when processing the action should continue, false otherwise.
88
      # If false is returned, render has already been called for display of the
89
      # password form.
90
      #
91
      # if @user.mail_changed?
92
      #   require_sudo_mode :user or return
93
      # end
94
      #
95
      def require_sudo_mode(*param_names)
96
        return true if SudoMode.active?
97

  
98
        if param_names.blank?
99
          param_names = params.keys - %w(id action controller sudo_password)
100
        end
101

  
102
        process_sudo_form
103

  
104
        if SudoMode.active?
105
          true
106
        else
107
          render_sudo_form param_names
108
          false
109
        end
110
      end
111

  
112
      # display the sudo password form
113
      def render_sudo_form(param_names)
114
        @sudo_form ||= SudoMode::Form.new
115
        @sudo_form.original_fields = params.slice( *param_names )
116
        # a simple 'render "sudo_mode/new"' works when used directly inside an
117
        # action, but not when called from a before_filter:
118
        respond_to do |format|
119
          format.html { render 'sudo_mode/new' }
120
          format.js   { render 'sudo_mode/new' }
121
        end
122
      end
123

  
124
      # handle sudo password form submit
125
      def process_sudo_form
126
        if params[:sudo_password]
127
          @sudo_form = SudoMode::Form.new(params[:sudo_password])
128
          if @sudo_form.valid?
129
            SudoMode.active!
130
          else
131
            flash.now[:error] = l(:notice_account_wrong_password)
132
          end
133
        end
134
      end
135

  
136
      def sudo_timestamp_valid?
137
        session[:sudo_timestamp].to_i > MAX_INACTIVITY.ago.to_i
138
      end
139

  
140
      def update_sudo_timestamp!(new_value = Time.now.to_i)
141
        session[:sudo_timestamp] = new_value
142
      end
143

  
144
      # Before Filter which is used by the require_sudo_mode class method.
145
      class SudoRequestFilter < Struct.new(:parameters, :request_methods)
146
        def before(controller)
147
          method_matches = request_methods.blank? || request_methods.include?(controller.request.method_symbol)
148
          if SudoMode.possible? && method_matches
149
            controller.require_sudo_mode( *parameters )
150
          else
151
            true
152
          end
153
        end
154
      end
155

  
156
      module ClassMethods
157

  
158
        # Handles sudo requirements for the given actions, preserving the named
159
        # parameters, or any parameters if you omit the :parameters option.
160
        #
161
        # Sudo enforcement by default is active for all requests to an action
162
        # but may be limited to a certain subset of request methods via the
163
        # :only option.
164
        #
165
        # Examples:
166
        #
167
        # require_sudo_mode :account, only: :post
168
        # require_sudo_mode :update, :create, parameters: %w(role)
169
        # require_sudo_mode :destroy
170
        #
171
        def require_sudo_mode(*args)
172
          actions = args.dup
173
          options = actions.extract_options!
174
          filter = SudoRequestFilter.new Array(options[:parameters]), Array(options[:only])
175
          before_filter filter, only: actions
176
        end
177
      end
178
    end
179

  
180

  
181
    # true if the sudo mode state was queried during this request
182
    def self.was_used?
183
      !!RequestStore.store[:sudo_mode_was_used]
184
    end
185

  
186
    # true if sudo mode is currently active.
187
    #
188
    # Calling this method also turns was_used? to true, therefore
189
    # it is important to only call this when sudo is actually needed, as the last
190
    # condition to determine wether a change can be done or not.
191
    #
192
    # If you do it wrong, timeout of the sudo mode will happen too late or not at
193
    # all.
194
    def self.active?
195
      if !!RequestStore.store[:sudo_mode]
196
        RequestStore.store[:sudo_mode_was_used] = true
197
      end
198
    end
199

  
200
    def self.active!
201
      RequestStore.store[:sudo_mode] = true
202
    end
203

  
204
    def self.possible?
205
      !disabled? && User.current.logged?
206
    end
207

  
208
    # Turn off sudo mode (never require password entry).
209
    def self.disable!
210
      RequestStore.store[:sudo_mode_disabled] = true
211
    end
212

  
213
    # Turn sudo mode back on
214
    def self.enable!
215
      RequestStore.store[:sudo_mode_disabled] = nil
216
    end
217

  
218
    def self.disabled?
219
      !!RequestStore.store[:sudo_mode_disabled]
220
    end
221

  
222
  end
223
end
224

  
test/functional/auth_sources_controller_test.rb
22 22

  
23 23
  def setup
24 24
    @request.session[:user_id] = 1
25
    Redmine::SudoMode.disable!
25 26
  end
26 27

  
27 28
  def test_index
test/functional/email_addresses_controller_test.rb
22 22

  
23 23
  def setup
24 24
    User.current = nil
25
    Redmine::SudoMode.disable!
25 26
  end
26 27

  
27 28
  def test_index_with_no_additional_emails
test/functional/groups_controller_test.rb
22 22

  
23 23
  def setup
24 24
    @request.session[:user_id] = 1
25
    Redmine::SudoMode.disable!
25 26
  end
26 27

  
27 28
  def test_index
test/functional/members_controller_test.rb
23 23
  def setup
24 24
    User.current = nil
25 25
    @request.session[:user_id] = 2
26
    Redmine::SudoMode.disable!
26 27
  end
27 28

  
28 29
  def test_new
test/functional/my_controller_test.rb
23 23

  
24 24
  def setup
25 25
    @request.session[:user_id] = 2
26
    Redmine::SudoMode.disable!
26 27
  end
27 28

  
28 29
  def test_index
......
253 254
    assert_redirected_to '/my/account'
254 255
  end
255 256

  
257
  def test_show_api_key
258
    get :show_api_key
259
    assert_response :success
260
    assert_select 'pre', User.find(2).api_key
261
  end
262

  
256 263
  def test_reset_api_key_with_existing_key
257 264
    @previous_token_value = User.find(2).api_key # Will generate one if it's missing
258 265
    post :reset_api_key
test/functional/projects_controller_test.rb
28 28
  def setup
29 29
    @request.session[:user_id] = nil
30 30
    Setting.default_language = 'en'
31
    Redmine::SudoMode.disable!
31 32
  end
32 33

  
33 34
  def test_index_by_anonymous_should_not_show_private_projects
test/functional/roles_controller_test.rb
23 23
  def setup
24 24
    User.current = nil
25 25
    @request.session[:user_id] = 1 # admin
26
    Redmine::SudoMode.disable!
26 27
  end
27 28

  
28 29
  def test_index
test/functional/settings_controller_test.rb
24 24
  def setup
25 25
    User.current = nil
26 26
    @request.session[:user_id] = 1 # admin
27
    Redmine::SudoMode.disable!
27 28
  end
28 29

  
29 30
  def test_index
test/functional/users_controller_test.rb
30 30
  def setup
31 31
    User.current = nil
32 32
    @request.session[:user_id] = 1 # admin
33
    Redmine::SudoMode.disable!
33 34
  end
34 35

  
35 36
  def test_index
test/integration/admin_test.rb
26 26
           :members,
27 27
           :enabled_modules
28 28

  
29
  def setup
30
    Redmine::SudoMode.enable!
31
  end
32

  
33
  def teardown
34
    Redmine::SudoMode.disable!
35
  end
36

  
29 37
  def test_add_user
30 38
    log_user("admin", "admin")
31 39
    get "/users/new"
......
36 44
                    :lastname => "Smith", :mail => "psmith@somenet.foo",
37 45
                    :language => "en", :password => "psmith09",
38 46
                    :password_confirmation => "psmith09" }
47
    assert_response :success
48
    assert_nil User.find_by_login("psmith")
49

  
50
    post "/users",
51
         :user => { :login => "psmith", :firstname => "Paul",
52
                    :lastname => "Smith", :mail => "psmith@somenet.foo",
53
                    :language => "en", :password => "psmith09",
54
                    :password_confirmation => "psmith09" },
55
         :sudo_password => 'admin'
39 56

  
40 57
    user = User.find_by_login("psmith")
41 58
    assert_kind_of User, user
test/integration/sudo_test.rb
1
require File.expand_path('../../test_helper', __FILE__)
2

  
3
class SudoTest < Redmine::IntegrationTest
4
  fixtures :projects, :members, :member_roles, :roles, :users
5

  
6
  def setup
7
    Redmine::SudoMode.enable!
8
  end
9

  
10
  def teardown
11
    Redmine::SudoMode.disable!
12
  end
13

  
14
  def test_create_member_xhr
15
    log_user 'admin', 'admin'
16
    get '/projects/ecookbook/settings/members'
17
    assert_response :success
18

  
19
    assert_no_difference 'Member.count' do
20
      xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}
21
    end
22

  
23
    assert_no_difference 'Member.count' do
24
      xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: ''
25
    end
26

  
27
    assert_no_difference 'Member.count' do
28
      xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong'
29
    end
30

  
31
    assert_difference 'Member.count' do
32
      xhr :post, '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin'
33
    end
34
    assert User.find(7).member_of?(Project.find(1))
35
  end
36

  
37
  def test_create_member
38
    log_user 'admin', 'admin'
39
    get '/projects/ecookbook/settings/members'
40
    assert_response :success
41

  
42
    assert_no_difference 'Member.count' do
43
      post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}
44
    end
45

  
46
    assert_no_difference 'Member.count' do
47
      post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: ''
48
    end
49

  
50
    assert_no_difference 'Member.count' do
51
      post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'wrong'
52
    end
53

  
54
    assert_difference 'Member.count' do
55
      post '/projects/ecookbook/memberships', membership: {role_ids: [1], user_id: 7}, sudo_password: 'admin'
56
    end
57

  
58
    assert_redirected_to '/projects/ecookbook/settings/members'
59
    assert User.find(7).member_of?(Project.find(1))
60
  end
61

  
62
  def test_create_role
63
    log_user 'admin', 'admin'
64
    get '/roles'
65
    assert_response :success
66

  
67
    get '/roles/new'
68
    assert_response :success
69

  
70
    post '/roles', role: { }
71
    assert_response :success
72
    assert_select 'h2', 'Confirm your password to continue'
73
    assert_select 'form[action="/roles"]'
74
    assert assigns(:sudo_form).errors.blank?
75

  
76
    post '/roles', role: { name: 'new role', issues_visibility: 'all' }
77
    assert_response :success
78
    assert_select 'h2', 'Confirm your password to continue'
79
    assert_select 'form[action="/roles"]'
80
    assert_match /"new role"/, response.body
81
    assert assigns(:sudo_form).errors.blank?
82

  
83
    post '/roles', role: { name: 'new role', issues_visibility: 'all' }, sudo_password: 'wrong'
84
    assert_response :success
85
    assert_select 'h2', 'Confirm your password to continue'
86
    assert_select 'form[action="/roles"]'
87
    assert_match /"new role"/, response.body
88
    assert assigns(:sudo_form).errors[:password].present?
89

  
90
    assert_difference 'Role.count' do
91
      post '/roles', role: { name: 'new role', issues_visibility: 'all', assignable: '1', permissions: %w(view_calendar) }, sudo_password: 'admin'
92
    end
93
    assert_redirected_to '/roles'
94
  end
95

  
96
  def test_update_email_address
97
    log_user 'jsmith', 'jsmith'
98
    get '/my/account'
99
    assert_response :success
100
    post '/my/account', user: { mail: 'newmail@test.com' }
101
    assert_response :success
102
    assert_select 'h2', 'Confirm your password to continue'
103
    assert_select 'form[action="/my/account"]'
104
    assert_match /"newmail@test\.com"/, response.body
105
    assert assigns(:sudo_form).errors.blank?
106

  
107
    # wrong password
108
    post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'wrong'
109
    assert_response :success
110
    assert_select 'h2', 'Confirm your password to continue'
111
    assert_select 'form[action="/my/account"]'
112
    assert_match /"newmail@test\.com"/, response.body
113
    assert assigns(:sudo_form).errors[:password].present?
114

  
115
    # correct password
116
    post '/my/account', user: { mail: 'newmail@test.com' }, sudo_password: 'jsmith'
117
    assert_redirected_to '/my/account'
118
    assert_equal 'newmail@test.com', User.find_by_login('jsmith').mail
119

  
120
    # sudo mode should now be active and not require password again
121
    post '/my/account', user: { mail: 'even.newer.mail@test.com' }
122
    assert_redirected_to '/my/account'
123
    assert_equal 'even.newer.mail@test.com', User.find_by_login('jsmith').mail
124
  end
125

  
126
end
    (1-1/1)