Mailinglist Archive: yast-devel (246 mails)

< Previous Next >
[yast-devel] users controller refactoring
  • From: "Duncan Mac-Vicar Prett" <dmacvicar@xxxxxxx>
  • Date: Wed, 5 Aug 2009 18:21:26 +0200
  • Message-id: <200908051821.26322.dmacvicar@xxxxxxx>

Trying to get the coverage running for all directories, I got stuck at the
user's one was failing because the testcase is still Scr based, when the
module is actually using the YaPI.

Once I tried to make the test pass I realized the code was not structured in a
way that makes it easy to test. Mainly, I found the following flaws:

- The controller did direct calls to YaPI

I pointed at this pattern in a previous email. If you want to test this, you
will need to stub the YaPI, which is a lot of noise for a controller

- The controller called instance modifier methods

This is a very confusing way of writing code. Basically, you are in a method,
lets say index. And from it you call another method, lets say fill_users,
which modifies an instance variable, which is later used in the code.

def index
# this method modifies/creates @var
fill_vars
# and from now, the code relies on what fill_vars did
@var.do_something

It is very hard to figure out that fill_users does something with @users if
you don't look at the implementation. As fill_users does not require anything
from the controller, why does it need instance variables? either move the code
to a module, or where it belongs, In this case, it makes sense to actually put
it into the class where it belongs: User

The only reason why in rails you have @variables is because the view accesses
them. But 1) if it is not passed to the view, there is no reason for member
variables 2) member variables is not a way to pass data across functions, it
is the state of the object!

Refactorings done:

- all the instance modifier methods where removed. Similar methods are now in
the User class:

users = User.find_all
user = User.find(id)
user = User.create(id)
user.save
user.destroy

The API imitates the ActiveRecord/ActiveResource one. Why not, that is what
people is used to see in controllers.

This also allows to separate testing in 2 parts:

- the User class test, for which mocha stubs YaPI calls
- the controller class, for which mocha stubs User class class

This makes the controller test much simpler, you need to stub User class to
return arrays of User, and not deal with stubing the complicated YaPI calls,
why should the controller know about those anyway. It is the User class the
class that is hiding that complexity.

I haven't enabled the User class tests because I get this when calling YaPI
calls in my machine:

TypeError: can't convert Array into String
/usr/lib64/ruby/vendor_ruby/1.8/dbus/type.rb:108:in `+'
/usr/lib64/ruby/vendor_ruby/1.8/dbus/type.rb:108:in `to_s'
/usr/lib64/ruby/vendor_ruby/1.8/dbus/marshall.rb:352:in `append'

However I wanted to post the patch for review (and I guess I still need to see
if it works, once the tests are done)

Another pattern is where to use static methods or not. Consider the destroy
method. If you implement it in the class User

def destroy
# do something with yapi and @id
end

You can define it as a class method:

def self.destroy(id)
# do something with yapi and id
end

And then define the previous one just as:
def destroy
self.class.destroy(@id)
end

Then you can use either as User.destroy("schubi") or as user.destroy (assuming
user is an User object)

--
Duncan Mac-Vicar P. - Engineering Manager, YaST
SUSE LINUX Products GmbH, GF: Markus Rex, HRB 16746 (AG Nuernberg)

diff --git a/plugins/users/app/controllers/users_controller.rb
b/plugins/users/app/controllers/users_controller.rb
index 59a0f1e..0e3d4e2 100644
--- a/plugins/users/app/controllers/users_controller.rb
+++ b/plugins/users/app/controllers/users_controller.rb
@@ -1,5 +1,4 @@
# import YastService class FIXME move into the model...
-require "yast_service"

include ApplicationHelper

@@ -8,132 +7,7 @@ class UsersController < ApplicationController
before_filter :login_required

def initialize
- @scr = Scr.instance
end
-
-#--------------------------------------------------------------------------------
-#
-#local methods
-#
-#--------------------------------------------------------------------------------
- def get_user_list
- @users = []
- parameters = {
- # how to index hash with users
- "index" => ["s", "uid"],
- # attributes to return for each user
- "user_attributes" => ["as", [ "cn" ]]
- }
- users_map = YastService.Call("YaPI::USERS::UsersGet", parameters)
- if users_map.nil?
- puts "something wrong happened -------------------------------------"
- else
- users_map.each do |key, val|
- user = User.new
- user.uid = key
- user.cn = val["cn"]
- @users << user
- end
- end
- end
-
- def get_user (id)
- parameters = {
- # user to find
- "uid" => [ "s", id ],
- # list of attributes to return;
- "user_attributes" => [ "as", [
- "cn", "uidNumber", "homeDirectory",
- "grouplist", "uid", "loginShell",
"groupname"
- ]]
- }
- user_map = YastService.Call("YaPI::USERS::UserGet", parameters)
- # TODO check if it is not empty
-
- @user = User.new
-
- #FIXME why User.new (user_map) does not work?
-
- # convert camel-cased YaST keys to ruby's under_scored ones
- @user.grouplist = user_map["grouplist"]
- @user.home_directory = user_map["homeDirectory"]
- @user.groupname = user_map["groupname"]
- @user.login_shell = user_map["loginShell"]
- @user.uid = id
- @user.uid_number = user_map["uidNumber"]
- @user.cn = user_map["cn"]
-
- return true
- end
-
- # -------------------------------------------------------
- # modify existing user
- def update_user userId
- config = {
- "type" => [ "s", "local" ],
- "uid" => [ "s", @user.uid ]
- }
- # => FIXME convert ruby's under_scored keys to YaST's camel-cased ones
- data = {
- "uid" => [ "s", @user.uid]
- }
-
- ret = YastService.Call("YaPI::USERS::UserModify", config, data)
-
- logger.debug "Command returns: #{ret.inspect}"
-
- @error_string = ret
- return (ret == "")
- end
-
- # -------------------------------------------------------
- # add the new local user
- def add_user
-
- # FIXME mandatory parameters must be required on web-client side...
- config = {
- "type" => [ "s", "local" ]
- }
- data = {
- "uid" => [ "s", @user.uid]
- }
- # FIXME convert ruby's under_scored keys to YaST's camel-cased ones
- data["cn"] = [ "s", @user.cn ] unless
@user.cn.blank?
- data["userPassword"] = [ "s", @user.user_password ] unless
@user.user_password.blank?
- data["homeDirectory"] = [ "s", @user.home_directory ] unless
@user.home_directory.blank?
- data["loginShell"] = [ "s", @user.login_shell ] unless
@user.login_shell.blank?
- data["uidNumber"] = [ "s", @user.uid_number ] unless
@user.uid_number.blank?
- data["groupname"] = [ "s", @user.groupname ] unless
@user.groupname.blank?
-
- ret = YastService.Call("YaPI::USERS::UserAdd", config, data)
-
- logger.debug "Command returns: #{ret.inspect}"
-
- @error_string = ret
- return (ret == "")
- end
-
- # -------------------------------------------------------
- # delete existing local user
- def delete_user
-
- config = {
- "type" => [ "s", "local" ],
- "uid" => [ "s", @user.uid ]
- }
-
- ret = YastService.Call("YaPI::USERS::UserDelete", config)
- logger.debug "Command returns: #{ret}"
-
- @error_string = ret
- return (ret == "")
- end
-
-#--------------------------------------------------------------------------------
-#
-# actions
-#
-#--------------------------------------------------------------------------------

# GET /users
# GET /users.xml
@@ -142,7 +16,7 @@ class UsersController < ApplicationController
unless permission_check("org.opensuse.yast.modules.yapi.users.usersget")
render ErrorResult.error(403, 1, "no permission") and return
end
- get_user_list
+ @users = User.find_all
end

# GET /users/1
@@ -154,9 +28,17 @@ class UsersController < ApplicationController
if params[:id].blank?
render ErrorResult.error(404, 2, "empty parameter") and return
end
- unless get_user params[:id]
- render ErrorResult.error(404, 2, "user not found") and return
+
+ begin
+ # try to find the user, and 404 if it does not exist
+ @user = User.find(params[:id])
+ if @user.nil?
+ render ErrorResult.error(404, 2, "user not found") and return
+ end
+ rescue Exception => e
+ render ErrorResult.error(500, 2, e.message) and return
end
+
end


@@ -168,15 +50,12 @@ class UsersController < ApplicationController
render ErrorResult.error(403, 1, "no permission") and return
end

- @user = User.new
- if @user.update_attributes(params[:users])
- add_user
- if @error_string != ""
- render ErrorResult.error(404, @error_id, @error_string) and return
- end
- else
- render ErrorResult.error(404, 2, "wrong parameter") and return
+ begin
+ @user = User.create(params[:users])
+ rescue Exception => e
+ render ErrorResult.error(404, @error_id, @error_string) and return
end
+
render :show
end

@@ -186,19 +65,23 @@ class UsersController < ApplicationController
unless permission_check("org.opensuse.yast.modules.yapi.users.usermodify")
render ErrorResult.error(403, 1, "no permission") and return
end
- @user = User.new
+
if params[:users] && params[:users][:uid]
params[:id] = params[:users][:uid] #for sync only
end
- get_user params[:id]
- if @user.update_attributes(params[:users])
- update_user params[:id]
- if @error_string != ""
- render ErrorResult.error(404, @error_id, @error_string) and return
+
+ begin
+ begin
+ @user = User.find(params[:id])
+ rescue Exception => e
+ render ErrorResult.error(404, 2, e.message) and return
end
- else
- render ErrorResult.error(404, 2, "wrong parameter") and return
- end
+ @user.load_attributes(params[:users])
+ @user.save
+ rescue Exception => e
+ # FIXME here should be internal error I guess
+ render ErrorResult.error(404, 2, e.message) and return
+ end
render :show
end

@@ -209,14 +92,14 @@ class UsersController < ApplicationController
unless permission_check("org.opensuse.yast.modules.yapi.users.userdelete")
render ErrorResult.error(403, 1, "no permission") and return
end
- get_user params[:id]
- delete_user
- logger.debug "DELETE: #{@user.inspect}"
- if @error_string != ""
- render ErrorResult.error(404, @error_id, @error_string) and return
- else
- render :show
+
+ begin
+ @user = User.find(params[:id])
+ @user.destroy
+ rescue Exception => e
+ render ErrorResult.error(404, @error_id, e.message) and return
end
+ render :show
end

end
diff --git a/plugins/users/app/models/user.rb b/plugins/users/app/models/user.rb
index ba13e5b..8a0396f 100644
--- a/plugins/users/app/models/user.rb
+++ b/plugins/users/app/models/user.rb
@@ -1,55 +1,159 @@

+require 'yast_service'
+
+# User model, not ActiveRecord based, but a
+# thin model over the YaPI, with some
+# ActiveRecord like convenience API
class User

- attr_accessor :cn,
- :uid,
- :uid_number,
- :gid_number,
- :grouplist,
- :groupname,
- :home_directory,
- :login_shell,
- :user_password,
- :addit_data,
- :type
+ attr_accessor_with_default :cn, ""
+ attr_accessor_with_default :uid, ""
+ attr_accessor_with_default :uid_number, ""
+ attr_accessor_with_default :gid_number, ""
+ attr_accessor_with_default :grouplist, {}
+ attr_accessor_with_default :groupname, ""
+ attr_accessor_with_default :home_directory, ""
+ attr_accessor_with_default :login_shell, ""
+ attr_accessor_with_default :user_password, ""
+ attr_accessor_with_default :type, "local"

- def id
- @uid
+ def initialize
+ end
+
+ # users = User.find_all
+ def self.find_all
+ users = []
+ parameters = {
+ # how to index hash with users
+ "index" => ["s", "uid"],
+ # attributes to return for each user
+ "user_attributes" => ["as", [ "cn" ]]
+ }
+ users_map = YastService.Call("YaPI::USERS::UsersGet", parameters)
+ if users_map.nil?
+ raise "Can't get user list"
+ else
+ users_map.each do |key, val|
+ user = User.new
+ user.uid = key
+ user.cn = val["cn"]
+ users << user
+ end
+ end
+ users
end

- def id=(id_val)
- @uid = id_val
+ # load the attributes of the user
+ def self.find(id)
+ user = User.new
+ parameters = {
+ # user to find
+ "uid" => [ "s", id ],
+ # list of attributes to return;
+ "user_attributes" =>
+ [ "as", [ "cn", "uidNumber", "homeDirectory",
+ "grouplist", "uid", "loginShell", "groupname" ] ]
+ }
+ user_map = YastService.Call("YaPI::USERS::UserGet", parameters)
+
+ raise "Got no data while loading user attributes" if user_map.empty?
+
+ load_data(user_map)
+ user.uid = id
+ end
+
+ # User.destroy("joe")
+ def self.destroy(uid)
+ # delete existing local user
+ config = {
+ "type" => [ "s", "local" ],
+ "uid" => [ "s", uid ]
+ }
+
+ ret = YastService.Call("YaPI::USERS::UserDelete", config)
+ Rails.logger "Command returns: #{ret}"
+ # @error_string = ret
+ return (ret == "")
+ end
+
+ # user.destroy
+ def destroy
+ self.class.destroy(uid)
+ end
+
+ def save
+ config = { "type" => [ "s", "local" ],
+ "uid" => [ "s", @user.uid ]
+ }
+ data = retrieve_data
+ ret = YastService.Call("YaPI::USERS::UserModify", config, data)
+
+ logger.debug "Command returns: #{ret.inspect}"
+ raise ret if not ret.blank?
+ true
end

- def initialize
- @cn = ""
- @uid = ""
- @uid_number = ""
- @gid_number = ""
- @grouplist = {}
- @groupname = ""
- @home_directory = ""
- @login_shell = ""
- @user_password = ""
- @type = "local"
+ # load a internally used data hash
+ # with camel-cased values
+ def load_data(data)
+ attrs = {}
+ data.each do |key, value|
+ attrs.store(key.underscore, value)
+ end
+ load_attribures(attrs)
end

- def update_attributes usr
- return false if usr==nil
- @grouplist = usr[:grouplist]
- @home_directory = usr[:home_directory]
- @type = usr[:type]
- @groupname = usr[:groupname]
- @login_shell = usr[:login_shell]
- @user_password = usr[:user_password]
- @uid = usr[:uid]
- @uid_number = usr[:uid_number]
- @gid_number = usr[:gid_number]
- @cn = usr[:cn]
-
- return true
+ # load a hash of attributes
+ def load_attributes(attrs)
+ return false if attrs.nil?
+ attrs.each do |key, value|
+ if self.respond_to?(key.to_sym)
+ self.send("#{key}=".to_sym, value)
+ end
+ end
+ true
end

+ # retrieves the internally used data
+ # hash with camel-cased values
+ def retrieve_data
+ data = { }
+ [ :cn, :uid, :uid_number, :gid_number, :grouplist, :groupname,
:home_directory, :login_shell, :user_password, :addit_data, :type ].each do
|attr_name|
+ if self.respond_to?(attr_name)
+ attr = self.send(attr_name)
+ data.store(attr_name.to_s.camelize(:lower), ['s', attr]) unless
attr.blank?
+ end
+ end
+ data
+ end
+
+ # create a user in the local system
+ def self.create(attrs)
+ config = {}
+ user = User.new
+ user.load_attributes(attrs)
+ data = user.retrieve_data
+
+ config.store("type", [ "s", "local" ])
+ data.store("uid", [ "s", user.uid])
+
+ ret = YastService.Call("YaPI::USERS::UserAdd", config, data)
+
+ Rails.logger.debug "Command returns: #{ret.inspect}"
+ # @error_string = ret
+ raise ret if not ret.blank?
+ user
+ end
+
+ def id
+ @uid
+ end
+
+ def id=(id_val)
+ @uid = id_val
+ end
+
+
def to_xml( options = {} )
xml = options[:builder] ||= Builder::XmlMarkup.new(options)
xml.instruct! unless options[:skip_instruct]
@@ -80,5 +184,4 @@ class User
return hash.to_json
end

-
end
diff --git a/plugins/users/test/functional/users_controller_test.rb
b/plugins/users/test/functional/users_controller_test.rb
index fa977bb..fdd7067 100644
--- a/plugins/users/test/functional/users_controller_test.rb
+++ b/plugins/users/test/functional/users_controller_test.rb
@@ -2,6 +2,7 @@ require File.expand_path(File.dirname(__FILE__) +
"/../test_helper")
require 'test/unit'
require 'rubygems'
require "scr"
+require "yast_service"
require 'mocha'


@@ -12,11 +13,11 @@ class UsersControllerTest < ActionController::TestCase
@request = ActionController::TestRequest.new
# http://railsforum.com/viewtopic.php?id=1719
@request.session[:account_id] = 1 # defined in fixtures
- Scr.any_instance.stubs(:initialize)
- Scr.any_instance.stubs(:execute).with(['/sbin/yast2', 'users',
'list']).returns({:stderr=>"schubi19 \nschubi2 \nschubi5 \ntuxtux \n",
:exit=>16, :stdout=>""})
- Scr.any_instance.stubs(:execute).with(['/sbin/yast2', 'users', 'show',
'username=schubi5']).returns({:stderr=>"Full Name:\n\tschubi5\nList of
Groups:\n\t\nDefault Group:\n\tusers\nHome Directory:\n\t/home/schubi5\nLogin
Shell:\n\t/bin/bash\nLogin Name:\n\tschubi5\nUID:\n\t1005\n", :exit=>16,
:stdout=>""})
+
+ User.stubs(:find_all).returns([])
end

+
test "access index" do
get :index
assert_response :success
@@ -37,12 +38,15 @@ class UsersControllerTest < ActionController::TestCase
end

test "access show" do
+ u = User.new
+ u.load_attributes({:uid => "schubi5"})
+ User.stubs(:find).with("schubi5").returns(u)
get :show, :id => "schubi5"
assert_response :success
end

test "access show with wrong user" do
- Scr.any_instance.stubs(:execute).with(['/sbin/yast2', 'users', 'show',
'username=schubi_not_found']).returns({:stderr=>"There is no such user.\n",
:exit=>0, :stdout=>""})
+ User.stubs(:find).with("schubi_not_found").returns(nil)
get :show, :id => "schubi_not_found"
assert_response 404
end
< Previous Next >