Developer-Dashboard
view release on metacpan or search on metacpan
t/105-web-app-coverage-2.t view on Meta::CPAN
# =====================================================================
is( $app->handle( path => '/app/route-skill', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill index' );
is( $app->handle( path => '/app/route-skill/edit', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill index edit' );
is( $app->handle( path => '/app/route-skill/source', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill index source' );
is( $app->handle( path => '/app/route-skill/foo', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill page' );
is( $app->handle( path => '/app/route-skill/ajax-demo', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill ajax-demo page' );
is( $app->handle( path => '/app/route-skill/missing', remote_addr => '127.0.0.1', headers => H() )->[0], 404, 'skill missing bookmark 404' );
is( $app->handle( path => '/app/route-skill/def', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'nested skill index' );
is( $app->handle( path => '/app/route-skill/def/edit', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'nested skill edit' );
is( $app->handle( path => '/app/route-skill/def/foo', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'nested skill page' );
is( $app->handle( path => '/app/missing-skill/foo', remote_addr => '127.0.0.1', headers => H() )->[0], 404, 'missing nested skill 404' );
# skill edit POST render (source_kind skill render path)
{
my $instr = "TITLE: Skill Route Index\n:--------------------------------------------------------------------------------:\nBOOKMARK: index\n:--------------------------------------------------------------------------------:\nHTML:\nEdited Skill In...
is( $app->handle( path => '/app/route-skill/edit', method => 'POST', body => 'mode=render&instruction=' . uri_escape($instr), remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill edit POST render' );
}
# custom skill routes
is( $app->handle( path => '/apps/route-skill/home', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'custom skill app route' );
is( $app->handle( path => '/apps/route-skill/child/foo', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'nested custom skill app route' );
is( $app->handle( path => '/v1/route-skill/bar', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'custom skill ajax route' );
is( $app->handle( path => '/v1/route-skill/raw', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'custom skill ajax raw mime' );
is( $app->handle( path => '/assets/route-skill.js', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'custom skill js route' );
is( $app->handle( path => '/assets/route-skill.css', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'custom skill css route' );
is( $app->handle( path => '/downloads/route-skill.txt', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'custom skill others route' );
# skill-local static + ajax routes
is( $app->handle( path => '/js/route-skill/skill.js', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill-local js' );
is( $app->handle( path => '/css/route-skill/skill.css', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill-local css' );
is( $app->handle( path => '/others/route-skill/info.txt', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill-local others' );
is( $app->handle( path => '/ajax/route-skill/bar', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'skill-local ajax' );
# legacy /skill/<repo>/<route>
is( $app->handle( path => '/skill/route-skill/bookmarks/foo', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'legacy /skill route' );
# global fallback when first segment matches skill name but asset is global
wfile( File::Spec->catfile( $paths->dashboards_root, 'public', 'js', 'route-skill', 'fallback.js' ), qq|console.log("global");\n|, 0644 );
is( $app->handle( path => '/js/route-skill/fallback.js', remote_addr => '127.0.0.1', headers => H() )->[0], 200, 'global js fallback under skill name' );
# =====================================================================
# 5. API machine-auth + helper login/logout (non-loopback host -> non-admin).
# =====================================================================
{
my $api_json = sprintf
'{"good":{"secret":"%s","ajax":["/ajax/demo.json"]},"nosec":{"ajax":["/ajax/nosec"]},"bad1":"string","bad2":{"ajax":"notarray","secret":"z"}}',
sha256_hex('sekret');
wfile( File::Spec->catfile( $paths->config_root, 'api.json' ), $api_json, 0644 );
my $api_config = Developer::Dashboard::Config->new( files => $files, paths => $paths );
my $app_api = Developer::Dashboard::Web::App->new(
actions => $actions,
auth => $auth,
config => $api_config,
pages => $store,
prompt => $prompt,
runtime => $runtime,
sessions => $sessions,
);
my $ah = 'helper.example:7890';
# registered ajax route, no credentials -> forbidden (api path, no session)
is( $app_api->handle( path => '/ajax/demo.json', query => 'type=json', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 403, 'registered api route without creds -> 403' );
# registered ajax route with valid credentials -> 200
is( $app_api->handle( path => '/ajax/demo.json', query => 'type=json', remote_addr => '203.0.113.7', headers => { host => $ah, 'x-dd-api-key' => 'good', 'x-dd-api-secret' => 'sekret' } )->[0], 200, 'registered api route with creds -> 200' );
# unregistered route, no helper users yet -> helper access disabled (401 empty)
is( $app_api->handle( path => '/app/welcome', query => '', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'outsider before helper users -> 401' );
# create a helper user; now outsiders get a login page
$auth->add_user( username => 'helper', password => 'helper-pass-123', role => 'helper' );
is( $app_api->handle( path => '/app/welcome', query => 'x=1', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'outsider after helper users -> login 401' );
# bad + good login
is( $app_api->handle( path => '/login', method => 'POST', body => 'username=helper&password=wrong', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 401, 'bad login -> 401' );
my $login = $app_api->handle( path => '/login', method => 'POST', body => 'username=helper&password=helper-pass-123&redirect_to=/app/welcome', remote_addr => '203.0.113.7', headers => { host => $ah } );
is( $login->[0], 302, 'good login -> 302' );
my $cookie = $login->[3]{'Set-Cookie'};
# DD-415: a malicious backslash-authority redirect_to (%2F%5C -> "/\") must
# never reach the 302 Location header; browsers treat "/\evil.com" as an
# off-site authority, so the sanitizer has to collapse it to "/".
my $evil_login = $app_api->handle( path => '/login', method => 'POST', body => 'username=helper&password=helper-pass-123&redirect_to=%2F%5Cevil.com', remote_addr => '203.0.113.7', headers => { host => $ah } );
is( $evil_login->[0], 302, 'malicious backslash redirect still authenticates' );
is( $evil_login->[3]{Location}, '/', 'DD-415: backslash-authority redirect_to collapses to / in Location header' );
# DD-415: same bypass class through a raw tab (%2F%09%2F -> "/\t/"). A URL
# parser strips the tab before parsing, so an unsanitized target reaches the
# browser as "//evil.com" and becomes an off-site protocol-relative redirect.
my $tab_login = $app_api->handle( path => '/login', method => 'POST', body => 'username=helper&password=helper-pass-123&redirect_to=%2F%09%2Fevil.com', remote_addr => '203.0.113.7', headers => { host => $ah } );
is( $tab_login->[0], 302, 'malicious tab redirect still authenticates' );
is( $tab_login->[3]{Location}, '/', 'DD-415: tab-hidden authority redirect_to collapses to / in Location header' );
# registered ajax route WITH session cookie -> session path (skip api)
is( $app_api->handle( path => '/ajax/demo.json', query => 'type=json', remote_addr => '203.0.113.7', headers => { host => $ah, cookie => $cookie } )->[0], 200, 'session cookie keeps ajax access' );
# unregistered route WITH session cookie -> authorized
ok( $app_api->handle( path => '/app/welcome', query => '', remote_addr => '203.0.113.7', headers => { host => $ah, cookie => $cookie } )->[0], 'session cookie authorizes app route' );
# logout removes the helper user (role helper + username set)
is( $app_api->handle( path => '/logout', remote_addr => '203.0.113.7', headers => { host => $ah, cookie => $cookie } )->[0], 302, 'logout with helper session' );
# logout with no session
is( $app_api->handle( path => '/logout', remote_addr => '203.0.113.7', headers => { host => $ah } )->[0], 302, 'logout with no session' );
# logout with a non-helper session (role admin, username empty -> 376 false side)
my $admin_sess = $sessions->create( username => 'nonhelper', role => 'admin', remote_addr => '203.0.113.7' );
is( $app_api->handle( path => '/logout', remote_addr => '203.0.113.7', headers => { host => $ah, cookie => "dashboard_session=$admin_sess->{session_id}" } )->[0], 302, 'logout with non-helper session' );
# login_response with no body (364 false side)
ok( $app_api->login_response( remote_addr => '203.0.113.7' )->[0], 'login_response tolerates missing body' );
# GET /login (path=/login but method ne POST -> 119 middle)
ok( $app_api->handle( path => '/login', method => 'GET', remote_addr => '127.0.0.1', headers => H() )->[0], 'GET /login falls through' );
# direct _api_route_registered edge cases
is( $app_api->_api_route_registered(undef), 0, 'api route registered undef path' );
is( $app_api->_api_route_registered('/foo'), 0, 'api route registered non-ajax path' );
is( $app_api->_api_route_registered('/ajax/demo.json'), 1, 'api route registered known path' );
# direct _authorize_api_request edge cases
ok( $app_api->_authorize_api_request( path => '/ajax/demo.json', headers => { 'x-dd-api-key' => 'good', 'x-dd-api-secret' => 'sekret' } ), 'api auth ok' );
ok( !$app_api->_authorize_api_request( path => '/ajax/demo.json', headers => { 'x-dd-api-key' => 'good', 'x-dd-api-secret' => 'wrong' } ), 'api auth bad secret' );
ok( !$app_api->_authorize_api_request( path => '/ajax/other', headers => { 'x-dd-api-key' => 'good', 'x-dd-api-secret' => 'sekret' } ), 'api auth path not allowed' );
ok( !$app_api->_authorize_api_request( path => '/x', headers => { 'x-dd-api-key' => 'bad1', 'x-dd-api-secret' => 'z' } ), 'api auth entry not a hash' );
ok( !$app_api->_authorize_api_request( path => '/x', headers => { 'x-dd-api-key' => 'bad2', 'x-dd-api-secret' => 'z' } ), 'api auth ajax not an array' );
ok( !$app_api->_authorize_api_request( path => '/ajax/nosec', headers => { 'x-dd-api-key' => 'nosec', 'x-dd-api-secret' => 'anything' } ), 'api auth entry without secret' );
ok( !$app_api->_authorize_api_request( headers => {} ), 'api auth empty headers' );
( run in 1.392 second using v1.01-cache-2.11-cpan-007c89162af )