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