Revision history for Catalyst-Plugin-Authentication-Store-DBIx-Class
+0.10 2007-07-07 3pm CST
+ Proper handling of missing id_field config (load from primary_key)
+ Throw exception if id_field specified does not exist
+ Full test suite added. (based loosely on old DBIC store)
+
0.03 XXX
Switch to Module::Install
for Catalyst Authentication using DBIx::Class
VERSION
- This documentation refers to version 0.02.
+ This documentation refers to version 0.10.
SYNOPSIS
use Catalyst qw/
};
# Log a user in:
-
- sub login : Global {
+
+ sub login : Global {
my ( $self, $c ) = @_;
-
- $c->authenticate({
+
+ $c->authenticate({
username => $c->req->params->username,
password => $c->req->params->password,
status => [ 'registered', 'loggedin', 'active']
}))
}
-
- # verify a role
-
- if ( $c->check_user_roles( 'editor' ) ) {
+
+ # verify a role
+
+ if ( $c->check_user_roles( 'editor' ) ) {
# do editor stuff
}
-
+
DESCRIPTION
The Catalyst::Plugin::Authentication::Store::DBIx::Class class provides
access to authentication information stored in a database via
my $rs = $c->model('MyApp::User')->search({ email => $c->request->params->{'email'} });
... # further $rs adjustments
-
- if ($c->authenticate({
+
+ if ($c->authenticate({
password => $password,
'dbix_class' => { resultset = $rs }
})) {
# do successful authentication actions here.
- }
+ }
Be aware that the resultset method will not verify that you are
passing a resultset that is attached to the same user_class as
use warnings;
use base qw/Class::Accessor::Fast/;
+<<<<<<< .mine
+our $VERSION= "0.10";
+=======
our $VERSION= "0.03";
+>>>>>>> .r6516
BEGIN {
__PACKAGE__->mk_accessors(qw/config/);
=head1 VERSION
-This documentation refers to version 0.02.
+This documentation refers to version 0.10.
=head1 SYNOPSIS
=item Searchargs
The B<searchargs> method of retrieval allows you to specify an arrayref containing
-the two arguments to the search() method from L<DBIx::Class::Resultset>. If provided,
+the two arguments to the search() method from L<DBIx::Class::ResultSet>. If provided,
all other args are ignored, and the search args provided are used directly to locate
the user. An example will probably make more sense:
if (!exists($self->config->{'id_field'})) {
- $self->config->{'id_field'} = 'id';
+ my @pks = $self->{'resultset'}->result_source->primary_columns;
+ if ($#pks == 0) {
+ $self->config->{'id_field'} = $pks[0];
+ } else {
+ Catalyst::Exception->throw("user table does not contain a single primary key column - please specify 'id_field' in config!");
+ }
+ }
+ if (!$self->{'resultset'}->result_source->has_column($self->config->{'id_field'})) {
+ Catalyst::Exception->throw("id_field set to " . $self->config->{'id_field'} . " but user table has no column by that name!");
}
## if we have lazyloading turned on - we should not query the DB unless something gets read.
my @roles = ();
if (exists($self->config->{'role_column'})) {
- @roles = split /[ ,\|]+/, $self->get($self->config->{'role_column'});
+ my $role_data = $self->get($self->config->{'role_column'});
+ if ($role_data) {
+ @roles = split /[ ,\|]+/, $self->get($self->config->{'role_column'});
+ }
$self->_roles(\@roles);
} elsif (exists($self->config->{'role_relation'})) {
my $relation = $self->config->{'role_relation'};
=head1 VERSION
-This documentation refers to version 0.02.
+This documentation refers to version 0.10.
=head1 SYNOPSIS
--- /dev/null
+#!perl\r
+\r
+use strict;\r
+use warnings;\r
+use DBI;\r
+use File::Path;\r
+use FindBin;\r
+use Test::More;\r
+use lib "$FindBin::Bin/lib";\r
+\r
+BEGIN {\r
+ eval { require DBD::SQLite }\r
+ or plan skip_all =>\r
+ "DBD::SQLite is required for this test";\r
+\r
+ eval { require DBIx::Class }\r
+ or plan skip_all =>\r
+ "DBIx::Class is required for this test";\r
+\r
+ plan tests => 14;\r
+\r
+ $ENV{TESTAPP_DB_FILE} = "$FindBin::Bin/auth.db" unless exists($ENV{TESTAPP_DB_FILE});\r
+\r
+ $ENV{TESTAPP_CONFIG} = {\r
+ name => 'TestApp',\r
+ authentication => {\r
+ default_realm => "users",\r
+ realms => {\r
+ users => {\r
+ credential => {\r
+ 'class' => "Password",\r
+ 'password_field' => 'password',\r
+ 'password_type' => 'clear'\r
+ },\r
+ store => {\r
+ 'class' => 'DBIx::Class',\r
+ 'user_class' => 'TestApp::User',\r
+ },\r
+ },\r
+ },\r
+ },\r
+ };\r
+\r
+ $ENV{TESTAPP_PLUGINS} = [\r
+ qw/Authentication/\r
+ ];\r
+}\r
+\r
+use SetupDB;\r
+\r
+use Catalyst::Test 'TestApp';\r
+\r
+# log a user in\r
+{\r
+ ok( my $res = request('http://localhost/user_login?username=joeuser&password=hackme'), 'request ok' );\r
+ is( $res->content, 'joeuser logged in', 'user logged in ok' );\r
+}\r
+\r
+# invalid user\r
+{\r
+ ok( my $res = request('http://localhost/user_login?username=foo&password=bar'), 'request ok' );\r
+ is( $res->content, 'not logged in', 'user not logged in ok' );\r
+}\r
+\r
+# disabled user - no disable check\r
+{\r
+ ok( my $res = request('http://localhost/user_login?username=spammer&password=broken'), 'request ok' );\r
+ is( $res->content, 'spammer logged in', 'status check - disabled user logged in ok' );\r
+}\r
+\r
+# disabled user - should fail login\r
+{\r
+ ok( my $res = request('http://localhost/notdisabled_login?username=spammer&password=broken'), 'request ok' );\r
+ is( $res->content, 'not logged in', 'status check - disabled user not logged in ok' );\r
+}\r
+\r
+# log the user out\r
+{\r
+ ok( my $res = request('http://localhost/user_logout'), 'request ok' );\r
+ is( $res->content, 'logged out', 'user logged out ok' );\r
+}\r
+\r
+# searchargs test\r
+{\r
+ ok( my $res = request('http://localhost/searchargs_login?email=nada%40mucho.net&password=much'), 'request ok' );\r
+ is( $res->content, 'nuffin logged in', 'searchargs based login ok' );\r
+}\r
+\r
+# resultset test\r
+# searchargs test\r
+{\r
+ ok( my $res = request('http://localhost/resultset_login?email=j%40cpants.org&password=letmein'), 'request ok' );\r
+ is( $res->content, 'jayk logged in', 'resultset based login ok' );\r
+}\r
+\r
+\r
+# clean up\r
+unlink $ENV{TESTAPP_DB_FILE};\r
--- /dev/null
+#!perl
+
+use strict;
+use warnings;
+use DBI;
+use File::Path;
+use FindBin;
+use Test::More;
+use lib "$FindBin::Bin/lib";
+
+BEGIN {
+ eval { require Test::WWW::Mechanize::Catalyst }
+ or plan skip_all =>
+ "Test::WWW::Mechanize::Catalyst is required for this test";
+
+ eval { require DBD::SQLite }
+ or plan skip_all =>
+ "DBD::SQLite is required for this test";
+
+ eval { require DBIx::Class }
+ or plan skip_all =>
+ "DBIx::Class is required for this test";
+
+ eval { require Catalyst::Plugin::Session;
+ die unless $Catalyst::Plugin::Session::VERSION >= 0.02 }
+ or plan skip_all =>
+ "Catalyst::Plugin::Session >= 0.02 is required for this test";
+
+ plan tests => 8;
+
+ $ENV{TESTAPP_DB_FILE} = "$FindBin::Bin/auth.db" unless exists($ENV{TESTAPP_DB_FILE});
+
+ $ENV{TESTAPP_CONFIG} = {
+ name => 'TestApp',
+ authentication => {
+ default_realm => "users",
+ realms => {
+ users => {
+ credential => {
+ 'class' => "Password",
+ 'password_field' => 'password',
+ 'password_type' => 'clear'
+ },
+ store => {
+ 'class' => 'DBIx::Class',
+ 'user_class' => 'TestApp::User',
+ },
+ },
+ },
+ },
+ };
+
+ $ENV{TESTAPP_PLUGINS} = [
+ qw/Authentication
+ Session
+ Session::Store::Dummy
+ Session::State::Cookie
+ /
+ ];
+}
+
+use SetupDB;
+
+use Test::WWW::Mechanize::Catalyst 'TestApp';
+my $m = Test::WWW::Mechanize::Catalyst->new;
+
+# log a user in
+{
+ $m->get_ok( 'http://localhost/user_login?username=joeuser&password=hackme', undef, 'request ok' );
+ $m->content_is( 'joeuser logged in', 'user logged in ok' );
+}
+
+# verify the user is still logged in
+{
+ $m->get_ok( 'http://localhost/get_session_user', undef, 'request ok' );
+ $m->content_is( 'joeuser', 'user still logged in' );
+}
+
+# log the user out
+{
+ $m->get_ok( 'http://localhost/user_logout', undef, 'request ok' );
+ $m->content_is( 'logged out', 'user logged out ok' );
+}
+
+# verify there is no session
+{
+ $m->get_ok( 'http://localhost/get_session_user', undef, 'request ok' );
+ $m->content_is( '', "user's session deleted" );
+}
+
+# clean up
+unlink $ENV{TESTAPP_DB_FILE};
--- /dev/null
+#!perl
+
+use strict;
+use warnings;
+use DBI;
+use File::Path;
+use FindBin;
+use Test::More;
+use lib "$FindBin::Bin/lib";
+
+BEGIN {
+ eval { require DBD::SQLite }
+ or plan skip_all =>
+ "DBD::SQLite is required for this test";
+
+ eval { require DBIx::Class }
+ or plan skip_all =>
+ "DBIx::Class is required for this test";
+
+ eval { require Catalyst::Plugin::Authorization::Roles }
+ or plan skip_all =>
+ "Catalyst::Plugin::Authorization::Roles is required for this test";
+
+ plan tests => 8;
+
+ $ENV{TESTAPP_DB_FILE} = "$FindBin::Bin/auth.db" unless exists($ENV{TESTAPP_DB_FILE});
+
+
+ $ENV{TESTAPP_CONFIG} = {
+ name => 'TestApp',
+ authentication => {
+ default_realm => "users",
+ realms => {
+ users => {
+ credential => {
+ 'class' => "Password",
+ 'password_field' => 'password',
+ 'password_type' => 'clear'
+ },
+ store => {
+ 'class' => 'DBIx::Class',
+ 'user_class' => 'TestApp::User',
+ 'role_relation' => 'roles',
+ 'role_field' => 'role'
+ },
+ },
+ },
+ },
+ };
+
+ $ENV{TESTAPP_PLUGINS} = [
+ qw/Authentication
+ Authorization::Roles
+ /
+ ];
+}
+
+use SetupDB;
+
+use Catalyst::Test 'TestApp';
+
+# test user's admin access
+{
+ ok( my $res = request('http://localhost/user_login?username=jayk&password=letmein&detach=is_admin'), 'request ok' );
+ is( $res->content, 'ok', 'user is an admin' );
+}
+
+# test unauthorized user's admin access
+{
+ ok( my $res = request('http://localhost/user_login?username=nuffin&password=much&detach=is_admin'), 'request ok' );
+ is( $res->content, 'failed', 'user is not an admin' );
+}
+
+# test multiple auth roles
+{
+ ok( my $res = request('http://localhost/user_login?username=jayk&password=letmein&detach=is_admin_user'), 'request ok' );
+ is( $res->content, 'ok', 'user is an admin and a user' );
+}
+
+# test multiple unauth roles
+{
+ ok( my $res = request('http://localhost/user_login?username=nuffin&password=much&detach=is_admin_user'), 'request ok' );
+ is( $res->content, 'failed', 'user is not an admin and a user' );
+}
+
+# clean up
+unlink $ENV{TESTAPP_DB_FILE};
--- /dev/null
+#!perl
+
+use strict;
+use warnings;
+use DBI;
+use File::Path;
+use FindBin;
+use Test::More;
+use lib "$FindBin::Bin/lib";
+
+BEGIN {
+ eval { require DBD::SQLite }
+ or plan skip_all =>
+ "DBD::SQLite is required for this test";
+
+ eval { require DBIx::Class }
+ or plan skip_all =>
+ "DBIx::Class is required for this test";
+
+ eval { require Catalyst::Plugin::Authorization::Roles }
+ or plan skip_all =>
+ "Catalyst::Plugin::Authorization::Roles is required for this test";
+
+ plan tests => 8;
+
+ $ENV{TESTAPP_DB_FILE} = "$FindBin::Bin/auth.db" unless exists($ENV{TESTAPP_DB_FILE});
+
+
+ $ENV{TESTAPP_CONFIG} = {
+ name => 'TestApp',
+ authentication => {
+ default_realm => "users",
+ realms => {
+ users => {
+ credential => {
+ 'class' => "Password",
+ 'password_field' => 'password',
+ 'password_type' => 'clear'
+ },
+ store => {
+ 'class' => 'DBIx::Class',
+ 'user_class' => 'TestApp::User',
+ 'role_column' => 'role_text'
+ },
+ },
+ },
+ },
+ };
+
+ $ENV{TESTAPP_PLUGINS} = [
+ qw/Authentication
+ Authorization::Roles
+ /
+ ];
+}
+
+use SetupDB;
+
+use Catalyst::Test 'TestApp';
+
+# test user's admin access
+{
+ ok( my $res = request('http://localhost/user_login?username=joeuser&password=hackme&detach=is_admin'), 'request ok' );
+ is( $res->content, 'ok', 'user is an admin' );
+}
+
+# test unauthorized user's admin access
+{
+ ok( my $res = request('http://localhost/user_login?username=jayk&password=letmein&detach=is_admin'), 'request ok' );
+ is( $res->content, 'failed', 'user is not an admin' );
+}
+
+# test multiple auth roles
+{
+ ok( my $res = request('http://localhost/user_login?username=nuffin&password=much&detach=is_admin_user'), 'request ok' );
+ is( $res->content, 'ok', 'user is an admin and a user' );
+}
+
+# test multiple unauth roles
+{
+ ok( my $res = request('http://localhost/user_login?username=joeuser&password=hackme&detach=is_admin_user'), 'request ok' );
+ is( $res->content, 'failed', 'user is not an admin and a user' );
+}
+
+# clean up
+unlink $ENV{TESTAPP_DB_FILE};
+++ /dev/null
-#!perl -T
-
-use strict;
-use warnings;
-use Test::More tests => 3;
-
-sub not_in_file_ok {
- my ($filename, %regex) = @_;
- open my $fh, "<", $filename
- or die "couldn't open $filename for reading: $!";
-
- my %violated;
-
- while (my $line = <$fh>) {
- while (my ($desc, $regex) = each %regex) {
- if ($line =~ $regex) {
- push @{$violated{$desc}||=[]}, $.;
- }
- }
- }
-
- if (%violated) {
- fail("$filename contains boilerplate text");
- diag "$_ appears on lines @{$violated{$_}}" for keys %violated;
- } else {
- pass("$filename contains no boilerplate text");
- }
-}
-
-not_in_file_ok(README =>
- "The README is used..." => qr/The README is used/,
- "'version information here'" => qr/to provide version information/,
-);
-
-not_in_file_ok(Changes =>
- "placeholder date/time" => qr(Date/time)
-);
-
-sub module_boilerplate_ok {
- my ($module) = @_;
- not_in_file_ok($module =>
- 'the great new $MODULENAME' => qr/ - The great new /,
- 'boilerplate description' => qr/Quick summary of what the module/,
- 'stub function definition' => qr/function[12]/,
- );
-}
-
-module_boilerplate_ok('lib/Catalyst/Plugin/Authentication/Store/DBIx/Class.pm');
--- /dev/null
+# create the database
+my $db_file = $ENV{TESTAPP_DB_FILE};
+unlink $db_file if -e $db_file;
+
+my $dbh = DBI->connect( "dbi:SQLite:$db_file" ) or die $DBI::errstr;
+my $sql = q{
+ CREATE TABLE user (
+ id INTEGER PRIMARY KEY,
+ username TEXT,
+ email TEXT,
+ password TEXT,
+ status TEXT,
+ role_text TEXT,
+ session_data TEXT
+ );
+ CREATE TABLE role (
+ id INTEGER PRIMARY KEY,
+ role TEXT
+ );
+ CREATE TABLE user_role (
+ id INTEGER PRIMARY KEY,
+ user INTEGER,
+ roleid INTEGER
+ );
+
+ INSERT INTO user VALUES (1, 'joeuser', 'joeuser@nowhere.com', 'hackme', 'active', 'admin', NULL);
+ INSERT INTO user VALUES (2, 'spammer', 'bob@spamhaus.com', 'broken', 'disabled', NULL, NULL);
+ INSERT INTO user VALUES (3, 'jayk', 'j@cpants.org', 'letmein', 'active', NULL, NULL);
+ INSERT INTO user VALUES (4, 'nuffin', 'nada@mucho.net', 'much', 'registered', 'user admin', NULL);
+ INSERT INTO role VALUES (1, 'admin');
+ INSERT INTO role VALUES (2, 'user');
+ INSERT INTO user_role VALUES (1, 3, 1);
+ INSERT INTO user_role VALUES (2, 3, 2);
+ INSERT INTO user_role VALUES (3, 4, 2)
+};
+$dbh->do( $_ ) for split /;/, $sql;
+$dbh->disconnect;
\ No newline at end of file
--- /dev/null
+package TestApp;\r
+\r
+use strict;\r
+use Catalyst;\r
+use Data::Dumper;\r
+\r
+TestApp->config( $ENV{TESTAPP_CONFIG} );\r
+\r
+TestApp->setup( @{$ENV{TESTAPP_PLUGINS}} );\r
+\r
+sub user_login : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ ## this allows anyone to login regardless of status.\r
+ $c->authenticate({ username => $c->request->params->{'username'},\r
+ password => $c->request->params->{'password'}\r
+ });\r
+\r
+ if ( $c->user_exists ) {\r
+ if ( $c->req->params->{detach} ) {\r
+ $c->detach( $c->req->params->{detach} );\r
+ }\r
+ $c->res->body( $c->user->get('username') . ' logged in' );\r
+ }\r
+ else {\r
+ $c->res->body( 'not logged in' );\r
+ }\r
+}\r
+\r
+sub notdisabled_login : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ $c->authenticate({ username => $c->request->params->{'username'},\r
+ password => $c->request->params->{'password'},\r
+ status => [ 'active', 'registered' ]\r
+ });\r
+\r
+ if ( $c->user_exists ) {\r
+ if ( $c->req->params->{detach} ) {\r
+ $c->detach( $c->req->params->{detach} );\r
+ }\r
+ $c->res->body( $c->user->get('username') . ' logged in' );\r
+ }\r
+ else {\r
+ $c->res->body( 'not logged in' );\r
+ }\r
+}\r
+\r
+sub searchargs_login : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ my $username = $c->request->params->{'username'} || '';\r
+ my $email = $c->request->params->{'email'} || '';\r
+ \r
+ $c->authenticate({ \r
+ password => $c->request->params->{'password'},\r
+ dbix_class => {\r
+ searchargs => [ { "-or" => [ username => $username,\r
+ email => $email ]}, \r
+ { prefetch => qw/ map_user_role /}\r
+ ]\r
+ }\r
+ });\r
+\r
+ if ( $c->user_exists ) {\r
+ if ( $c->req->params->{detach} ) {\r
+ $c->detach( $c->req->params->{detach} );\r
+ }\r
+ $c->res->body( $c->user->get('username') . ' logged in' );\r
+ }\r
+ else {\r
+ $c->res->body( 'not logged in' );\r
+ }\r
+}\r
+\r
+sub resultset_login : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ my $username = $c->request->params->{'username'} || '';\r
+ my $email = $c->request->params->{'email'} || '';\r
+ \r
+ \r
+ my $rs = $c->model('TestApp::User')->search({ "-or" => [ username => $username,\r
+ email => $email ]});\r
+ \r
+ $c->authenticate({ \r
+ password => $c->request->params->{'password'},\r
+ dbix_class => { resultset => $rs }\r
+ });\r
+ \r
+ if ( $c->user_exists ) {\r
+ if ( $c->req->params->{detach} ) {\r
+ $c->detach( $c->req->params->{detach} );\r
+ }\r
+ $c->res->body( $c->user->get('username') . ' logged in' );\r
+ }\r
+ else {\r
+ $c->res->body( 'not logged in' );\r
+ }\r
+}\r
+\r
+## need to add a resultset login test and a search args login test\r
+\r
+sub user_logout : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ $c->logout;\r
+\r
+ if ( ! $c->user ) {\r
+ $c->res->body( 'logged out' );\r
+ }\r
+ else {\r
+ $c->res->body( 'not logged ok' );\r
+ }\r
+}\r
+\r
+sub get_session_user : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ if ( $c->user_exists ) {\r
+ $c->res->body( $c->user->get('username') );\r
+ }\r
+}\r
+\r
+sub is_admin : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ eval {\r
+ if ( $c->assert_user_roles( qw/admin/ ) ) {\r
+ $c->res->body( 'ok' );\r
+ }\r
+ };\r
+ if ($@) {\r
+ $c->res->body( 'failed' );\r
+ }\r
+}\r
+\r
+sub is_admin_user : Global {\r
+ my ( $self, $c ) = @_;\r
+\r
+ eval {\r
+ if ( $c->assert_user_roles( qw/admin user/ ) ) {\r
+ $c->res->body( 'ok' );\r
+ }\r
+ };\r
+ if ($@) {\r
+ $c->res->body( 'failed' );\r
+ }\r
+}\r
+\r
+sub set_usersession : Global {\r
+ my ( $self, $c, $value ) = @_;\r
+ $c->user_session->{foo} = $value;\r
+ $c->res->body( 'ok' );\r
+}\r
+\r
+sub get_usersession : Global {\r
+ my ( $self, $c ) = @_;\r
+ $c->res->body( $c->user_session->{foo} || '' );\r
+}\r
+\r
+\r
+1;\r
--- /dev/null
+package TestApp::Model::TestApp;
+
+use base qw/Catalyst::Model::DBIC::Schema/;
+use strict;
+
+
+our $db_file = $ENV{TESTAPP_DB_FILE};
+
+__PACKAGE__->config(
+ schema_class => 'TestApp::Schema',
+ connect_info => [ "dbi:SQLite:$db_file",
+ '',
+ '',
+ { AutoCommit => 1 },
+ ],
+
+);
+
+# Load all of the classes
+#__PACKAGE__->load_classes(qw/Role User UserRole/);
+
+
+1;
--- /dev/null
+package TestApp::Schema;
+
+# Created by DBIx::Class::Schema::Loader v0.03007 @ 2006-10-18 12:38:33
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Schema';
+
+__PACKAGE__->load_classes;
+
+1;
\ No newline at end of file
--- /dev/null
+package TestApp::Schema::Role;
+
+use strict;
+use warnings;
+use base 'DBIx::Class';
+
+__PACKAGE__->load_components(qw/ Core /);
+
+__PACKAGE__->table( 'role' );
+__PACKAGE__->add_columns( qw/id role/ );
+__PACKAGE__->set_primary_key( 'id' );
+
+#__PACKAGE__->has_many( map_user_role => 'TestApp::Schema::UserRole' => 'roleid' );
+
+1;
--- /dev/null
+package TestApp::Schema::User;
+
+use strict;
+use warnings;
+use base 'DBIx::Class';
+
+__PACKAGE__->load_components(qw/ Core /);
+
+__PACKAGE__->table( 'user' );
+__PACKAGE__->add_columns( qw/id username email password status role_text session_data/ );
+__PACKAGE__->set_primary_key( 'id' );
+
+__PACKAGE__->has_many( 'map_user_role' => 'TestApp::Schema::UserRole' => 'user' );
+
+__PACKAGE__->many_to_many( roles => 'map_user_role', 'role');
+
+1;
--- /dev/null
+package TestApp::Schema::UserRole;
+
+use strict;
+use warnings;
+use base 'DBIx::Class';
+
+__PACKAGE__->load_components(qw/ Core /);
+
+__PACKAGE__->table( 'user_role' );
+__PACKAGE__->add_columns( qw/id user roleid/ );
+__PACKAGE__->set_primary_key( qw/id/ );
+
+__PACKAGE__->belongs_to('role', 'TestApp::Schema::Role', { 'foreign.id' => 'self.roleid'});
+
+1;