1use strict; 2use warnings; 3use Test::More import => ['!pass']; 4use Plack::Test; 5use HTTP::Request::Common; 6use Ref::Util qw<is_coderef>; 7use List::Util qw<all>; 8 9use Dancer2::Core::App; 10use Dancer2::Core::Response; 11use Dancer2::Core::Request; 12use Dancer2::Core::Error; 13 14use JSON::MaybeXS qw/JSON/; # Error serialization 15 16my $env = { 17 'psgi.url_scheme' => 'http', 18 REQUEST_METHOD => 'GET', 19 SCRIPT_NAME => '/foo', 20 PATH_INFO => '/bar/baz', 21 REQUEST_URI => '/foo/bar/baz', 22 QUERY_STRING => 'foo=42&bar=12&bar=13&bar=14', 23 SERVER_NAME => 'localhost', 24 SERVER_PORT => 5000, 25 SERVER_PROTOCOL => 'HTTP/1.1', 26 REMOTE_ADDR => '127.0.0.1', 27 HTTP_COOKIE => 28 'dancer.session=1234; fbs_102="access_token=xxxxxxxxxx%7Cffffff"', 29 HTTP_X_FORWARDED_FOR => '127.0.0.2', 30 REMOTE_HOST => 'localhost', 31 HTTP_USER_AGENT => 'Mozilla', 32 REMOTE_USER => 'sukria', 33}; 34 35my $app = Dancer2::Core::App->new( name => 'main' ); 36my $request = $app->build_request($env); 37 38$app->set_request($request); 39 40subtest 'basic defaults of Error object' => sub { 41 my $err = Dancer2::Core::Error->new( app => $app ); 42 is $err->status, 500, 'code'; 43 is $err->title, 'Error 500 - Internal Server Error', 'title'; 44 is $err->message, '', 'message'; 45 like $err->content, qr!http://localhost:5000/foo/css!, 46 "error content contains css path relative to uri_base"; 47}; 48 49subtest "send_error in route" => sub { 50 { 51 52 package App; 53 use Dancer2; 54 55 set serializer => 'JSON'; 56 57 get '/error' => sub { 58 send_error "This is a custom error message"; 59 return "send_error returns so this content is not processed"; 60 }; 61 } 62 63 my $app = App->to_app; 64 ok( is_coderef($app), 'Got app' ); 65 66 test_psgi $app, sub { 67 my $cb = shift; 68 my $r = $cb->( GET '/error' ); 69 70 is( $r->code, 500, 'send_error sets the status to 500' ); 71 like( 72 $r->content, 73 qr{This is a custom error message}, 74 'Error message looks good', 75 ); 76 77 is( 78 $r->content_type, 79 'application/json', 80 'Response has appropriate content type after serialization', 81 ); 82 }; 83}; 84 85subtest "send_error with custom stuff" => sub { 86 { 87 88 package App; 89 use Dancer2; 90 91 get '/error/:x' => sub { 92 my $x = param('x'); 93 send_error "Error $x", "5$x"; 94 }; 95 } 96 97 my $app = App->to_app; 98 ok( is_coderef($app), 'Got app' ); 99 100 test_psgi $app, sub { 101 my $cb = shift; 102 my $r = $cb->( GET '/error/42' ); 103 104 is( $r->code, 542, 'send_error sets the status to 542' ); 105 like( $r->content, qr{Error 42}, 'Error message looks good' ); 106 }; 107}; 108 109subtest 'Response->error()' => sub { 110 my $resp = Dancer2::Core::Response->new; 111 112 isa_ok $resp->error( message => 'oops', status => 418 ), 113 'Dancer2::Core::Error'; 114 115 is $resp->status => 418, 'response code is 418'; 116 like $resp->content => qr/oops/, 'response content overriden by error'; 117 like $resp->content => qr/teapot/, 'error code title is present'; 118 ok $resp->is_halted, 'response is halted'; 119}; 120 121subtest 'Throwing an error with a response' => sub { 122 my $resp = Dancer2::Core::Response->new; 123 124 my $err = eval { Dancer2::Core::Error->new( 125 exception => 'our exception', 126 show_errors => 1 127 )->throw($resp) }; 128 129 isa_ok($err, 'Dancer2::Core::Response', "Error->throw() accepts a response"); 130}; 131 132subtest 'Error with show_errors: 0' => sub { 133 my $err = Dancer2::Core::Error->new( 134 exception => 'our exception', 135 show_errors => 0 136 )->throw; 137 unlike $err->content => qr/our exception/; 138}; 139 140subtest 'Error with show_errors: 1' => sub { 141 my $err = Dancer2::Core::Error->new( 142 exception => 'our exception', 143 show_errors => 1 144 )->throw; 145 like $err->content => qr/our exception/; 146}; 147 148subtest 'App dies with serialized error' => sub { 149 { 150 package AppDies; 151 use Dancer2; 152 set serializer => 'JSON'; 153 154 get '/die' => sub { 155 die "oh no\n"; # I should serialize 156 }; 157 } 158 159 my $app = AppDies->to_app; 160 isa_ok( $app, 'CODE', 'Got app' ); 161 162 test_psgi $app, sub { 163 my $cb = shift; 164 my $r = $cb->( GET '/die' ); 165 166 is( $r->code, 500, '/die returns 500' ); 167 168 my $out = eval { JSON->new->utf8(0)->decode($r->decoded_content) }; 169 ok(!$@, 'JSON decoding serializer error produces no errors'); 170 isa_ok($out, 'HASH', 'Error deserializes to a hash'); 171 like($out->{exception}, qr/^oh no/, 'Get expected error message'); 172 }; 173}; 174 175subtest 'Error with exception object' => sub { 176 local $@; 177 eval { MyTestException->throw('a test exception object') }; 178 my $err = Dancer2::Core::Error->new( 179 exception => $@, 180 show_errors => 1, 181 )->throw; 182 183 like $err->content, qr/a test exception object/, 'Error content contains exception message'; 184}; 185 186subtest 'Errors without server tokens' => sub { 187 { 188 package AppNoServerTokens; 189 use Dancer2; 190 set serializer => 'JSON'; 191 set no_server_tokens => 1; 192 193 get '/ohno' => sub { 194 die "oh no"; 195 }; 196 } 197 198 my $test = Plack::Test->create( AppNoServerTokens->to_app ); 199 my $r = $test->request( GET '/ohno' ); 200 is( $r->code, 500, "/ohno returned 500 response"); 201 is( $r->header('server'), undef, "No server header when no_server_tokens => 1" ); 202}; 203 204subtest 'Errors with show_errors and circular references' => sub { 205 { 206 package App::ShowErrorsCircRef; 207 use Dancer2; 208 set show_errors => 1; 209 set something_with_config => {something => config}; 210 set password => '===VERY-UNIQUE-STRING==='; 211 set innocent_thing => '===VERY-INNOCENT-STRING==='; 212 set template => 'simple'; 213 214 # Trigger an error that makes Dancer2::Core::Error::_censor enter an 215 # infinite loop 216 get '/ohno' => sub { 217 template q{I don't exist}; 218 }; 219 220 } 221 222 my $test = Plack::Test->create( App::ShowErrorsCircRef->to_app ); 223 my $r = $test->request( GET '/ohno' ); 224 is( $r->code, 500, "/ohno returned 500 response"); 225 like( $r->content, qr{Stack}, 'it includes a stack trace' ); 226 227 my @password_values = ($r->content =~ /\bpassword\b(.+)\n/g); 228 my $is_password_hidden = 229 all { /Hidden \(looks potentially sensitive\)/ } @password_values; 230 231 ok($is_password_hidden, "password was hidden in stacktrace"); 232 233 cmp_ok(@password_values, '>', 1, 234 'password key appears more than once in the stacktrace'); 235 236 unlike($r->content, qr{===VERY-UNIQUE-STRING===}, 237 'password value does not appear in the stacktrace'); 238 239 like($r->content, qr{===VERY-INNOCENT-STRING===}, 240 'Values for other keys (non-sensitive) appear in the stacktrace'); 241}; 242 243done_testing; 244 245 246{ # Simple test exception class 247 package MyTestException; 248 249 use overload '""' => \&as_str; 250 251 sub new { 252 return bless {}; 253 } 254 255 sub throw { 256 my ( $class, $error ) = @_; 257 my $self = ref($class) ? $class : $class->new; 258 $self->{error} = $error; 259 260 die $self; 261 } 262 263 sub as_str { return $_[0]->{error} } 264} 265