The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# $Id: user.t,v 1.1.1.1 2006/03/01 21:00:55 sommerb Exp $

=pod

=head1 NAME

user.t - Myco::Core::User tests

=cut

use strict;
use warnings;
use lib 'lib';
use lib '../myco_core_person/lib';
use Myco::Core::User;
use Myco::Core::Person;
use sigtrap qw(BUS SEGV);

use Test::More;
BEGIN { plan tests => 21 };

my $class = 'Myco::Core::User';

sub test_person {
  my $p = Myco::Core::Person->new( first => 'Jim-Bob', last => 'Lancelot',
				   prefix => 'Sir' );
#  $p->save;

  return $p;
};

sub test_user {
  my $u = Myco::Core::User->new( login => 'larry',
				 pass  => 'Knnnnnnnnn-igits!',
				 person => test_person() );
  return $u;
}

# Create closure that knows how to clean up the mess
sub test_cleanup {
  my $u_ = Myco->remote('Myco::Core::User');
  my $p_ = Myco->remote('Myco::Core::Person');
  my (@p) = Myco->select( $p_, $p_->{last} eq 'Lancelot' &
			       $p_->{first} eq 'Jim-Bob', );
  Myco->destroy(@p);

  my @u = Myco->select( $u_, $u_->{login} eq 'larry' );
  Myco->destroy(@u);

}


##############################################################################
# Now test the login with a user logged in.
sub test_login_access {
    my $test = shift;
    ok( my $u = &test_user, "Get $class object" );
    $u->add_roles('admin');

    ok($u->get_login eq 'larry', 'Login is "larry"');
    $u->set_login('czbsd');
    ok($u->get_login eq 'czbsd', 'Login czbsd');
}

##############################################################################
# Test password attribute.
sub test_pass {
    ok( my $u = &test_user,
                   "Get $class object" );
    ok($u->chk_pass('Knnnnnnnnn-igits!'),
                  'First password check');
    ok(! $u->chk_pass('bogus password'),
                  'Bogus password check');
    $u->set_pass('Aaaaaa....herring!');
    ok(! $u->chk_pass('Knnnnnnnnn-igits!'),
                  'New bogus password check');
    ok($u->chk_pass('Aaaaaa....herring!'),
                  'Successful new password check');
    eval { $u->pass };
    ok($@ && $@ =~ /^unknown method/, 'pass() dies');
    $@ = undef;
    eval { $u->get_pass };
    ok($@ && $@ =~ /^unknown method/, 'get_pass() dies');
}

##############################################################################
# Test password attribute with user logged in.
sub test_pass_access {
    ok( my $u = &test_user,
                   "Get $class object" );
    $u->add_roles('admin');

    ok($u->chk_pass('Knnnnnnnnn-igits!'),
		  'First password check');
    ok(! $u->chk_pass('bogus password'),
		  'Bogus password check');
    $u->set_pass('Aaaaaa....herring!');
    ok(! $u->chk_pass('Knnnnnnnnn-igits!'),
		  'New bogus password check');
    ok($u->chk_pass('Aaaaaa....herring!'),
		  'Successful new password check');
    eval { $u->pass };
    ok($@ && $@ =~ /^unknown method/, 'pass() dies');
    $@ = undef;
    eval { $u->get_pass };
    ok($@ && $@ =~ /^unknown method/, 'get_pass() dies');

}

##############################################################################
# Test person attribute with user logged in.
sub test_person_access {
    ok( my $u = &test_user,
                   "Get $class object" );

    $u->add_roles('admin');

    ok( my $p = $u->get_person, "Get person" );
    ok( UNIVERSAL::isa($p, 'Myco::Core::Person'),
		   "It's a person!" );
    ok( $p->get_prefix eq 'Sir',
                   "You are a knight of the round table?" );
}

##############################################################################
# Test access checking in Entity.pm. Start with set().

#
# DISABLED while ::Entity::set/get is disabled
#
sub _test_set_access {
    my $test = shift;
    return if $test->should_skip;    # skip over this test if asked

    # Okay, save the existing user class access roles so we can restore them
    # at the end of this method. The reason we're using our own here is so
    # that we can have fine control over testing both class-level and
    # attribute-level access roles.
    my $md = $class->introspect;
    my $apiroles = _save_class_roles($md);

    # Set up the user with some roles and make sure that he's the current
    # user.
    ok( my $u = &test_user,
                   "Get $class object" );
    $u->set_roles(qw(master));

    Myco::UI::Auth->_set_current_user($u);
    # (You didn't see that.)

    # Only the rw permissions are checked for get().
    $md->set_access_list( { rw => ['master'] });

    # Okay now, try to do something to $u. Because Lancelot is the current
    # user, and he's a member of the "master" role, and the Myco::Core::User class
    # allows that role to have read/write access, Lancelot should have no
    # problems.
    $u->set_login('foober');
    ok($u->get_login eq 'foober', "Cool, the access was granted!");

    # Okay, now deny access. Start by removing the role from Lancelot and giving
    # him another role. set_roles() will replace existing roles.
    $u->set_roles('servant');
    # Now just eval an access.
    eval { $u->set_login('arrgghh') };
    # Yes, Test::Unit really *is* that retarded.
    my $err = $@ && $@ =~ /You do not have permission to edit/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($err, "Deeeeeenied!" );

    # Now, let's try attribute permissions.
    my $la = $md->get_attributes->{login};
    # Sanity checks.
    ok( UNIVERSAL::isa($la, "Myco::Base::Entity::Meta::Attribute"),
                   "Yes, it's an attribute");
    ok( $la->get_name eq 'login', "Yes, it's the login attribute" );

    # Eliminate the class-level access list.
    $md->set_access_list({});
    # Only the rw permissions are checked for get().
    $la->set_access_list( { rw => ['master'] });

    # Return the user to that role.
    Myco::UI::Auth->_set_current_user(undef);
    $u->add_roles('master');
    Myco::UI::Auth->_set_current_user($u);
    # (More stuff you didn't see.)

    # Test it!
    $u->set_login('foober');
    ok($u->get_login eq 'foober',
                  "Cool, the attr access was granted!");

    # Now deny them access. Remove them from the master role. Give 'em another
    # role just for the hell of it.
    $u->set_roles('servant');
    eval { $u->set_login('arrgghh') };
    $err = $@ && $@ =~ /You do not have permission to edit the/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($err, "Deeeeeenied attr!" );

    # Clean up our mess.
    Myco::UI::Auth->_set_current_user(undef);
    # (You didn't see that, either.)

    # Restore roles assigned by class.
    _restore_class_roles($md, $apiroles);
}

##############################################################################
# Now test get() access checking in Entity.pm.

#
# DISABLED while ::Entity::set/get is disabled
#
sub _test_get_access {
    my $test = shift;
    return if $test->should_skip;    # skip over this test if asked

    # Okay, save the existing user class access roles so we can restore them
    # at the end of this method. The reason we're using our own here is so
    # that we can have fine control over testing both class-level and
    # attribute-level access roles.
    my $md = $class->introspect;
    my $apiroles = _save_class_roles($md);

    # Set up the user with some roles and make sure that he's the current
    # user.
    ok( my $u = &test_user,
                   "Get $class object" );
    $u->set_roles(qw(master));
    Myco::UI::Auth->_set_current_user($u);
    # (You didn't see that.)

    # Only the rw permissions are checked for get().
    $md->set_access_list( { ro => ['master'] });

    # Okay now, try to do something to $u. Because Lancelot is the current
    # user, and he's a member of the "master" role, and the Myco::Core::User class
    # allows that role to have read/write access, Lancelot should have no
    # problems.
    ok($u->get_login eq 'larry', "Cool, the access was granted!");

    # Access to call set() should be denied, though.
    eval { $u->set_login('foober') };
    my $err = $@ && $@ =~ /You do not have permission to edit/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($err, "Deeeeeenied set()!" );

    # Okay, now deny access. Start by removing the role from Lancelot and
    # giving him another role. set_roles() will replace existing roles.
    Myco::UI::Auth->_set_current_user(undef);
    $u->set_roles('servant');
    Myco::UI::Auth->_set_current_user($u);
    # More stuff you didn't see!

    # Now just eval an access.
    eval { $u->get_login };
    $err = $@ && $@ =~ /You do not have permission to read/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($err, "Deeeeeenied get()!" );

    # Setting still shouldn't be possible.
    eval { $u->set_login('arrgghh') };
    $err = $@ && $@ =~ /You do not have permission to edit/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($@, "Deeeeeenied set() again!" );

    # Now, let's try attribute permissions.
    my $la = $md->get_attributes->{login};
    # Sanity checks.
    ok( UNIVERSAL::isa($la, "Myco::Base::Entity::Meta::Attribute"),
                   "Yes, it's an attribute");
    ok( $la->get_name eq 'login', "Yes, it's the login attribute" );

    # Fake out an attribute role.
    $md->set_access_list({});
    my $attr_old_roles = $la->get_access_list;
    # Only the rw permissions are checked for get().
    $la->set_access_list( { ro => ['master'] });

    # Return the user to that role.
    Myco::UI::Auth->_set_current_user(undef);
    $u->add_roles('master');
    Myco::UI::Auth->_set_current_user($u);
    # (More stuff you didn't see.)

    # Test it
    ok($u->get_login eq 'larry',
                  "Cool, the access was granted again!");

    # Setting still shouldn't be possible.
    eval { $u->set_login('arrgghh') };
    $err = $@ && $@ =~ /You do not have permission to edit the/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($err, "Deeeeeenied set attr!" );

    # Now deny them access. Remove them from the master role. Give 'em another
    # role just for the hell of it.
    $u->set_roles('servant');
    eval { $u->get_login };
    $err = $@ && $@ =~ /You do not have permission to read the/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($err, "Deeeeeenied get() again!" );

    # Setting *still* shouldn't be possible.
    eval { $u->set_login('arrgghh') };
    $err = $@ && $@ =~ /You do not have permission to edit the/;
    _restore_class_roles($md, $apiroles) unless $err; # Rollback!
    ok($err, "Deeeeeenied get() again!" );

    # Clean up our mess.
    Myco::UI::Auth->_set_current_user(undef);
    # (You didn't see that, either.)

    # Restore roles assigned by class.
    _restore_class_roles($md, $apiroles);
}

##############################################################################
# Persistent Tests.
##############################################################################
sub test_auth {
    # First, save the user.
    ok( my $u = &test_user,
                   "Get $class object" );
    ok( my $id = $u->save, "Save user" );

    # update person obj so its user's ref in 'stuff' is saved
    my $p = $u->get_person;
    $p->save;

    # Now remove the user from memory.
    Myco->unload($u, $p);
    ok(! Myco->is_transient($id), "User not transient" );
    undef $u;
    undef $p;

    # Now instantiate the user by looking it up by login.
    my $u_r = Myco->remote('Myco::Core::User');
    ($u) = Myco->select($u_r, $u_r->{login} eq 'larry');

    # Check the password.
    ok( $u->chk_pass('Knnnnnnnnn-igits!'), "Validate password." );

    # Scheduled deletion via erasure of Person owning this User obj
    test_cleanup();
}

##############################################################################
# Test user roles.
sub test_roles {
    ok( my $u = &test_user,
                   "Get $class object" );
    $u->set_roles( qw(master));
    $u->add_roles( qw(servant admin));
    my $roles = $u->get_roles;
    ok( ref $roles eq 'ARRAY', "Roles are an array" );
    ok( $#$roles == 2, "Correct number of roles" );
    ok( $roles->[0] eq 'admin', "Admin is present" );
    ok( $roles->[1] eq 'master', "Master is present" );
    ok( $roles->[2] eq 'servant', "Servant is present" );

    ok( my $id = $u->save, "Save user" );

    # update person obj so its user's ref in 'stuff' is saved
    my $p = $u->get_person;
    $p->save;

    # Now remove the user from memory.
    Myco->unload($u, $p);
    ok(! Myco->is_transient($id), "User not transient" );
    undef $u;
    undef $p;
    undef $roles;

    # Now instantiate the user by looking it up by login.
    my $u_r = Myco->remote('Myco::Core::User');
    ($u) = Myco->select($u_r, $u_r->{login} eq 'larry');

    # Check the roles.
    $roles = $u->get_roles;
    ok( ref $roles eq 'ARRAY', "Roles are still an array" );
    ok( $#$roles == 2, "Correct number of roles after save" );
    ok( $roles->[0] eq 'admin', "Admin is present after save" );
    ok( $roles->[1] eq 'master', "Master is present after save" );
    ok( $roles->[2] eq 'servant', "Servant is present after save" );

    # Try deleting a role, etc.
    $u->del_roles('admin');
    $roles = $u->get_roles;
    ok( $#$roles == 1, "Correct number of roles after delete" );
    ok( $roles->[0] eq 'master', "Master is present after delete" );
    ok( $roles->[1] eq 'servant', "Servant is present after delete" );

    # Save it again and make sure we get it all back again!
    $u->save;
    ($u) = Myco->select($u_r, $u_r->{login} eq 'larry');
    $roles = $u->get_roles;
    ok( $#$roles == 1,
                   "Correct number of roles after delete and save" );
    ok( $roles->[0] eq 'master',
                   "Master is present after delete and save" );
    ok( $roles->[1] eq 'servant',
                   "Servant is present after delete and save" );

    # And finally, get them as objects.
    $u->set_roles(qw(master));
    my ($role) = $u->get_role_objs;
    ok( $role->get_name eq 'master', "Name is 'master'" );
    ok( $role->get_disp_name eq 'Master',
                   "Display Name is 'Master'" );

    # Scheduled deletion via erasure of Person owning this User obj
    test_cleanup();
}

##############################################################################
# Trying to get this sucker to segfault.
sub test_segfault {
    # First, save the user.
    ok( my $u = &test_user,
                   "Get $class object" );
    my $p = $u->get_person;
    $p->set_middle('segfault');

    # we're expecting $p to be in DB
    ok( $p->is_transient, '$p in db' );
    ok( my $id = $u->save, 'Save user' );
    ok( $id, "reasonable obj id" );

    # Update state
    $p->save;

    # Now remove the user from memory.
    Myco->unload($u, $p);
    ok(! Myco->is_transient($id), "Remove user from memory" );
    undef $u;
    undef $p;

    # Now instantiate the user by looking it up by login.
    my $u_r = Myco->remote('Myco::Core::User');
    ($u) = Myco->select($u_r, $u_r->{login} eq 'larry');
    # Make sure that we've loaded the person, too.
    ok($u->get_person->get_middle eq 'segfault',
                  "Check middle name" );

    # Clean up our mess.
    test_cleanup();
}

##############################################################################
# Utility methods.
##############################################################################

##############################################################################
sub _save_class_roles {
    my $md = shift; #$class->introspect;
    my %apiroles = ( "User Roles" => $md->get_access_list );
    my $attrs = $md->get_attributes;
    while (my ($k, $v) = each %$attrs) {
        $apiroles{$k} = $v->get_access_list;
        $v->set_access_list({});
    }
    return \%apiroles;
}

##############################################################################
sub _restore_class_roles {
    my ($md, $apiroles) = @_;
    $md->set_access_list($apiroles->{"User Roles"});
    my $attrs = $md->get_attributes;
    while (my ($k, $v) = each %$attrs) {
        $v->set_access_list($apiroles->{$k});
    }
}

#
#
#  run the tests!!!!!
#
#

&test_login_access;
&test_pass;
&test_pass_access;
&test_person_access;

# we don't want to test persistence for CPAN distribution
#&test_auth;
#&test_roles;
#&test_segfault;


1;
__END__

=back

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2006 the myco project. All rights reserved.
This software is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 AUTHORS

David Wheeler <david@wheeler.net> and Ben Sommer <ben@mycohq.com>

=head1 SEE ALSO

L<Myco::Core::User|Myco::Core::User>

=cut