Developer-Dashboard

 view release on metacpan or  search on metacpan

integration/blank-env/run-integration.pl  view on Meta::CPAN


    _write_text(
        File::Spec->catfile( $configs, 'config.json' ),
        <<'JSON'
{
  "collectors": [
    {
      "name": "fake.config.collector",
      "command": "printf 'fake config collector output\n'",
      "cwd": "home",
      "interval": 30
    },
    {
      "name": "broken.collector",
      "code": "this is broken perl code",
      "cwd": "home",
      "interval": 1,
      "indicator": {
        "name": "broken.indicator",
        "label": "Broken",
        "icon": "B"
      }
    },
    {
      "name": "healthy.collector",
      "command": "printf 'healthy config collector output\n'",
      "cwd": "home",
      "interval": 1,
      "indicator": {
        "name": "healthy.indicator",
        "label": "Healthy",
        "icon": "H"
      }
    }
  ]
}
JSON
    );

    _write_text(
        File::Spec->catfile( $bookmarks, 'project-home' ),
        <<'BOOKMARK'
TITLE: Project Home
:--------------------------------------------------------------------------------:
BOOKMARK: project-home
:--------------------------------------------------------------------------------:
STASH: {}
:--------------------------------------------------------------------------------:
HTML: <div id="project-marker">Fake Project Home</div>
BOOKMARK
    );
    _write_text(
        File::Spec->catfile( $bookmarks, 'legacy-ajax' ),
        <<'BOOKMARK'
TITLE: Legacy Ajax
:--------------------------------------------------------------------------------:
BOOKMARK: legacy-ajax
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.project.endpoint', type => 'text', file => 'project-endpoint.json', code => q{
print "saved-start\n";
warn "saved-warn\n";
system 'sh', '-c', 'printf "saved-child-out\n"; printf "saved-child-err\n" >&2';
die "saved-die\n";
};
BOOKMARK
    );
    _write_text(
        File::Spec->catfile( $bookmarks, 'legacy-ajax-stream' ),
        <<'BOOKMARK'
TITLE: Legacy Ajax Stream
:--------------------------------------------------------------------------------:
BOOKMARK: legacy-ajax-stream
:--------------------------------------------------------------------------------:
HTML: <script>var configs = {};</script>
:--------------------------------------------------------------------------------:
CODE1: Ajax jvar => 'configs.project.stream', file => 'project-stream.txt', code => q{
for (1..3) {
    print "stream$_\n";
    sleep 1;
}
};
BOOKMARK
    );
    _write_text(
        File::Spec->catfile( $bookmarks, 'nav', 'alpha.tt' ),
        <<'BOOKMARK'
TITLE: Alpha Nav
:--------------------------------------------------------------------------------:
BOOKMARK: nav/alpha.tt
:--------------------------------------------------------------------------------:
HTML: [% IF env.current_page == '/app/project-home' %]<div id="nav-alpha">Alpha Nav Current</div>[% ELSE %]<a id="nav-alpha" href="/app/project-home">Alpha Nav Link</a>[% END %]
BOOKMARK
    );
    _write_text(
        File::Spec->catfile( $bookmarks, 'nav', 'beta.tt' ),
        <<'BOOKMARK'
TITLE: Beta Nav
:--------------------------------------------------------------------------------:
BOOKMARK: nav/beta.tt
:--------------------------------------------------------------------------------:
HTML: <div id="nav-beta">[% env.current_page %] / [% env.runtime_context.current_page %]</div>
BOOKMARK
    );

    _run_shell( 'init fake project git repo', 'git init ' . _shell_quote($project) );

    my $install = _run_shell( 'cpanm install host-built tarball', 'cpanm --notest ' . _shell_quote($install_tarball) );
    _assert( $install->{exit_code} == 0, 'cpanm --notest installed host-built distribution tarball after the source-tree test gates passed' );

    my $bare = _run_shell( 'dashboard bare usage', 'dashboard', allow_fail => 1 );
    _assert( $bare->{exit_code} != 0, 'bare dashboard returns non-zero usage exit' );
    _assert_match( $bare->{stdout}, qr/Usage:/, 'bare dashboard prints usage output' );

    my $help = _run_shell( 'dashboard help', 'dashboard help' );
    _assert_match( $help->{stdout}, qr/Description:/, 'dashboard help renders extended POD help' );

    my $version = _run_shell( 'dashboard version', 'dashboard version' );
    _assert_match( $version->{stdout}, qr/^\Q$expected_version\E$/m, 'dashboard version reports the installed runtime version' );

integration/blank-env/run-integration.pl  view on Meta::CPAN

    _assert_match( $collector_inspect->{stdout}, qr/"job"|"status"|"output"/, 'dashboard collector inspect returns combined view' );

    my $collector_start = _run_shell( 'dashboard collector start', $project_cd . 'dashboard collector start fake.config.collector' );
    _assert_match( $collector_start->{stdout}, qr/\d+/, 'dashboard collector start returns a pid' );

    sleep 2;

    my $collector_restart = _run_shell( 'dashboard collector restart', $project_cd . 'dashboard collector restart fake.config.collector' );
    _assert_match( $collector_restart->{stdout}, qr/\d+/, 'dashboard collector restart returns a pid' );

    my $collector_stop = _run_shell( 'dashboard collector stop', $project_cd . 'dashboard collector stop fake.config.collector' );
    _assert_match( $collector_stop->{stdout}, qr/\d+/, 'dashboard collector stop returns the stopped pid' );

    my $collector_log = _run_shell( 'dashboard collector log', $project_cd . 'dashboard collector log' );
    _assert( defined $collector_log->{stdout}, 'dashboard collector log returns log text' );

    my $auth_add = _run_shell( 'dashboard auth add-user', $project_cd . q{dashboard auth add-user explicit_helper explicit-pass-123} );
    _assert_match( $auth_add->{stdout}, qr/"username"\s*:\s*"explicit_helper"/, 'dashboard auth add-user creates helper user' );

    my $auth_list = _run_shell( 'dashboard auth list-users', $project_cd . 'dashboard auth list-users' );
    _assert_match( $auth_list->{stdout}, qr/"explicit_helper"/, 'dashboard auth list-users includes explicit helper' );

    my $auth_remove = _run_shell( 'dashboard auth remove-user', $project_cd . 'dashboard auth remove-user explicit_helper' );
    _assert_match( $auth_remove->{stdout}, qr/"removed"\s*:\s*"explicit_helper"/, 'dashboard auth remove-user removes explicit helper' );

    my $docker_dry = _run_shell(
        'dashboard docker compose --dry-run',
        'dashboard docker compose --project ' . _shell_quote($compose) . ' --dry-run config'
    );
    _assert_match( $docker_dry->{stdout}, qr/"command"\s*:/, 'dashboard docker compose dry-run returns resolved command' );
    _assert_match( $docker_dry->{stdout}, qr/compose\.yaml/, 'dashboard docker compose dry-run includes compose file' );

    my $serve = _run_shell( 'dashboard serve', $project_cd . 'dashboard serve' );
    _assert_match( $serve->{stdout}, qr/"pid"\s*:/, 'dashboard serve starts background web service' );
    _wait_for_http( 'http://127.0.0.1:7890/', 200 );

    my $blocked_transient = _run_shell(
        'curl transient token denied by default',
        'curl -sS -o /tmp/transient-denied.body -w \'%{http_code}\' ' . _shell_quote( 'http://127.0.0.1:7890/?token=' . $token ),
    );
    _assert_match( $blocked_transient->{stdout}, qr/^403$/, 'loopback transient token route is denied by default' );
    _assert_match( _read_text('/tmp/transient-denied.body'), qr/Transient token URLs are disabled/, 'loopback transient token denial explains the policy' );

    my $root = _run_shell( 'curl loopback root', q{curl -fsS http://127.0.0.1:7890/} );
    _assert_match( $root->{stdout}, qr/editor-blocks/, 'loopback root serves the split-block bookmark editor' );
    my $root_dom = _run_browser_dom( 'browser loopback root', 'http://127.0.0.1:7890/', user_data_dir => $profile );
    _assert_match( $root_dom, qr/editor-blocks/, 'browser loopback root renders the split-block editor DOM' );
    _assert_match( $root_dom, qr/editor-blocks/, 'browser loopback root renders the split-block editor container' );
    _assert_match( $root_dom, qr/instruction-block-editor/, 'browser loopback root renders split editor textarea blocks' );
    _assert_match( $root_dom, qr/ddLoadBlocks\(ddSource\.value\);/, 'browser loopback root boots the split-block editor from the canonical hidden source' );

    my $project_dom = _run_browser_dom( 'browser fake project page', 'http://127.0.0.1:7890/app/project-home', user_data_dir => $profile );
    _assert_match( $project_dom, qr/project-marker/, 'browser renders fake project bookmark page' );
    _assert_match( $project_dom, qr/Fake Project Home/, 'browser renders fake project bookmark content' );
    _assert_match( $project_dom, qr/dashboard-nav-items/, 'browser renders shared nav container on fake project bookmark page' );
    _assert_match( $project_dom, qr/Alpha Nav Current/, 'browser renders shared nav TT output against the outer page path' );
    _assert_match( $project_dom, qr{<div id="nav-beta">/app/project-home / /app/project-home</div>}, 'browser exposes current_page through env and env.runtime_context for shared nav TT fragments' );
    _assert( index( $project_dom, 'nav-alpha' ) < index( $project_dom, 'nav-beta' ), 'browser renders shared nav bookmark fragments in sorted filename order' );
    _assert( index( $project_dom, 'dashboard-nav-items' ) < index( $project_dom, 'project-marker' ), 'browser renders shared nav fragments before the main page body' );
    my $legacy_ajax_page = _run_shell( 'curl legacy ajax saved page', q{curl -fsS http://127.0.0.1:7890/app/legacy-ajax} );
    _assert_match( $legacy_ajax_page->{stdout}, qr{/ajax/project-endpoint\.json\?type=text}, 'saved bookmark Ajax renders a stable file-backed ajax endpoint by default' );
    my $legacy_ajax_saved = _run_shell( 'curl saved bookmark ajax endpoint', q{curl -fsS 'http://127.0.0.1:7890/ajax/project-endpoint.json?type=text'} );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-start/, 'saved bookmark ajax endpoint streams direct perl stdout' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-warn/, 'saved bookmark ajax endpoint streams perl stderr warnings' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-child-out/, 'saved bookmark ajax endpoint streams child stdout' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-child-err/, 'saved bookmark ajax endpoint streams child stderr' );
    _assert_match( $legacy_ajax_saved->{stdout}, qr/saved-die/, 'saved bookmark ajax endpoint streams uncaught perl die output' );
    my $legacy_ajax_stream_page = _run_shell( 'curl legacy ajax stream saved page', q{curl -fsS http://127.0.0.1:7890/app/legacy-ajax-stream} );
    _assert_match( $legacy_ajax_stream_page->{stdout}, qr{/ajax/project-stream\.txt\?type=text}, 'saved bookmark ajax stream page renders a stable default text ajax endpoint' );
    my $legacy_ajax_stream = _capture_stream_prefix(
        'curl saved bookmark ajax stream endpoint',
        q{curl --no-buffer -fsS 'http://127.0.0.1:7890/ajax/project-stream.txt'},
        expected_chunks => [ 'stream1', 'stream2' ],
        timeout         => 4,
    );
    _assert( @{ $legacy_ajax_stream->{events} || [] } >= 2, 'saved bookmark ajax stream endpoint produced multiple early chunks before process exit' );
    _assert( ( $legacy_ajax_stream->{events}[0]{at} || 99 ) < 1.5, 'saved bookmark ajax stream endpoint flushes the first chunk before the long-running ajax loop finishes' );
    _assert( ( $legacy_ajax_stream->{events}[1]{at} || 99 ) < 2.5, 'saved bookmark ajax stream endpoint keeps flushing later chunks during the long-running ajax loop' );

    my $container_ip = _trim( _run_shell( 'container ip', q{hostname -I | awk '{print $1}'} )->{stdout} );
    _assert( $container_ip ne '', 'container ip discovered for helper-access path' );

    my $helper_root_disabled = _run_shell(
        'curl helper root before helper user exists',
        'curl -sS -o /tmp/helper-root.html -w \'%{http_code}\' http://' . $container_ip . ':7890/'
    );
    _assert_match( $helper_root_disabled->{stdout}, qr/^401$/, 'non-loopback self-access stays unauthorized before any helper user exists' );
    _assert( _read_text('/tmp/helper-root.html') eq q{}, 'outsider bootstrap response keeps the body empty before any helper user exists' );
    _assert( _read_text('/tmp/helper-root.html') !~ /<form[^>]*action="\/login"/, 'outsider bootstrap response does not expose the login form before any helper user exists' );
    my $helper_disabled_dom = _run_browser_dom( 'browser helper root before helper user exists', "http://$container_ip:7890/", user_data_dir => $profile );
    _assert_match( $helper_disabled_dom, qr/HTTP ERROR 401/, 'browser outsider bootstrap response resolves to a generic 401 browser error page before any helper user exists' );
    _assert( $helper_disabled_dom !~ /Helper access is disabled until a helper user is added\./, 'browser outsider bootstrap response does not leak helper bootstrap guidance before any helper user exists' );
    _assert( $helper_disabled_dom !~ /action="\/login"/, 'browser outsider bootstrap response omits the login form before any helper user exists' );

    _run_shell( 'dashboard auth add helper-login user', $project_cd . q{dashboard auth add-user helper_login helper-login-pass-123} );
    my $helper_root = _run_shell(
        'curl helper root after helper user exists',
        'curl -sS -o /tmp/helper-root-after-enable.html -w \'%{http_code}\' http://' . $container_ip . ':7890/'
    );
    _assert_match( $helper_root->{stdout}, qr/^401$/, 'non-loopback self-access returns helper login after a helper user exists' );
    _assert_match( _read_text('/tmp/helper-root-after-enable.html'), qr/<form[^>]*action="\/login"/, 'helper root serves login page after a helper user exists' );
    my $helper_dom = _run_browser_dom( 'browser helper root after helper user exists', "http://$container_ip:7890/", user_data_dir => $profile );
    _assert_match( $helper_dom, qr/action="\/login"/, 'browser helper root renders login form after a helper user exists' );

    my $login = _run_shell(
        'helper login',
        'curl -sS -c ' . _shell_quote($cookie) .
          ' -o /tmp/helper-login.body -D /tmp/helper-login.headers -d ' .
          _shell_quote('username=helper_login&password=helper-login-pass-123') .
          ' http://' . $container_ip . ':7890/login'
    );
    _assert( $login->{exit_code} == 0, 'helper login request completed' );
    _assert_match( _read_text('/tmp/helper-login.headers'), qr/^HTTP\/1\.1 302/m, 'helper login redirects after success' );

    my $helper_page = _run_shell(
        'helper page after login',
        'curl -fsS -b ' . _shell_quote($cookie) . ' http://' . $container_ip . ':7890/'
    );
    _assert_match( $helper_page->{stdout}, qr/id="logout-url"/, 'helper page chrome renders logout link' );

    my $helper_logout = _run_shell(
        'helper logout',
        'curl -sS -b ' . _shell_quote($cookie) . ' -o /tmp/helper-logout.body -D /tmp/helper-logout.headers http://' . $container_ip . ':7890/logout'
    );
    _assert( $helper_logout->{exit_code} == 0, 'helper logout request completed' );
    _assert_match( _read_text('/tmp/helper-logout.headers'), qr/^HTTP\/1\.1 302/m, 'helper logout redirects to login' );

    my $post_logout_users = _run_shell( 'dashboard auth list-users after logout', $project_cd . 'dashboard auth list-users' );
    _assert( $post_logout_users->{stdout} !~ /helper_login/, 'helper logout removes helper account from auth store' );

    my $restart = _run_shell( 'dashboard restart', $project_cd . 'dashboard restart -o json' );
    _assert_match( $restart->{stdout}, qr/"web_pid"\s*:/, 'dashboard restart returns structured lifecycle data' );
    _wait_for_http( 'http://127.0.0.1:7890/', 200 );

    sleep 2;

    my $restart_collectors = _run_shell( 'dashboard collector list after restart', $project_cd . 'dashboard collector list' );
    my $restart_collectors_data = decode_json( $restart_collectors->{stdout} );



( run in 0.488 second using v1.01-cache-2.11-cpan-524268b4103 )