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 )