Developer-Dashboard
view release on metacpan or search on metacpan
t/10-extension-action-docker.t view on Meta::CPAN
page => $saved_page,
source => 'saved',
);
like( $paths_result->{body}, qr/"runtime"/, 'builtin paths.list action returns runtime data' );
my ($command_action) = grep { $_->{id} eq 'run' } @{ $saved_page->as_hash->{actions} };
my $command_result = $actions->run_page_action(
action => $command_action,
page => $saved_page,
source => 'saved',
);
is( $command_result->{exit_code}, 0, 'trusted saved page command action executes' );
like( $command_result->{stdout}, qr/shell-output/, 'trusted command action captures stdout' );
my $env_result = $actions->run_command_action(
command => 'printf "$ACTION_ENV"',
cwd => $repo,
env => { ACTION_ENV => 'env-ok' },
timeout_ms => 1000,
);
like( $env_result->{stdout}, qr/env-ok/, 'command actions inject explicit env values' );
my $timeout_result = $actions->run_command_action(
command => "$^X -e 'sleep 2'",
cwd => $repo,
timeout_ms => 200,
);
is( $timeout_result->{exit_code}, 124, 'command action timeout returns timeout exit code' );
ok( $timeout_result->{timed_out}, 'command action timeout is marked' );
my $background_result = $actions->run_command_action(
command => "$^X -e 'sleep 1'",
cwd => $repo,
background => 1,
);
ok( $background_result->{pid} > 0, 'background command action returns a child pid' );
ok( $actions->_pid_is_running( $background_result->{pid} ), 'background action child is running initially' );
kill 'TERM', $background_result->{pid};
my $background_wait_loops = $INC{'Devel/Cover.pm'} ? 100 : 20;
for ( 1 .. $background_wait_loops ) {
last if !$actions->_pid_is_running( $background_result->{pid} );
sleep 0.1;
}
ok( !$actions->_pid_is_running( $background_result->{pid} ), 'background action child stops cleanly without leaving a direct child for the caller to reap' );
my $transient = Developer::Dashboard::PageDocument->new(
title => 'Transient',
actions => [
{ id => 'run', label => 'Run', kind => 'command', command => 'printf nope' },
],
permissions => {},
);
my $transient_page = $transient;
eval {
$actions->run_page_action(
action => { id => 'run', label => 'Run', kind => 'command', command => 'printf nope' },
page => $transient_page,
source => 'transient',
);
};
like( $@, qr/not trusted/, 'transient encoded page command action is blocked by default' );
eval {
$actions->run_page_action(
action => { id => 'bad', kind => 'weird' },
page => $saved_page,
source => 'saved',
);
};
like( $@, qr/Unsupported action kind/, 'unsupported action kinds are rejected' );
eval {
$actions->run_page_action(
action => { id => 'bad', kind => 'builtin', builtin => 'unknown' },
page => $saved_page,
source => 'saved',
);
};
like( $@, qr/Unsupported builtin action/, 'unsupported builtin actions are rejected' );
my $transient_allowed = Developer::Dashboard::PageDocument->new(
title => 'Transient Allowed',
actions => [
{ id => 'run', label => 'Run', kind => 'command', command => 'printf allowed' },
],
permissions => {
allow_untrusted_actions => 1,
trusted_actions => ['run'],
},
);
my $allowed_page = $transient_allowed;
my $allowed_result = $actions->run_page_action(
action => { id => 'run', label => 'Run', kind => 'command', command => 'printf allowed' },
page => $allowed_page,
source => 'transient',
);
like( $allowed_result->{stdout}, qr/allowed/, 'transient encoded page can opt in specific trusted actions' );
{
my $old = Cwd::getcwd();
chdir $repo or die $!;
my $docker = Developer::Dashboard::DockerCompose->new(
config => $config,
paths => $paths,
);
local $ENV{DOCKER_SKILL_BASE} = 'skill-base';
local $ENV{DD_TEST_DOCKER_ROOT} = File::Spec->catdir( $home, '.developer-dashboard', 'config', 'docker' );
is(
$docker->_expand_env_path('${DD_TEST_DOCKER_ROOT}/green/compose.yml'),
File::Spec->catfile( $ENV{DD_TEST_DOCKER_ROOT}, 'green', 'compose.yml' ),
'docker compose resolver expands defined braced environment variables in configured compose paths',
);
is(
$docker->_expand_env_path('${DD_TEST_DOCKER_MISSING}green/compose.yml'),
'green/compose.yml',
'docker compose resolver collapses undefined braced environment variables in configured compose paths',
);
is(
$docker->_expand_env_path('$DD_TEST_DOCKER_ROOT/green/compose.yml'),
File::Spec->catfile( $ENV{DD_TEST_DOCKER_ROOT}, 'green', 'compose.yml' ),
'docker compose resolver expands defined bare environment variables in configured compose paths',
t/10-extension-action-docker.t view on Meta::CPAN
my $auth = Developer::Dashboard::Auth->new( files => $files, paths => $paths );
my $sessions = Developer::Dashboard::SessionStore->new( paths => $paths );
my $app = Developer::Dashboard::Web::App->new(
actions => $actions,
auth => $auth,
pages => $pages,
resolver => $resolver,
sessions => $sessions,
);
my ( $provider_code, undef, $provider_body ) = @{ $app->handle( path => '/app/shared-provider', query => '', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
is( $provider_code, 200, 'provider page renders through web app' );
like( $provider_body, qr/Shared Provider/, 'provider page content is rendered' );
my ( $state_render_code, undef, $state_render_body ) = @{ $app->handle( path => '/app/action-page', query => 'filter=active', remote_addr => '127.0.0.1', headers => { host => '127.0.0.1' } ) };
is( $state_render_code, 200, 'saved page render with query state succeeds' );
like( $state_render_body, qr/active/, 'query parameters are reflected into page state rendering' );
my $atoken = $actions->encode_action_payload(
action => $saved_page->as_hash->{actions}[0],
page => $saved_page,
source => 'saved',
);
my ( $encoded_action_code, $encoded_action_type, $encoded_action_body ) = @{ $app->handle(
path => '/action',
method => 'POST',
query => 'atoken=' . uri_escape($atoken),
remote_addr => '127.0.0.1',
headers => { host => '127.0.0.1' },
) };
is( $encoded_action_code, 403, 'encoded action route is denied by default' );
like( $encoded_action_type, qr/text\/plain/, 'encoded action route denial returns plain text' );
like( $encoded_action_body, qr/Transient token URLs are disabled/, 'encoded action route denial explains the policy' );
{
local $ENV{DEVELOPER_DASHBOARD_ALLOW_TRANSIENT_URLS} = 1;
my ( $allowed_encoded_action_code, $allowed_encoded_action_type, $allowed_encoded_action_body ) = @{ $app->handle(
path => '/action',
method => 'POST',
query => 'atoken=' . uri_escape($atoken),
remote_addr => '127.0.0.1',
headers => { host => '127.0.0.1' },
) };
is( $allowed_encoded_action_code, 200, 'encoded action route executes when transient token URLs are enabled' );
like( $allowed_encoded_action_type, qr/application\/json/, 'encoded action route returns json when transient token URLs are enabled' );
like( $allowed_encoded_action_body, qr/"alpha"\s*:\s*"one"/, 'encoded action route returns page state when transient token URLs are enabled' );
}
my $transient_safe = Developer::Dashboard::PageDocument->new(
title => 'Transient Safe',
actions => [
{ id => 'state', label => 'State', kind => 'builtin', builtin => 'page.state', safe => 1 },
],
state => { beta => 'two' },
);
my $token = $actions->encode_action_payload(
action => { id => 'state', label => 'State', kind => 'builtin', builtin => 'page.state', safe => 1 },
page => $transient_safe,
source => 'transient',
);
my ( $transient_blocked_code, undef, $transient_blocked_body ) = @{ $app->handle(
path => '/action',
method => 'POST',
query => 'atoken=' . uri_escape($token),
remote_addr => '127.0.0.1',
headers => { host => '127.0.0.1' },
) };
is( $transient_blocked_code, 403, 'transient encoded builtin action route is denied by default' );
like( $transient_blocked_body, qr/Transient token URLs are disabled/, 'transient encoded builtin action denial explains the policy' );
{
local $ENV{DEVELOPER_DASHBOARD_ALLOW_TRANSIENT_URLS} = 1;
my ( $transient_code, $transient_type, $transient_body ) = @{ $app->handle(
path => '/action',
method => 'POST',
query => 'atoken=' . uri_escape($token),
remote_addr => '127.0.0.1',
headers => { host => '127.0.0.1' },
) };
is( $transient_code, 200, 'transient encoded builtin action route executes for safe actions when transient token URLs are enabled' );
like( $transient_type, qr/application\/json/, 'transient encoded builtin action route returns json when transient token URLs are enabled' );
like( $transient_body, qr/"beta"\s*:\s*"two"/, 'transient encoded builtin action route returns action output when transient token URLs are enabled' );
}
my ( $missing_action_code ) = @{ $app->handle(
path => '/app/action-page/action/missing',
method => 'POST',
remote_addr => '127.0.0.1',
headers => { host => '127.0.0.1' },
) };
is( $missing_action_code, 404, 'missing page actions return not found' );
my $app_without_actions = Developer::Dashboard::Web::App->new(
auth => $auth,
pages => $pages,
resolver => $resolver,
sessions => $sessions,
);
my ( $no_runner_code ) = @{ $app_without_actions->handle(
path => '/app/action-page/action/state',
method => 'POST',
remote_addr => '127.0.0.1',
headers => { host => '127.0.0.1' },
) };
is( $no_runner_code, 501, 'web action routes return not implemented when no action runner is configured' );
$auth->add_user( username => 'helper', password => 'helper-pass-123', role => 'helper' );
my @users_before_remove = $auth->list_users;
ok( scalar(@users_before_remove), 'helper users can be listed before removal' );
$auth->remove_user('helper');
my @users_after_remove = $auth->list_users;
is( scalar(@users_after_remove), 0, 'remove_user deletes helper records' );
done_testing;
__END__
=head1 NAME
10-extension-action-docker.t - extension, action, and docker resolver tests
=head1 DESCRIPTION
This test verifies config-driven extensions, page actions, encoded action
transport, and docker compose resolution behavior.
=for comment FULL-POD-DOC START
=head1 PURPOSE
( run in 2.401 seconds using v1.01-cache-2.11-cpan-75ffa21a3d4 )