1package Dancer2::Plugin::Auth::Extensible::Test::App; 2 3=head1 NAME 4 5Dancer2::Plugin::Auth::Extensible::Test::App - Dancer2 app for testing providers 6 7=cut 8 9our $VERSION = '0.710'; 10 11use strict; 12use warnings; 13use Test::More; 14use Test::Deep qw(bag cmp_deeply); 15use Test::Fatal; 16use Dancer2 appname => 'TestApp'; 17use Dancer2::Plugin::Auth::Extensible; 18use Scalar::Util qw(blessed); 19use YAML (); 20 21set session => 'simple'; 22set logger => 'capture'; 23set log => 'debug'; 24set show_errors => 1; 25 26# nasty shared global makes it easy to pass data between app and test script 27our $data = {}; 28 29config->{plugins}->{"Auth::Extensible"}->{password_reset_send_email} = 30 __PACKAGE__ . "::email_send"; 31config->{plugins}->{"Auth::Extensible"}->{welcome_send} = 32 __PACKAGE__ . "::email_send"; 33 34sub email_send { 35 my ( $plugin, %args ) = @_; 36 $data = { %args, called => 1 }; 37} 38 39# we need the plugin object and a provider for provider tests 40my $plugin = app->with_plugin('Auth::Extensible'); 41my $provider = $plugin->auth_provider('config1'); 42 43my @provider_can = (); 44 45push @provider_can, 'record_lastlogin' if $plugin->config->{record_lastlogin}; 46 47config->{plugins}->{"Auth::Extensible"}->{reset_password_handler} = 1 48 if $provider->can('get_user_by_code'); 49 50# 51# IMPORTANT NOTE 52# 53# We use "isnt exception {...}, undef, ..." a lot which is REALLY BAD 54# practice. This should only ever be done in provider tests since we cannot 55# be sure what exception message a provider returns and we do NOT mandate 56# specific messages so we can only test if something died. 57# 58# When writing new provider tests please always test first with a "like /qr/" 59# instead and then once the tests are all working against one provider switch 60# them to the bad "isnt undef" style so they are portable. 61# 62 63subtest 'Provider authenticate_user tests' => sub { 64 my $ret; 65 push @provider_can, 'authenticate_user'; 66 67 isnt exception { $ret = $provider->authenticate_user(); }, 68 undef, 69 "authenticate_user with no args dies."; 70 71 isnt exception { $ret = $provider->authenticate_user(''); }, 72 undef, 73 "authenticate_user with empty username and no password dies."; 74 75 isnt exception { $ret = $provider->authenticate_user(undef, ''); }, 76 undef, 77 "authenticate_user with undef username and empty password dies."; 78 79 is exception { $ret = $provider->authenticate_user('', ''); }, 80 undef, 81 "authenticate_user with empty username and empty password lives."; 82 ok !$ret, "... and returns a false value."; 83 84 is exception { $ret = $provider->authenticate_user('unknown', 'beer'); }, 85 undef, 86 "authenticate_user with unknown user lives."; 87 ok !$ret, "... and returns a false value."; 88 89 is exception { $ret = $provider->authenticate_user('dave', 'notcorrect'); }, 90 undef, 91 "authenticate_user with known user and bad password lives."; 92 ok !$ret, "... and returns a false value."; 93 94 is exception { $ret = $provider->authenticate_user('dave', 'beer'); }, 95 undef, 96 "authenticate_user with known user and good password."; 97 ok $ret, "... and returns a true value."; 98}; 99 100SKIP: { 101 skip "Provider has no get_user_details method", 1 102 unless $provider->can('get_user_details'); 103 104 subtest 'Provider get_user_details tests' => sub { 105 my $ret; 106 107 push @provider_can, 'get_user_details'; 108 109 isnt exception { $ret = $provider->get_user_details(); }, 110 undef, 111 "get_user_details with no args dies."; 112 113 is exception { $ret = $provider->get_user_details(''); }, 114 undef, 115 "get_user_details with empty username lives."; 116 ok !$ret, "... and returns a false value."; 117 118 is exception { $ret = $provider->get_user_details('unknown'); }, 119 undef, 120 "get_user_details with unknown user lives."; 121 ok !$ret, "... and returns a false value."; 122 123 is exception { $ret = $provider->get_user_details('dave'); }, 124 undef, 125 "get_user_details with known user lives."; 126 ok $ret, "... and returns a true value"; 127 ok blessed($ret) || ref($ret) eq 'HASH', 128 "... which is either an object or a hash reference" 129 or diag explain $ret; 130 is blessed($ret) ? $ret->name : $ret->{name}, 'David Precious', 131 "... and user's name is David Precious."; 132 }; 133} 134 135SKIP: { 136 skip "Provider has no get_user_roles method", 1 137 unless $provider->can('get_user_roles'); 138 139 subtest 'Provider get_user_roles tests' => sub { 140 my $ret; 141 142 push @provider_can, 'get_user_roles'; 143 144 isnt exception { $ret = $provider->get_user_roles(); }, 145 undef, 146 "get_user_roles with no args dies."; 147 148 is exception { $ret = $provider->get_user_roles(''); }, undef, 149 "get_user_roles with empty username lives"; 150 ok !$ret, "... and returns false value."; 151 152 is exception { $ret = $provider->get_user_roles('unknown'); }, undef, 153 "get_user_roles with unknown user lives"; 154 ok !$ret, "... and returns false value."; 155 156 is exception { $ret = $provider->get_user_roles('dave'); }, undef, 157 "get_user_roles with known user \"dave\" lives"; 158 ok $ret, "... and returns true value"; 159 is ref($ret), 'ARRAY', "... which is an array reference"; 160 cmp_deeply $ret, bag( "BeerDrinker", "Motorcyclist" ), 161 "... and dave is a BeerDrinker and Motorcyclist."; 162 }; 163} 164 165SKIP: { 166 skip "Provider has no create_user method", 1 167 unless $provider->can('create_user'); 168 169 subtest 'Provider create_user tests' => sub { 170 my $ret; 171 172 push @provider_can, 'create_user'; 173 174 isnt exception { $ret = $provider->create_user(); }, 175 undef, 176 "create_user with no args dies."; 177 178 isnt exception { $ret = $provider->create_user(username => ''); }, 179 undef, 180 "create_user with empty username dies."; 181 182 isnt exception { $ret = $provider->create_user(username => 'dave'); }, 183 undef, 184 "create_user with existing username dies."; 185 186 is exception { 187 $ret = $provider->get_user_details('provider_create_user'); 188 }, 189 undef, 190 "get_user_details \"provider_create_user\" lives"; 191 ok !defined $ret, "... and does not return a user."; 192 193 is exception { 194 $ret = $provider->create_user( 195 username => 'provider_create_user', 196 name => 'Create User' 197 ); 198 }, 199 undef, 200 "create_user \"provider_create_user\" lives"; 201 202 ok defined $ret, "... and returns a user"; 203 is blessed($ret) ? $ret->name : $ret->{name}, "Create User", 204 "... and user's name is correct."; 205 206 is exception { 207 $ret = $provider->get_user_details('provider_create_user'); 208 }, 209 undef, 210 "get_user_details \"provider_create_user\" lives"; 211 ok defined $ret, "... and now *does* return a user."; 212 is blessed($ret) ? $ret->name : $ret->{name}, "Create User", 213 "... and user's name is correct."; 214 }; 215} 216 217SKIP: { 218 skip "Provider has no set_user_details method", 1 219 unless $provider->can('set_user_details'); 220 221 subtest 'Provider set_user_details tests' => sub { 222 my $ret; 223 224 push @provider_can, 'set_user_details'; 225 226 isnt exception { $ret = $provider->set_user_details(); }, 227 undef, 228 "set_user_details with no args dies."; 229 230 isnt exception { $ret = $provider->set_user_details(''); }, 231 undef, 232 "set_user_details with empty username dies."; 233 234 is exception { 235 $ret = $provider->create_user( 236 username => 'provider_set_user_details', 237 name => 'Initial Name' 238 ); 239 }, 240 undef, 241 "Create a user for testing lives"; 242 243 is exception { 244 $ret = $provider->get_user_details('provider_set_user_details') 245 }, 246 undef, 247 "... and get_user_details on new user lives"; 248 249 is blessed($ret) ? $ret->name : $ret->{name}, 'Initial Name', 250 "... and user has expected name."; 251 252 is exception { 253 $ret = $provider->set_user_details( 'provider_set_user_details', 254 name => 'New Name', ); 255 }, 256 undef, 257 "Using set_user_details to change user's name lives"; 258 259 is blessed($ret) ? $ret->name : $ret->{name}, 'New Name', 260 "... and returned user has expected name."; 261 262 is exception { 263 $ret = $provider->get_user_details('provider_set_user_details') 264 }, 265 undef, 266 "... and get_user_details on new user lives"; 267 268 is blessed($ret) ? $ret->name : $ret->{name}, 'New Name', 269 "... and returned user has expected name."; 270 }; 271} 272 273SKIP: { 274 skip "Provider has no get_user_by_code method", 1 275 unless $provider->can('get_user_by_code'); 276 277 subtest 'Provider get_user_by_code tests' => sub { 278 my $ret; 279 280 push @provider_can, 'get_user_by_code'; 281 282 isnt exception { $ret = $provider->get_user_by_code(); }, 283 undef, 284 "get_user_by_code with no args dies."; 285 286 isnt exception { $ret = $provider->get_user_by_code(''); }, 287 undef, 288 "get_user_by_code with empty code dies."; 289 290 is exception { $ret = $provider->get_user_by_code('nosuchcode'); }, 291 undef, 292 "get_user_by_code with non-existant code lives"; 293 ok !defined $ret, "... and returns undef."; 294 295 is exception { 296 $ret = $provider->create_user( 297 username => 'provider_get_user_by_code', 298 pw_reset_code => '01234567890get_user_by_code', 299 ); 300 }, 301 undef, 302 "Create a user for testing lives"; 303 304 is exception { 305 $ret = $provider->get_user_by_code('01234567890get_user_by_code'); 306 }, 307 undef, 308 "get_user_by_code with non-existant code lives"; 309 ok defined $ret, "... and returns something true"; 310 311 is $ret, 'provider_get_user_by_code', 312 "... and returned username is correct."; 313 }; 314} 315 316SKIP: { 317 skip "Provider has no set_user_password method", 1 318 unless $provider->can('set_user_password'); 319 320 subtest 'Provider set_user_password tests' => sub { 321 my $ret; 322 323 push @provider_can, 'set_user_password'; 324 325 isnt exception { $ret = $provider->set_user_password(); }, 326 undef, 327 "set_user_password with no args dies."; 328 329 isnt exception { $ret = $provider->set_user_password(''); }, 330 undef, 331 "set_user_password with username but undef password dies"; 332 333 isnt exception { $ret = $provider->set_user_password( undef, '' ); }, 334 undef, 335 "set_user_password with password but undef username dies"; 336 337 is exception { 338 $ret = 339 $provider->create_user( username => 'provider_set_user_password' ) 340 }, 341 undef, 342 "Create a user for testing lives"; 343 344 is exception { 345 $ret = $provider->set_user_password( 'provider_set_user_password', 346 'aNicePassword' ) 347 }, 348 undef, "set_user_password for our new user lives"; 349 350 is exception { 351 $ret = $provider->authenticate_user( 'provider_set_user_password', 352 'aNicePassword' ) 353 }, 354 undef, "... and authenticate_user with correct password lives"; 355 ok $ret, "... and authenticate_user passes (returns true)"; 356 357 is exception { 358 $ret = $provider->authenticate_user( 'provider_set_user_password', 359 'badpwd' ) 360 }, 361 undef, "... and whilst authenticate_user with bad password lives"; 362 ok !$ret, "... it returns false."; 363 }; 364} 365 366SKIP: { 367 skip "Provider has no password_expired method", 1 368 unless $provider->can('password_expired'); 369 370 subtest 'Provider password_expired tests' => sub { 371 my $ret; 372 373 push @provider_can, 'password_expired'; 374 375 isnt exception { $ret = $provider->password_expired(); }, 376 undef, 377 "password_expired with no args dies."; 378 379 is exception { 380 $ret = 381 $provider->create_user( username => 'provider_password_expired' ) 382 }, 383 undef, 384 "Create a user for testing lives"; 385 386 is exception { 387 $ret = $provider->password_expired($ret) 388 }, 389 undef, 390 "... and password_expired for user lives"; 391 392 ok $ret, "... and password is expired since it has never been set."; 393 394 is exception { 395 $ret = $provider->set_user_password( 'provider_password_expired', 396 'password' ) 397 }, 398 undef, 399 "Setting password for user lives"; 400 401 is exception { 402 $ret = $provider->password_expired($ret) 403 }, 404 undef, 405 "... and password_expired for user lives"; 406 407 ok !$ret, "... and password is now *not* expired."; 408 409 is exception { 410 $ret = $provider->set_user_details( 'provider_password_expired', 411 pw_changed => DateTime->now->subtract( weeks => 1 ) ) 412 }, 413 undef, 414 "Set pw_changed to one week ago lives and so now password is expired"; 415 416 is exception { 417 $ret = $provider->password_expired($ret) 418 }, 419 undef, 420 "... and password_expired for user lives"; 421 422 ok $ret, "... and password *is* now expired since expiry is 2 days."; 423 424 }; 425} 426 427subtest "Plugin coverage testing" => sub { 428 # DO NOT use this for testing things that can be tested elsewhere since 429 # these tests are purely to catch the code paths that we can't get to 430 # any other way. 431 432 like exception { $plugin->realm() }, qr/realm name not provided/, 433 "Calling realm method with no args dies"; 434 435 like exception { $plugin->realm('') }, qr/realm name not provided/, 436 "... and calling it with single empty arg dies."; 437 438 foreach my $username ( undef, +{}, '', 'username' ) { 439 foreach my $password ( undef, +{}, '', 'password' ) { 440 my $ret = $plugin->authenticate_user( $username, $password ); 441 is $ret, 0, 442 "Checking authenticate_user with username/password: " 443 . mydumper($username) . "/" 444 . mydumper($password); 445 } 446 } 447}; 448 449sub mydumper { 450 my $val = shift; 451 !defined $val && return '(undef)'; 452 ref($val) ne '' && return ref($val); 453 $val eq '' && return '(empty)'; 454 $val; 455}; 456 457# hooks 458 459hook before_authenticate_user => sub { 460 debug "before_authenticate_user", to_json( shift, { canonical => 1 } ); 461}; 462hook after_authenticate_user => sub { 463 debug "after_authenticate_user", to_json( shift, { canonical => 1 } ); 464}; 465hook before_create_user => sub { 466 debug "before_create_user", to_json( shift, { canonical => 1 } ); 467}; 468hook after_create_user => sub { 469 my ( $username, $user, $errors ) = @_; 470 my $ret = $user ? 1 : 0; 471 debug "after_create_user,$username,$ret,",scalar @$errors ? 'yes' : 'no'; 472}; 473 474# and finally the routes for the main plugin tests 475 476get '/provider_can' => sub { 477 send_as YAML => \@provider_can; 478}; 479 480get '/' => sub { 481 "Index always accessible"; 482}; 483 484post '/authenticate_user' => sub { 485 my $params = body_parameters->as_hashref; 486 my @ret = authenticate_user( $params->{username}, $params->{password}, 487 $params->{realm} ); 488 send_as YAML => \@ret; 489}; 490 491post '/create_user' => sub { 492 my $params = body_parameters->as_hashref; 493 my $user = create_user %$params; 494 return $user ? 1 : 0; 495}; 496 497post '/get_user_details' => sub { 498 my $params = body_parameters->as_hashref; 499 my $user = get_user_details $params->{username}, $params->{realm}; 500 if ( blessed($user) ) { 501 if ( $user->isa('DBIx::Class::Row')) { 502 $user = +{ $user->get_columns }; 503 } 504 else { 505 # assume some kind of hash-backed object 506 $user = \%$user; 507 } 508 } 509 return $user ? send_as YAML => $user : 0; 510}; 511 512get '/session_data' => sub { 513 my $session = session->data; 514 send_as YAML => $session; 515}; 516 517get '/logged_in_user_lastlogin' => sub { 518 my $dt = logged_in_user_lastlogin; 519 if ( ref($dt) eq 'DateTime' ) { 520 return $dt->ymd; 521 } 522 return 'not set'; 523}; 524 525get '/logged_in_user' => sub { 526 my $user = logged_in_user; 527 if ( blessed($user) ) { 528 if ( $user->isa('DBIx::Class::Row')) { 529 $user = +{ $user->get_columns }; 530 } 531 else { 532 # assume some kind of hash-backed object 533 $user = \%$user; 534 } 535 } 536 send_as YAML => $user ? $user : 'none'; 537}; 538 539get '/logged_in_user_twice' => sub { 540 logged_in_user; # retrieve 541 my $user = logged_in_user; # should now be stashed in var 542 if ( blessed($user) ) { 543 if ( $user->isa('DBIx::Class::Row')) { 544 $user = +{ $user->get_columns }; 545 } 546 else { 547 # assume some kind of hash-backed object 548 $user = \%$user; 549 } 550 } 551 send_as YAML => $user ? $user : 'none'; 552}; 553 554get '/loggedin' => require_login sub { 555 "You are logged in"; 556}; 557 558get qr{/regex/(.+)} => require_login sub { 559 return "Matched"; 560}; 561 562get '/require_login_no_sub' => require_login; 563 564get '/require_login_not_coderef' => require_login { a => 1 }; 565 566get '/roles' => require_login sub { 567 my $roles = user_roles() || []; 568 return join ',', sort @$roles; 569}; 570 571get '/roles/:user' => require_login sub { 572 my $user = param 'user'; 573 return join ',', sort @{ user_roles($user) }; 574}; 575 576get '/roles/:user/:realm' => require_login sub { 577 my $user = param 'user'; 578 my $realm = param 'realm'; 579 return join ',', sort @{ user_roles($user, $realm) }; 580}; 581 582get '/user_roles' => sub { 583 return join ',', sort @{ user_roles() }; 584}; 585 586get '/beer' => require_role BeerDrinker => sub { 587 "You can have a beer"; 588}; 589 590get '/piss' => require_role BearGrylls => sub { 591 "You can drink piss"; 592}; 593 594get '/piss/regex' => require_role qr/beer/i => sub { 595 "You can drink piss now"; 596}; 597 598get '/anyrole' => require_any_role ['Foo','BeerDrinker'] => sub { 599 "Matching one of multiple roles works"; 600}; 601 602get '/allroles' => require_all_roles ['BeerDrinker', 'Motorcyclist'] => sub { 603 "Matching multiple required roles works"; 604}; 605 606get '/not_allroles' => require_all_roles ['BeerDrinker', 'BadRole'] => sub { 607 "Matching multiple required roles should fail"; 608}; 609 610get '/does_dave_drink_beer' => sub { 611 return user_has_role('dave', 'BeerDrinker'); 612}; 613 614get '/does_dave_drink_cider' => sub { 615 return user_has_role('dave', 'CiderDrinker'); 616}; 617 618get '/does_undef_drink_beer' => sub { 619 return user_has_role(undef, 'BeerDrinker'); 620}; 621 622get '/user_password' => sub { 623 return user_password params('query'); 624}; 625post '/user_password' => sub { 626 return user_password %{ body_parameters->as_hashref }; 627}; 628 629get '/update_current_user' => sub { 630 my $user = update_current_user name => "I love cider"; 631 if ( blessed($user) ) { 632 if ( $user->isa('DBIx::Class::Row')) { 633 $user = +{ $user->get_columns }; 634 } 635 else { 636 # assume some kind of hash-backed object 637 $user = \%$user; 638 } 639 } 640 YAML::Dump $user; 641}; 642 643get '/update_user_name/:realm' => sub { 644 my $realm = param 'realm'; 645 YAML::Dump update_user 'mark', realm => $realm, name => "Wiltshire Apples $realm"; 646}; 647 648post '/update_user' => sub { 649 my $params = body_parameters->as_hashref; 650 my $username = delete $params->{username}; 651 send_as YAML => update_user $username, %$params; 652}; 653 654get '/get_user_mark/:realm' => sub { 655 my $realm = param 'realm'; 656 content_type 'text/x-yaml'; 657 my $user = get_user_details 'mark', $realm; 658 if ( blessed($user) ) { 659 if ( $user->isa('DBIx::Class::Row')) { 660 $user = +{ $user->get_columns }; 661 } 662 else { 663 # assume some kind of hash-backed object 664 $user = \%$user; 665 } 666 } 667 YAML::Dump $user; 668}; 669 670post '/auth_provider' => sub { 671 $plugin->auth_provider( body_parameters->get('realm') ); 672 return; 673}; 674 675get '/logged_in_user_password_expired' => sub { 676 my $ret = logged_in_user_password_expired; 677 return $ret ? 'yes' : 'no'; 678}; 679 6801; 681