Developer-Dashboard
view release on metacpan or search on metacpan
t/08-web-update-coverage.t view on Meta::CPAN
my @patterns;
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::_pkill_perl = sub {
my ( $self, $pattern ) = @_;
push @patterns, $pattern;
return 1;
};
my ( $stop_code, undef, $stop_body ) = @{ $app->handle( path => '/ajax/singleton/stop', query => 'singleton=BROWSER-STOP', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
is( $stop_code, 204, 'singleton stop route returns no content after lifecycle cleanup' );
is( $stop_body, '', 'singleton stop route keeps the response body empty' );
my ( $direct_stop_code, undef, $direct_stop_body ) = @{ $app->ajax_singleton_stop_response( query => 'singleton=BROWSER-STOP' ) };
is( $direct_stop_code, 204, 'direct singleton stop response returns no content after lifecycle cleanup' );
is( $direct_stop_body, '', 'direct singleton stop response keeps the response body empty' );
}
is_deeply(
\@patterns,
[ '^dashboard ajax: BROWSER-STOP$', '^dashboard ajax: BROWSER-STOP$' ],
'singleton stop route and direct response target the matching saved ajax worker process title',
);
}
{
my $shebang_page = Developer::Dashboard::PageDocument->from_instruction(<<'PAGE');
BOOKMARK: ajax-shebang
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.demo.endpoint', type => 'text', file => 'script-runner', code => qq{#!/bin/sh\nprintf 'shell-out\\n'\nprintf 'shell-err\\n' >&2\n};
PAGE
$store->save_page($shebang_page);
my ( undef, undef, undef ) = @{ $app->handle( path => '/app/ajax-shebang', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
my ( $ajax_shebang_code, undef, $ajax_shebang_body ) = @{ $app->handle( path => '/ajax/script-runner', query => 'type=text', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
my $ajax_shebang_output = drain_stream_body($ajax_shebang_body);
is( $ajax_shebang_code, 200, 'legacy ajax saved-file route executes shebang scripts directly' );
like( $ajax_shebang_output, qr/shell-out/, 'legacy ajax saved-file route streams direct executable stdout' );
like( $ajax_shebang_output, qr/shell-err/, 'legacy ajax saved-file route streams direct executable stderr' );
}
{
my $runtime_api_file = File::Spec->catfile( $paths->config_root, 'api.json' );
open my $runtime_api, '>:raw', $runtime_api_file
or die "Unable to write runtime api.json: $!";
print {$runtime_api} qq|{"runtime-client":{"secret":"@{[ sha256_hex('stream-secret') ]}","ajax":["/ajax/stream.txt"]}}|;
close $runtime_api or die "Unable to close runtime api.json: $!";
my $skill_api_dir = File::Spec->catdir( $dancer_skill_install->{path}, 'config' );
make_path($skill_api_dir);
open my $skill_api, '>:raw', File::Spec->catfile( $skill_api_dir, 'api.json' )
or die "Unable to write skill api.json: $!";
print {$skill_api} qq|{"skill-client":{"secret":"@{[ sha256_hex('skill-secret') ]}","ajax":["/ajax/dancer-route-skill/bar"]}}|;
close $skill_api or die "Unable to close skill api.json: $!";
my $api_host = 'dashboard-helper.example:7890';
my ( $registered_no_api_code, $registered_no_api_type, $registered_no_api_body ) = @{ $app->handle(
path => '/ajax/stream.txt',
query => 'type=text',
remote_addr => '127.0.0.1',
headers => { host => $api_host },
) };
is( $registered_no_api_code, 403, 'registered remote ajax route rejects requests that omit API credentials' );
like( $registered_no_api_type, qr/application\/json/, 'registered remote ajax route returns JSON when API credentials are missing' );
is_deeply( json_decode( decode_body_text($registered_no_api_body) ), { status => 'forbidden' }, 'registered remote ajax route returns the explicit forbidden JSON payload when API credentials are missing' );
my ( $registered_bad_api_code, undef, $registered_bad_api_body ) = @{ $app->handle(
path => '/ajax/stream.txt',
query => 'type=text',
remote_addr => '127.0.0.1',
headers => {
host => $api_host,
'x-dd-api-key' => 'runtime-client',
'x-dd-api-secret' => 'wrong-secret',
},
) };
is( $registered_bad_api_code, 403, 'registered remote ajax route rejects invalid API secrets' );
is_deeply( json_decode( decode_body_text($registered_bad_api_body) ), { status => 'forbidden' }, 'registered remote ajax route keeps the forbidden JSON payload for invalid API secrets' );
my ( $unregistered_remote_code, undef, $unregistered_remote_body ) = @{ $app->handle(
path => '/ajax/process-endpoint.json',
query => 'type=text',
remote_addr => '127.0.0.1',
headers => { host => $api_host },
) };
is( $unregistered_remote_code, 401, 'unregistered remote ajax routes still follow the existing helper-auth flow' );
is( $unregistered_remote_body, '', 'unregistered remote ajax routes stay silent before helper users exist' );
my ( $runtime_api_code, $runtime_api_type, $runtime_api_body ) = @{ $app->handle(
path => '/ajax/stream.txt',
query => 'type=text',
remote_addr => '127.0.0.1',
headers => {
host => $api_host,
'x-dd-api-key' => 'runtime-client',
'x-dd-api-secret' => 'stream-secret',
},
) };
is( $runtime_api_code, 200, 'registered runtime ajax route accepts a matching API key and secret' );
like( $runtime_api_type, qr/text\/plain/, 'registered runtime ajax route keeps the saved ajax content type under API auth' );
is( drain_stream_body($runtime_api_body), "first\nsecond\n", 'registered runtime ajax route executes through API auth without requiring a helper session' );
my ( $skill_api_code, $skill_api_type, $skill_api_body ) = @{ $app->handle(
path => '/ajax/dancer-route-skill/bar',
query => '',
remote_addr => '127.0.0.1',
headers => {
host => $api_host,
'x-dd-api-key' => 'skill-client',
'x-dd-api-secret' => 'skill-secret',
},
) };
is( $skill_api_code, 200, 'installed skill config/api.json can authorize its ajax routes through API credentials' );
like( $skill_api_type, qr/application\/json/, 'skill API auth keeps the skill ajax response content type' );
is( decode_body_text( drain_stream_body($skill_api_body) ), qq|{"route":"dancer-top"}\n|, 'installed skill API auth streams the skill ajax response body' );
my $psgi_app = Developer::Dashboard::Web::DancerApp->build_psgi_app( app => $app );
Local::PSGITest::test_psgi $psgi_app, sub {
my ($cb) = @_;
my $res = $cb->(
GET(
'http://127.0.0.1/ajax/stream.txt?type=text',
Host => $api_host,
'X-DD-API-Key' => 'runtime-client',
'X-DD-API-Secret' => 'stream-secret',
)
);
is( $res->code, 200, 'PSGI adapter forwards API auth headers to the backend ajax gate' );
is( decode_body_text( $res->content ), "first\nsecond\n", 'PSGI adapter keeps API-auth ajax output intact' );
};
}
is( $auth->trust_tier( remote_addr => '127.0.0.1', host => '127.0.0.1:7890' ), 'admin', 'exact loopback with numeric host is admin' );
is( $auth->trust_tier( remote_addr => '127.0.0.1', host => 'localhost:7890' ), 'admin', 'localhost is trusted as admin when it resolves only to loopback' );
is( $auth->trust_tier( remote_addr => '10.0.0.8', host => '127.0.0.1:7890' ), 'helper', 'non-loopback client is helper' );
my @initial_users = $auth->list_users;
is( scalar @initial_users, 0, 'auth store starts empty' );
ok( !$auth->verify_user( username => 'helper', password => 'nope' ), 'missing user does not verify' );
like( $auth->login_page( message => '<unsafe>' ), qr/<unsafe>/, 'login page escapes message content' );
my $helper_host = 'dashboard-helper.example:7890';
my ( $login_required_code, undef, $login_required_body ) = @{ $app->handle( path => '/', query => '', remote_addr => '127.0.0.1', headers => { host => $helper_host } ) };
is( $login_required_code, 401, 'non-loopback helper-host requests are unauthorized when no helper user exists' );
is( $login_required_body, '', 'outsider requests return an empty body before helper users exist' );
unlike( $login_required_body, qr/<form method="post" action="\/login">/, 'outsider requests without helper users do not receive a login form' );
my ( $saved_login_required_code, undef, $saved_login_required_body ) = @{ $app->handle(
path => '/app/index',
query => 'from=helper',
remote_addr => '127.0.0.1',
headers => { host => $helper_host },
) };
is( $saved_login_required_code, 401, 'outsider access to a saved page is unauthorized when no helper user exists' );
is( $saved_login_required_body, '', 'forbidden outsider access to a saved page stays silent before helper users exist' );
unlike( $saved_login_required_body, qr{<input[^>]*name="redirect_to"}, 'forbidden outsider requests do not expose a login redirect target before helper users exist' );
my ( $disabled_login_code, undef, $disabled_login_body ) = @{ $app->handle(
path => '/login',
method => 'POST',
body => form_body( username => 'helper', password => 'helper-pass-123' ),
remote_addr => '127.0.0.1',
headers => { host => $helper_host },
) };
is( $disabled_login_code, 401, 'login submission is unauthorized when no helper user exists' );
is( $disabled_login_body, '', 'login submission stays silent before helper users exist' );
my $user = $auth->add_user( username => 'helper', password => 'helper-pass-123', role => 'helper' );
is( $user->{role}, 'helper', 'helper user can be created' );
ok( $auth->verify_user( username => 'helper', password => 'helper-pass-123' ), 'correct password verifies' );
ok( !$auth->verify_user( username => 'helper', password => 'wrong' ), 'wrong password does not verify' );
my $alpha = $auth->add_user( username => 'alpha', password => 'alpha-pass-123', role => 'helper' );
is( $alpha->{username}, 'alpha', 'second helper user can be created' );
my @listed_users = $auth->list_users;
( run in 0.960 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )