Group
Extension

Test-RestAPI/lib/Test/RestAPI.pm

package Test::RestAPI;
use Moo;

our $VERSION = '0.1.6';

use Types::Standard qw(ArrayRef InstanceOf Int Str);
use Test::RestAPI::Endpoint qw(convert_path_to_filename);
use Test::RestAPI::MojoGenerator;
use Port::Selector;
use Path::Tiny;
use Mojo::JSON qw(decode_json);
use Mojo::UserAgent;

use constant WINDOWS => ($^O eq 'MSWin32');

BEGIN {
    if (WINDOWS) {
        ## no critic (ProhibitStringyEval)
        eval q{
            use Win32::Process qw(NORMAL_PRIORITY_CLASS);
        };

        die $@ if $@;
    }
}

=head1 NAME

Test::RestAPI - Real mock of REST API

=head1 SYNOPSIS

    my $api = Test::RestAPI->new(
        endpoints => [
            Test::RestAPI::Endpoint->new(
                path => '/a',
                method   => 'any',
            )
        ],
    );

    $api->start();

    HTTP::Tiny->new->get($api->uri.'/test');


=head1 DESCRIPTION

In many (test) case you need mock some REST API. One way is mock your REST-API class abstraction or HTTP client.
This module provides other way - start generated L<Mojolicious> server and provides pseudo-real your defined API.

=head1 METHODS

=head2 new(%attribute)

=head3 %attribute

=head4 endpoints

I<ArrayRef> of instances L<Test::RestAPI::Endpoint>

default is I</> (root) 200 OK - hello:

    Test::RestAPI::Endpoint->new(
        path   => '/',
        method => 'any',
        render => {text => 'Hello'},
    );

=cut
has 'endpoints' => (
    is  => 'ro',
    isa => ArrayRef [ InstanceOf ['Test::RestAPI::Endpoint'] ],
    default => sub {
        return [
            Test::RestAPI::Endpoint->new(
                path   => '/',
                method => 'any',
                render => {text => 'Hello'},
            )
        ];
    }
);

=head4 mojo_app_generator

This attribute is used for generating mojo application.

default is L<Test::RestAPI::MojoGenerator>

=cut
has 'mojo_app_generator' => (
    is      => 'ro',
    isa     => InstanceOf ['Test::RestAPI::MojoGenerator'],
    default => sub {
        return Test::RestAPI::MojoGenerator->new();
    }
);

has 'pid' => (
    is  => 'rw',
    isa => Int,
);

has 'uri' => (
    is  => 'rw',
    isa => Str,
);

has 'mojo_home' => (
    is      => 'ro',
    default => sub {
        my $mojo_home = Path::Tiny->tempdir();

        path($mojo_home, 'log')->mkpath();

        return $mojo_home;
    }
);  

=head3 start

Start REST API (L<Mojolicious>) application on some random unused port
and wait to initialize.

For start new process is used C<fork-exec> on non-windows machines and L<Win32::Process> for windows machines.

For generating L<Mojolicious> application is used L<Test::RestAPI::MojoGenerator> in C<mojo_app_generator> attribute - is possible set own generator.

=cut
sub start {
    my ($self) = @_;

    my $app_path = $self->mojo_app_generator->create_app($self->endpoints);

    $self->pid($self->_start($app_path));
}

sub _start {
    my ($self, $app_path) = @_;

    $self->_create_uri();

    my $pid;
    if (WINDOWS) {
        $pid = $self->_start_win($app_path);
    }
    else {
        $pid = $self->_start_fork($app_path);
    }

    $self->_wait_to_start();

    return $pid;
}

sub _create_uri {
    my ($self) = @_;

    my $port = Port::Selector->new->port();

    $self->uri("http://localhost:$port");
}

sub _start_win {
    my ($self, $app_path) = @_;

    #This trick is copied from IPC::System::Simple
    #If is check in this sub to non-Win32 system,
    #perl don't check NORMAL_PRIORITY_CLASS constant in compilation phase.
    if (!WINDOWS) {
        die '_start_win ca be called only anna Windows';
    }
    else {
        my $args = 'perl '.$app_path->canonpath().' '.join ' ', $self->_mojo_args();

        Win32::Process::Create(
            my $proc,
            $^X,
            $args,
            0,
            NORMAL_PRIORITY_CLASS,
            "."
        ) || die "Process $args start fail $^E";

        return $proc->GetProcessID();
    }
}

sub _start_fork {
    my ($self, $app_path) = @_;

    my @args = ($^X, $app_path->stringify, $self->_mojo_args());

    my $pid = fork;

    if ($pid) {
        return $pid
    }
    elsif ($pid == 0) {
        exec {$args[0]} @args;
        exit 1;
    }
    else {
        die "Fork problem: $!";
    }
}

sub _mojo_args {
    my ($self) = @_;

    return ('daemon', '-l', $self->uri, '-m', 'production', '--home', $self->mojo_home->canonpath());
}

sub _wait_to_start {
    my ($self) = @_;

    while (1) {
        if (Mojo::UserAgent->new->get($self->uri.'/app_mojo_healtcheck')->res->body() eq 'OK') {
            return 1;
        }
        sleep 1;
    }
}

=head2 count_of_requests($path)

return count of request to C<$path> endpoint

=cut
sub count_of_requests {
    my ($self, $path) = @_;

    $path = '/' if !defined $path;

    my $fh = path($self->mojo_home, convert_path_to_filename($path))->filehandle();

    my $lines = 0;
    while (<$fh>) {
        $lines++;
    }

    return $lines;
}

=head2 list_of_requests_body($path)

return list (ArrayRef) of requests body to C<$path> endpoint

=cut
sub list_of_requests_body {
    my ($self, $path) = @_;

    $path = '/' if !defined $path;

    my $fh = path($self->mojo_home, convert_path_to_filename($path))->filehandle();

    my @lines;
    while (my $line = <$fh>) {
        chomp $line;

        push @lines, decode_json($line);
    }

    return \@lines;
}


sub DESTROY {
    my ($self) = @_;

    if ($^O eq 'MSWin32') {
        Win32::Process::KillProcess($self->pid, 0);
    }
    else {
        kill 'SIGTERM', $self->pid;
    }
}

=head1 LICENSE

Copyright (C) Avast Software.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 AUTHOR

Jan Seidl E<lt>seidl@avast.comE<gt>

=cut

1;


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.