PAGI-Tools

 view release on metacpan or  search on metacpan

t/app/02-routing.t  view on Meta::CPAN

        $urlmap->mount('/api' => $inner);
        my $app = $urlmap->to_app;

        run_async(async sub {
            await $app->(
                { type => 'http', path => '/api/users/123' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; },
            );
        });

        is $received_path, '/users/123', 'path adjusted (prefix removed)';
    };

    subtest 'returns 404 for unmatched path' => sub {
        my $urlmap = PAGI::App::URLMap->new;
        $urlmap->mount('/api' => make_response_app(200, 'API'));
        my $app = $urlmap->to_app;

        my @sent;
        run_async(async sub {
            await $app->(
                { type => 'http', path => '/unknown' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; push @sent, $event },
            );
        });

        is $sent[0]{status}, 404, 'returns 404';
    };

    subtest 'longest prefix wins' => sub {
        my $urlmap = PAGI::App::URLMap->new;
        $urlmap->mount('/api' => make_response_app(200, 'API'));
        $urlmap->mount('/api/v2' => make_response_app(200, 'API-V2'));
        my $app = $urlmap->to_app;

        my @sent;
        run_async(async sub {
            await $app->(
                { type => 'http', path => '/api/v2/users' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; push @sent, $event },
            );
        });

        is $sent[1]{body}, 'API-V2', 'longer prefix matched';
    };
};

# =============================================================================
# Test: PAGI::App::Cascade
# =============================================================================

subtest 'App::Cascade tries apps in sequence' => sub {

    subtest 'returns first non-404 response' => sub {
        my $cascade = PAGI::App::Cascade->new(
            apps => [
                make_response_app(404, 'Not Found'),
                make_response_app(200, 'Found'),
                make_response_app(200, 'Never Reached'),
            ],
        );
        my $app = $cascade->to_app;

        my @sent;
        run_async(async sub {
            await $app->(
                { type => 'http', path => '/test' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; push @sent, $event },
            );
        });

        is $sent[0]{status}, 200, 'returns 200';
        is $sent[1]{body}, 'Found', 'correct app responded';
    };

    subtest 'returns 404 if all apps return 404' => sub {
        my $cascade = PAGI::App::Cascade->new(
            apps => [
                make_response_app(404, 'Not Found 1'),
                make_response_app(404, 'Not Found 2'),
            ],
        );
        my $app = $cascade->to_app;

        my @sent;
        run_async(async sub {
            await $app->(
                { type => 'http', path => '/test' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; push @sent, $event },
            );
        });

        is $sent[0]{status}, 404, 'returns 404 when all fail';
    };

    subtest 'custom catch codes' => sub {
        my $cascade = PAGI::App::Cascade->new(
            apps => [
                make_response_app(403, 'Forbidden'),
                make_response_app(200, 'Success'),
            ],
            catch => [403, 404],
        );
        my $app = $cascade->to_app;

        my @sent;
        run_async(async sub {
            await $app->(
                { type => 'http', path => '/test' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; push @sent, $event },
            );
        });

        is $sent[0]{status}, 200, 'catches 403 and tries next';
    };
};

# =============================================================================
# Test: PAGI::App::NotFound
# =============================================================================

subtest 'App::NotFound returns 404' => sub {

    subtest 'default 404 response' => sub {
        my $app = PAGI::App::NotFound->new->to_app;

        my @sent;
        run_async(async sub {
            await $app->(
                { type => 'http', path => '/anything' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; push @sent, $event },
            );
        });

        is $sent[0]{status}, 404, 'returns 404';
        like $sent[1]{body}, qr/Not Found/i, 'default body';
    };

    subtest 'custom body' => sub {
        my $app = PAGI::App::NotFound->new(body => 'Custom 404')->to_app;

        my @sent;
        run_async(async sub {
            await $app->(
                { type => 'http', path => '/anything' },
                async sub { { type => 'http.disconnect' } },
                async sub  {
        my ($event) = @_; push @sent, $event },
            );
        });

        is $sent[1]{body}, 'Custom 404', 'custom body';
    };
};

# =============================================================================
# Test: PAGI::App::Redirect
# =============================================================================

subtest 'App::Redirect returns redirects' => sub {

t/app/02-routing.t  view on Meta::CPAN

        await $send->({
            type    => 'http.response.start',
            status  => 200,
            headers => [['content-type', 'text/plain']],
        });
        await $send->({ type => 'http.response.body', body => 'ok', more => 0 });
    };

    my $map = PAGI::App::URLMap->new;
    $map->mount('/api' => $inner);
    my $app = $map->to_app;

    my @sent;
    my $send = sub { my ($msg) = @_; push @sent, $msg; Future->done };
    $app->({ type => 'http', method => 'GET', path => '/api/users' },
        sub { Future->done }, $send)->get;

    is $coercion_calls[0]{root_path}, '/api', 'root_path set to mount prefix';
    is $coercion_calls[0]{path}, '/users', 'path has prefix stripped';
    ok !exists $coercion_calls[0]{script_name}, 'off-spec script_name key is gone';

    @sent = ();
    @coercion_calls = ();
    $app->({ type => 'http', method => 'GET', path => '/api/users', root_path => '/outer' },
        sub { Future->done }, $send)->get;
    is $coercion_calls[0]{root_path}, '/outer/api', 'root_path appends to existing (nested mounts)';
};

subtest 'URLMap coerces components, class names, and default' => sub {
    require TestApps::Component;

    my $map = PAGI::App::URLMap->new(
        default => TestApps::Component->new(body => 'fallback'),
    );
    $map->mount('/c' => TestApps::Component->new(body => 'mounted'));
    $map->mount('/s' => 'TestApps::Component');
    my $app = $map->to_app;

    my @sent;
    my $send = sub { my ($msg) = @_; push @sent, $msg; Future->done };

    $app->({ type => 'http', method => 'GET', path => '/c/x' },
        sub { Future->done }, $send)->get;
    is $sent[1]{body}, 'mounted', 'component object mounted directly';

    @sent = ();
    $app->({ type => 'http', method => 'GET', path => '/s/x' },
        sub { Future->done }, $send)->get;
    is $sent[1]{body}, 'component', 'class name mounted directly';

    @sent = ();
    $app->({ type => 'http', method => 'GET', path => '/nomatch' },
        sub { Future->done }, $send)->get;
    is $sent[1]{body}, 'fallback', 'default coerced too';
};

subtest 'Cascade coerces apps list and add()' => sub {
    require TestApps::Component;
    require PAGI::App::NotFound;

    my $cascade = PAGI::App::Cascade->new(
        apps => [ PAGI::App::NotFound->new ],   # always 404s -> falls through
    );
    $cascade->add(TestApps::Component->new(body => 'second'));
    my $app = $cascade->to_app;

    my @sent;
    my $send = sub { my ($msg) = @_; push @sent, $msg; Future->done };
    $app->({ type => 'http', method => 'GET', path => '/x' },
        sub { Future->done }, $send)->get;

    is $sent[0]{status}, 200, 'fell through the 404 component';
    is $sent[1]{body}, 'second', 'component object added without ->to_app';
};

subtest 'Redirect builds a response value (static + dynamic + query)' => sub {
    require PAGI::App::Redirect;

    my $run = sub {
        my ($app, $scope) = @_;
        my @sent;
        my $send = sub { my ($m) = @_; push @sent, $m; Future->done };
        $app->($scope, sub { Future->done }, $send)->get;
        return \@sent;
    };

    my $sent = $run->(
        PAGI::App::Redirect->new(to => '/new', status => 301)->to_app,
        { type => 'http', method => 'GET', path => '/old' },
    );
    is $sent->[0]{status}, 301, 'status';
    my %h = map { lc($_->[0]) => $_->[1] } @{$sent->[0]{headers}};
    is $h{location}, '/new', 'location';
    is $h{'content-type'}, 'text/plain', 'content-type preserved';
    is $h{'content-length'}, 0, 'content-length 0';
    is $sent->[1]{body}, '', 'empty body';

    $sent = $run->(
        PAGI::App::Redirect->new(to => sub { my ($s) = @_; "/from$s->{path}" })->to_app,
        { type => 'http', method => 'GET', path => '/p', query_string => 'a=1' },
    );
    %h = map { lc($_->[0]) => $_->[1] } @{$sent->[0]{headers}};
    is $h{location}, '/from/p?a=1', 'coderef receives scope; query preserved';
    is $sent->[0]{status}, 302, 'default status';

    $sent = $run->(
        PAGI::App::Redirect->new(to => '/x', preserve_query => 0)->to_app,
        { type => 'http', method => 'GET', path => '/y', query_string => 'a=1' },
    );
    %h = map { lc($_->[0]) => $_->[1] } @{$sent->[0]{headers}};
    is $h{location}, '/x', 'preserve_query => 0 suppresses query append';
};

subtest 'NotFound builds a response value (static + coderef body)' => sub {
    require PAGI::App::NotFound;

    my $run = sub {
        my ($app, $scope) = @_;
        my @sent;
        my $send = sub { my ($m) = @_; push @sent, $m; Future->done };
        $app->($scope, sub { Future->done }, $send)->get;
        return \@sent;
    };

    # defaults



( run in 1.504 second using v1.01-cache-2.11-cpan-9581c071862 )