Developer-Dashboard

 view release on metacpan or  search on metacpan

t/101-web-app-coverage.t  view on Meta::CPAN

my $paths      = Developer::Dashboard::PathRegistry->new( home => $home );
my $files      = Developer::Dashboard::FileRegistry->new( paths => $paths );
my $store      = Developer::Dashboard::PageStore->new( paths => $paths );
my $config     = Developer::Dashboard::Config->new( files => $files, paths => $paths );
my $indicators = Developer::Dashboard::IndicatorStore->new( paths => $paths );
my $auth       = Developer::Dashboard::Auth->new( files => $files, paths => $paths );
my $sessions   = Developer::Dashboard::SessionStore->new( paths => $paths );
my $runtime    = Developer::Dashboard::PageRuntime->new( paths => $paths );
my $prompt     = Developer::Dashboard::Prompt->new( paths => $paths, indicators => $indicators );
my $actions    = Developer::Dashboard::ActionRunner->new( files => $files, paths => $paths );

my $app = Developer::Dashboard::Web::App->new(
    actions  => $actions,
    auth     => $auth,
    config   => $config,
    pages    => $store,
    prompt   => $prompt,
    runtime  => $runtime,
    sessions => $sessions,
);

sub H { return { host => '127.0.0.1' } }

sub drain {
    my ($body) = @_;
    return $body if ref($body) ne 'HASH' || ref( $body->{stream} ) ne 'CODE';
    my $out = '';
    $body->{stream}->( sub { $out .= $_[0] if defined $_[0] } );
    return $out;
}

sub wfile {
    my ( $path, $content, $mode ) = @_;
    make_path( ( File::Spec->splitpath($path) )[1] );
    open my $fh, '>', $path or die "write $path: $!";
    print {$fh} $content;
    close $fh or die "close $path: $!";
    chmod( $mode, $path ) or die "chmod $path: $!" if defined $mode;
    return 1;
}

# ---------------------------------------------------------------------------
# Build one installed skill (route-skill) plus a nested skill (def) directly on
# disk under the skills root so all skill-routing branches are reachable.
# ---------------------------------------------------------------------------
{
    my $skill = File::Spec->catdir( $paths->skills_root, 'route-skill' );
    wfile( File::Spec->catfile( $skill, 'config', 'config.json' ), qq|{"skill_name":"route-skill"}\n|, 0644 );
    wfile(
        File::Spec->catfile( $skill, 'dashboards', 'index' ),
        "TITLE: Skill Route Index\n:--------------------------------------------------------------------------------:\nBOOKMARK: index\n:--------------------------------------------------------------------------------:\nHTML:\nSkill Route Index\n",
        0644,
    );
    wfile(
        File::Spec->catfile( $skill, 'dashboards', 'foo' ),
        "TITLE: Skill Route Foo\n:--------------------------------------------------------------------------------:\nBOOKMARK: foo\n:--------------------------------------------------------------------------------:\nHTML:\nSkill Route Foo\n",
        0644,
    );
    wfile(
        File::Spec->catfile( $skill, 'dashboards', 'ajax-demo' ),
        "TITLE: Skill Ajax Demo\n:--------------------------------------------------------------------------------:\nBOOKMARK: ajax-demo\n:--------------------------------------------------------------------------------:\nHTML:\n<script>var endpoints...
        0644,
    );
    wfile( File::Spec->catfile( $skill, 'dashboards', 'ajax', 'bar' ), qq|print qq[{"route":"bar"}\\n];\n|, 0700 );
    wfile( File::Spec->catfile( $skill, 'dashboards', 'ajax', 'raw' ), qq|print qq[{"route":"raw"}\\n];\n|, 0700 );
    wfile( File::Spec->catfile( $skill, 'dashboards', 'public', 'js',     'skill.js' ),  qq|console.log("route-skill js");\n|, 0644 );
    wfile( File::Spec->catfile( $skill, 'dashboards', 'public', 'css',    'skill.css' ), qq|body { color: #123456; }\n|,        0644 );
    wfile( File::Spec->catfile( $skill, 'dashboards', 'public', 'others', 'info.txt' ),  "route-skill info\n",                  0644 );
    wfile( File::Spec->catfile( $skill, 'dashboards', 'nav', 'skill.tt' ), "<div>Skill Route Nav</div>\n", 0644 );
    wfile(
        File::Spec->catfile( $skill, 'config', 'routes.json' ),
        <<'JSON',
{
   "/apps/route-skill/home" : "/app/index",
   "/apps/route-skill/foo" : "/app/foo",
   "/v1/route-skill/bar" : "/ajax/bar",
   "/v1/route-skill/raw" : { "to" : "/ajax/raw", "type" : "application/vnd.route+json" },
   "/assets/route-skill.css" : "/css/skill.css",
   "/assets/route-skill.js" : "/js/skill.js",
   "/downloads/route-skill.txt" : { "to" : "/others/info.txt", "type" : "text/plain; charset=utf-8" }
}
JSON
        0644,
    );

    # nested skill "def"
    my $nested = File::Spec->catdir( $skill, 'skills', 'def' );
    wfile(
        File::Spec->catfile( $nested, 'dashboards', 'index' ),
        "TITLE: Nested Skill Index\n:--------------------------------------------------------------------------------:\nBOOKMARK: index\n:--------------------------------------------------------------------------------:\nHTML:\nNested Skill Index\n",
        0644,
    );
    wfile(
        File::Spec->catfile( $nested, 'dashboards', 'foo' ),
        "TITLE: Nested Skill Foo\n:--------------------------------------------------------------------------------:\nBOOKMARK: foo\n:--------------------------------------------------------------------------------:\nHTML:\nNested Skill Foo\n",
        0644,
    );
    wfile( File::Spec->catfile( $nested, 'dashboards', 'nav', 'index.tt' ), "<div>Nested Skill Nav</div>\n", 0644 );
    wfile( File::Spec->catfile( $nested, 'dashboards', 'ajax', 'nested' ), qq|print "<p>nested ajax</p>\\n";\n|, 0700 );
    wfile(
        File::Spec->catfile( $nested, 'config', 'routes.json' ),
        qq|{ "/apps/route-skill/child/foo" : "/app/foo" }\n|,
        0644,
    );
}

# ---------------------------------------------------------------------------
# Saved pages for the primary route surface.
# ---------------------------------------------------------------------------
$store->save_page(
    Developer::Dashboard::PageDocument->new(
        id     => 'welcome',
        title  => 'Welcome',
        layout => { body => 'hello [% stash.name %]' },
    )
);
$store->save_page(
    Developer::Dashboard::PageDocument->new(
        id      => 'acted',
        title   => 'Acted',
        layout  => { body => '<div>acted body</div>' },

t/101-web-app-coverage.t  view on Meta::CPAN

sub api_keys { return [] }

package Local::IndNoPayload;    # indicators object without page_header_payload

package Local::IndEmptyPayload;    # indicators whose payload lacks an array key
sub page_header_payload { return {} }

package Local::NotAStore;    # a pages object that is not a PageStore

package Local::DispProbe;
sub _load_skill_page {
    return Developer::Dashboard::PageDocument->new(
        id => 'route-skill', title => 'Skill Route Index', meta => { source_kind => 'skill' },
    );
}

package Local::DispProbeDie;
sub _load_skill_page { die "boom\n" }

package Local::StubActions;
our $MODE = 'body';

# run_page_action(%args)
# Returns a controllable result for Web::App action-response coverage.
# Input: ignored named arguments.
# Output: hash reference, or dies when $MODE is 'die'.
sub run_page_action {
    die "action failure\n" if $MODE eq 'die';
    return { body => 'b', content_type => 'text/x' } if $MODE eq 'body';
    return { body => 'b2' } if $MODE eq 'body-default';
    return { ok => 1 };
}

# run_encoded_action(%args)
# Mirrors run_page_action for encoded-action response coverage.
# Input: ignored named arguments.
# Output: hash reference, or dies when $MODE is 'die'.
sub run_encoded_action { return run_page_action() }

# encode_action_payload(%args)
# Returns a stable fake action token for render-link coverage.
# Input: ignored named arguments.
# Output: token string.
sub encode_action_payload { return 'atok' }

package main;

__END__

=pod

=head1 NAME

t/101-web-app-coverage.t - branch and condition coverage closure for the web app backend

=head1 PURPOSE

This test is the executable coverage-closure contract for
C<Developer::Dashboard::Web::App>. It drives the browser-facing route table,
helper login, transient-token policy, saved-page render/source/edit/action
flows, saved and skill-local Ajax endpoints, static asset serving, and the
defensive fallback paths so every Devel::Cover branch and condition in the
module is exercised on the Linux test host.

=head1 WHY IT EXISTS

The web app backend is large and security-sensitive, and its happy paths were
already covered by the broader suite while the defensive sides (missing config,
absent headers, empty option defaults, error responses, and machine-auth edge
cases) were not. This file closes those remaining branch and condition sides so
the all-metric coverage gate stays honest for this module.

=head1 WHEN TO USE

Use this file when changing browser routes, auth gating, saved or transient page
handling, Ajax endpoints, skill route resolution, or the static file serving in
the web app backend.

=head1 HOW TO USE

Run C<prove -lv t/101-web-app-coverage.t> while iterating, then keep it green
under C<prove -lr t> and the Devel::Cover gate before release.

=head1 WHAT USES IT

Developers during TDD, the repository test suite, and the coverage gate use this
file to keep the web app backend's branch and condition coverage complete.

=head1 EXAMPLES

Example 1:

  prove -lv t/101-web-app-coverage.t

Run the focused coverage-closure test by itself.

Example 2:

  prove -lr t

Run it inside the full repository suite before release.

=cut



( run in 1.093 second using v1.01-cache-2.11-cpan-9789f410c06 )