The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
use strict;
use warnings;
use Test::More tests => 67;
use lib 't/';

# Some special helpers to restove the environment
my %ORIG_ENV = %ENV;
sub restoreENV { %ENV = %ORIG_ENV }

# TEST: use
BEGIN {
    use_ok('REST::Application::Routes');
    $SIG{'__WARN__'} = sub { &Carp::croak };
}

# TEST: new()
{
    my $rest = REST::Application::Routes->new();
    isa_ok($rest, 'REST::Application::Routes', "Object instantiation.");
    isa_ok($rest, 'REST::Application', "Object instantiation.");
}

# TEST: query()
{
    my $rest = REST::Application::Routes->new();
    my $query = $rest->defaultQueryObject();
    is(ref($query), 'CGI', "Retrieving default query object.");
}

# TEST: query()
{
    my $rest = REST::Application::Routes->new();
    my $query = $rest->query();
    is(ref($query), 'CGI', "Retrieving query object.");
}

# TEST: query($value)
{
    my $rest = REST::Application::Routes->new();
    my $query = $rest->query("x/a/b");
    is($query, 'x/a/b', "Setting and retrieving a query object.");
}

# TEST: query(undef)
{
    my $rest = REST::Application::Routes->new();
    my $query = $rest->query(undef);
    is($query, undef, "Setting and retrieving a query object w/ undef.");
}

# TEST: defaultQueryObject($value)
{
    my $rest = REST::Application::Routes->new();
    my $query = $rest->defaultQueryObject("xxx");
    is($query, 'xxx', "Setting and retrieving default query object.");
}

# TEST: defaultQueryObject(undef)
{
    my $rest = REST::Application::Routes->new();
    my $query = $rest->defaultQueryObject(undef);
    is($query, undef, "Setting and retrieving default query object w/ undef value.");
}

# TEST: resourceHooks()
{
    my $rest = REST::Application::Routes->new();
    my $resources = $rest->resourceHooks();
    is_deeply($resources, {}, "Getting resource hooks when none are set.");
}

# TEST: resourceHooks()
{
    my $rest = REST::Application::Routes->new();
    my $sub = sub {};
    my $template = '/var/foo/bar/baz';
    my $resources = $rest->resourceHooks($template => $sub);
    isa_ok($resources, 'HASH', "Resource hook using a code ref (data type check)");
    is($resources->{$template}, $sub, "Resource hook using a code ref (value check)");
}

# TEST: resourceHooks()
{
    my $rest = REST::Application::Routes->new();
    my %uniq;
    my @keys = map { int(rand(100000)); } (1 .. 10000);
    @uniq{@keys} = 1;
    @keys = keys(%uniq);  # Make sure we have no duplicate keys
    my @k2 = @keys;
    my $resources = $rest->resourceHooks(map { $_ => "x" } @keys);
    my $keys = [ keys %$resources ];
    is_deeply(\@k2, \@keys, "Resource hook regexes have their order preserved.");
}

# TEST: resourceHooks()
{
    my $rest = REST::Application::Routes->new();
    my $resources = $rest->resourceHooks({ foo => 1 });
    is_deeply($resources, {foo => 1}, "Resource hook set from a hash ref");
}

# TEST: resourceHooks()
{
    my $rest = REST::Application::Routes->new();
    $rest->resourceHooks(foo => 1);
    my $resources = $rest->resourceHooks();
    is_deeply($resources, {foo => 1}, "Resource hook set and retrieved in 2 steps.");
}

# TEST: getPathInfo()
{
    require_ok('CGI');
    restoreENV();
    CGI->initialize_globals();
    $ENV{PATH_INFO} = "/blah/bar";
    my $rest = REST::Application::Routes->new();
    is_deeply($rest->getPathInfo(), "/blah/bar", "Retrieving path info.");
}

# TEST: getRequestMethod()
{
    restoreENV();
    CGI->initialize_globals();
    $ENV{REQUEST_METHOD} = "PuT";
    my $rest = REST::Application::Routes->new();
    is_deeply($rest->getRequestMethod(), "PUT", "Retrieving request method.");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{PATH_INFO} = "/parts/12345/foo";
    $rest->resourceHooks('/parts/:nums/:var' =>
                            sub { ref($_[0]) . $_[1]->{nums}.$_[1]->{var} });
    is(${$rest->loadResource()}, "REST::Application::Routes12345foo", "Loading resource - code reference");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{PATH_INFO} = "/parts/12345/foo";
    $rest->resourceHooks('/parts/:nums/:var' => undef);
    is(${$rest->loadResource()}, undef, "Loading resource - default hook via undef");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{PATH_INFO} = "/NOEXIST/12345.xml";
    $rest->resourceHooks('/parts/:nums/:var' => sub {1});
    is(${$rest->loadResource()}, undef, "Loading resource - default hook via non-match");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    use_ok('RoutesTestClass');
    my $rest = RoutesTestClass->new();
    $rest->resourceHooks(parts =>  "barMethod");
    my $resource = $rest->loadResource("parts", "blah", "bar", "baz");
    is($$resource, 'blah:bar:baz', "Loading resource - \"methodName\" hook");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $rest->resourceHooks('/parts/:var/:foo' =>  [$rest, "getPathInfo"]);
    my $resource = $rest->loadResource();
    is($$resource, '/parts/12345/cows', "Loading resource - [\$object w/ \"methodName\"] hook");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "GET";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $rest->resourceHooks('/parts/:var/:foo' => {
                            get => sub { "x" },
                            puT => [$rest, "getPathInfo"],
                            POST => "getPathInfo",
                            'deLEte' =>  undef,
                         });
    my $resource = $rest->loadResource();
    is($$resource, 'x', "Loading resource for GET HTTP method.");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "PuT";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $rest->resourceHooks('/parts/:var/:foo' => {
                            get => sub { "x" },
                            puT => [$rest, "getPathInfo"],
                            POST => "getPathInfo",
                            'deLEte' =>  undef,
                         });
    my $resource = $rest->loadResource();
    is($$resource, '/parts/12345/cows', "Loading resource for PUT HTTP method.");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "PoSt";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $rest->resourceHooks('/parts/:var/:foo' => {
                            get => sub { "x" },
                            puT => [$rest, "getPathInfo"],
                            POST => "getPathInfo",
                            'deLEte' =>  undef,
                         });
    my $resource = $rest->loadResource();
    is($$resource, '/parts/12345/cows', "Loading resource for POST HTTP method.");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "delete";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $rest->resourceHooks('/parts/:var/:foo' => {
                            get => sub { "x" },
                            puT => [$rest, "getPathInfo"],
                            POST => "getPathInfo",
                            'deLEte' =>  undef,
                         });
    my $resource = $rest->loadResource();
    is($$resource, undef, "Loading resource for DELETE HTTP method.");
}

# TEST: loadResource()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "delete";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    my $obj = RoutesTestClass->new();
    $rest->resourceHooks('/parts/:var/:nam' => $obj);
    my $resource = $rest->loadResource();
    is($$resource, "xAbC", "Loading resource - with \$object->getResource() hook");
}

# TEST: headerType
{
    my $rest = REST::Application::Routes->new();
    is($rest->headerType(), 'header', "Retrieving default header type");
    $rest->headerType("redIRect");
    is($rest->headerType(), 'redirect', "Setting header type to \"redirect\".");
    $rest->headerType("nOne");
    is($rest->headerType(), 'none', "Setting header type to \"none\".");
    eval { $rest->headerType("blahblahlbha") };
    ok($@, "Checking error for invalid header type");
}

# TEST: header
{
    my $rest = REST::Application::Routes->new();
    my %hash = $rest->header();
    is_deeply(\%hash, {}, "Retrieving default header values.");
    $rest->header(-type => 'text/html', -foobar => 5);
    %hash = $rest->header();
    is_deeply(\%hash, {-type => 'text/html', -foobar => 5}, "Retrieving custom header values.");
}

# TEST: resetHeader()
{
    my $rest = REST::Application::Routes->new();
    my %hash1 = $rest->header(-type => 'text/html', -foobar => 5);
    my %hash2 = $rest->resetHeader();
    my %hash3 = $rest->header();
    is_deeply(\%hash1, \%hash2, "Resetting header, verifying return value.");
    is_deeply(\%hash3, {}, "Resetting header, verifying reset.");
}

# TEST: run()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "GET";
    $ENV{PATH_INFO} = "/honda/hubcaps/12345";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub {
        my ($app, $hash) = @_;
        $app->header(-type => 'text/plain');
        return "hubcap - $hash->{id} - Honda";
    };
    $rest->resourceHooks('/honda/hubcaps/:id' => $resourceHook);
    my $output = $rest->run();
    my $answer = "Content-Type: text/plain; charset=ISO-8859-1\r\n\r\nhubcap - 12345 - Honda";
    is($output, $answer, "Running a REST Application");
}

# TEST: run()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "GET";
    $ENV{PATH_INFO} = "/honda/hubcaps/12345";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { "this is a test" };
    $rest->resourceHooks('/honda/hubcaps/:id' => $resourceHook);
    my $answer = "Content-Type: text/html; charset=ISO-8859-1\r\n\r\nthis is a test";
    is($rest->run(), $answer, "Running a REST Application which has a resource being its own repr.");
}

# TEST: addRepresentation
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    my $x = "hi";
    $rest->addRepresentation(" world", \$x);
    is($x, "hi world", "Adding representation w/ a string.");

    my $xx = "hi";
    my $y = " world";
    $rest->addRepresentation(\$y, \$xx);
    is ($xx, "hi world", "Adding representation w/ a scalar references.");
}

# TEST: getHeaders 
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $rest->header(-type => "text/xml", -foop => "helloWorld");
    like($rest->getHeaders(),
       qr{[Ff]oop: helloWorld\r\nContent-Type: text/xml; charset=ISO-8859-1\r\n\r\n},
       "Sending representation.");
}

# TEST: BUG: run() produced warnings when sendRepresentation() returned an
# undefined value.  This test should exploit that and fail if it happens.
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $ENV{REQUEST_METHOD} = "GET";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { return };
    $rest->resourceHooks('/parts/:num/:var' => $resourceHook);
    my $output = $rest->run();
    my $answer = "Content-Type: text/html; charset=ISO-8859-1\r\n\r\n";
    is($output, $answer, "Running a REST Application");
}

# TEST: preRun() and postRun()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{REQUEST_METHOD} = "GET";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { return "my resource" };
    $rest->resourceHooks('/parts/:num/:var' => $resourceHook);
    $rest->run();
    is($rest->{preRun}, 1, "preRun() method.");
    is($rest->{postRun}, "my resource", "postRun() method.");
}

# TEST: setRedirect()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = REST::Application::Routes->new();
    $rest->setRedirect("http://www.google.com");
    like($rest->getHeaders(), qr{^Status: 302 (Moved|Found)\r\n[lL]ocation: http://www\.google\.com\r\n\r\n$}, "Redirect header");
}

# TEST: getMatchText()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{REQUEST_METHOD} = "GET";
    $ENV{PATH_INFO} = "/parts/12345/cows";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    $rest->{TEST_TEXT} = "radio/is/friendly";  # See TestClass
    my $resourceHook = sub {
        my ($app, $h) = @_;
        return "$h->{bar} $h->{foo}";
    };
    $rest->resourceHooks('radio/:foo/:bar' => $resourceHook);
    my $output = $rest->run();
    is($output, "Content-Type: text/html; charset=ISO-8859-1\r\n\r\nfriendly is", "Using alternate matching text instead of PATH_INFO.");
}

# TEST: checkMatch()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{REQUEST_METHOD} = "GET";
    $ENV{PATH_INFO} = "/parts/12345.xml";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    $rest->{TEST_TEXT} = "Quis/hic/locus?";  # See TestClass
    $rest->resourceHooks( q(Quis/hic/locus?) => sub { return "I match" },
                          q(Quis) => sub { undef } );
    my $output = $rest->run();
    is($output, "Content-Type: text/html; charset=ISO-8859-1\r\n\r\nI match", "Using custom matching logic, checkMatch().");
}

# TEST: extraHandlerArgs()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { shift; shift; join(" ", @_) };
    $rest->{TEST_TEXT} = "foo";
    $rest->resourceHooks(q(foo) => $resourceHook);
    $rest->extraHandlerArgs(qw(hello jello world foo bar));
    my $output = $rest->run();
    is($output, "Content-Type: text/html; charset=ISO-8859-1\r\n\r\nhello jello world foo bar", "Setting arguments for the handler.");
}

# TEST: preHandler()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { shift; shift; join(" ", @_) };
    $rest->{TEST_PRE} = 1;
    $rest->resourceHooks(q(foo) => $resourceHook);
    $rest->extraHandlerArgs(qw(hello jello world foo bar));
    my $output = $rest->run();
    is($rest->{preHandler}, "hello:jello:world:foo:bar");
}

# TEST: postHandler()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{PATH_INFO} = "foo";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { shift; shift; return join(" ", @_) ; };
    $rest->{TEST_POST} = 1;
    $rest->resourceHooks(foo => $resourceHook);
    $rest->extraHandlerArgs(qw(hello jello world foo bar));
    my $output = $rest->run();
    is($rest->{postHandler}, "hello jello world foo barhello:jello:world:foo:bar");
}

# TEST: callHandler()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{PATH_INFO} = "foo";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { };
    $rest->{TEST_CALL} = 1;
    my $output = $rest->callHandler($resourceHook, {}, "a", "b", "c");
    is($output, "CODEa:b:c", "The handle caller w/o error.");
}

# TEST: callHandler()
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{PATH_INFO} = "foo";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $resourceHook = sub { die "TEST ERROR" };
    $rest->{TEST_CALL} = 1;
    $rest->{TEST_CALL_ERROR} = 1;
    eval {
        $rest->callHandler($resourceHook);
    };
    like($@, qr/TEST ERROR/, "The handle caller with error.");
}

# TEST: Test that the order of the routes is preserved.
{
    my $obj = REST::Application::Routes->new();
    $obj->resourceHooks(map {$_ => $_*$_} (1 .. 1000));
    is_deeply([keys %{$obj->resourceHooks}], [(1 .. 1000)],
              'resourceHooks contains ordered keys.');
    is_deeply([values %{$obj->resourceHooks}], [map {$_*$_} (1 .. 1000)],
              'resourceHooks contains ordered values.');
}

# TEST: run, simple usage.
{
    restoreENV();
    CGI->initialize_globals();
    my $rest = RoutesTestClass->new();
    $ENV{PATH_INFO} = "foo";
    $ENV{REST_APP_RETURN_ONLY} = 1;
    my $obj = REST::Application::Routes->new();
    $obj->resourceHooks('/foo/:var/bar', => sub {shift; shift;});
    is_deeply(${$obj->loadResource("/foo/42/bar")}, {var => 42},
              "Testing parsing action");
    is($obj->getLastMatchPath(), "/foo/42/bar", "Testing getLastMatchPath");
    is($obj->getLastMatchPattern(), "/foo/:var/bar", 
       "Testing getLastMatchPattern");
}

# TEST: run, more advanced usage.
{
   my $obj = REST::Application::Routes->new();
   $obj->resourceHooks(
       '/data/tags/:tag', => sub {shift; shift},
       '/data/tags', => sub {shift; shift;},
       '/data/pages/:page/sections/:section', => sub {shift; shift;},
       '/data/workspaces/:ws/pages/:page', => sub {shift; shift;},
       '/data/workspaces/:ws', => sub {shift; shift;},
   );

   is_deeply(${$obj->loadResource("/data/tags")}, {}, '/data/tags matches');
   is_deeply(${$obj->loadResource("/data/tags/foo")}, {tag => 'foo'}, 
             '/data/tags/foo matches');
   is_deeply(${$obj->loadResource("/data/pages/cows/sections/udder")},
                       {page => 'cows', section => 'udder'},
                       '/data/pages/cows/sections/udder/love/cakes matches');
   is(${$obj->loadResource("/data/pages/cows/sections/udder/love/cakes")},
                        undef,
                       '/data/pages/cows/sections/udder/love/cakes no match');
   is_deeply(${$obj->loadResource("/data/workspaces/cows")}, {ws => 'cows'},
             '/data/workspaces/cows matches');
}

# TEST: Empty variable fix, from Chris Dent.
{
   my $obj = REST::Application::Routes->new();
   $obj->resourceHooks(
       '/data/tags/:tag', => sub {shift; shift},
       '/data/tags', => sub {shift; "cow"},
   );

   is_deeply(${$obj->loadResource("/data/tags")}, "cow", '/data/tags matches');
   is_deeply(${$obj->loadResource("/data/tags/")}, "cow", '/data/tags matches');
   is_deeply(${$obj->loadResource("/data/tags/foo")}, {tag => 'foo'}, 
             '/data/tags/foo matches');
}

# TEST: make sure /foo does not match in the middle of something else
{
    my $obj = REST::Application::Routes->new();
    $obj->resourceHooks(
        '/foo', => sub { shift; "cow" },
    );
    is( ${$obj->loadResource("/fooCOW")}, undef,
        '/fowCOW is not matched' );
}