#! /usr/bin/env perl
use strict;
use warnings;
use Test::More tests => 308;
use Pinwheel::Context;
use Pinwheel::Controller;
use FindBin qw($Bin);
# Config::Hooks template
{
package Config::Hooks;
our @log;
sub _initialise { push @log, 'initialise' };
sub _before_dispatch { push @log, ['before_dispatch', @_] };
sub _after_dispatch { push @log, ['after_dispatch', @_] };
}
{
package Pinwheel::Helpers;
our @EXPORT_OK = qw(render);
use Pinwheel::Controller qw(render);
package Helpers::Application;
our @EXPORT_OK = qw(foo yield);
sub foo { 'foo' }
sub yield { Pinwheel::Context::get('render')->{content}{layout} }
package Helpers::NumberOne;
our @EXPORT_OK = qw(bar);
sub bar { }; bar(); # Keep Devel::Cover happy
package Helpers::Pagination;
our @EXPORT_OK = qw(paginate nonexistent);
sub paginate { }; paginate(); # Keep Devel::Cover happy
package Helpers::Three;
our @EXPORT_OK = qw(foo);
sub foo { }; foo(); # Keep Devel::Cover happy
package Controllers::NumberOne;
our @ACTIONS = qw(show);
sub show { }; show(); # Keep Devel::Cover happy
package Controllers::Two;
our @ACTIONS = qw(show ping error bad_template);
our @HELPERS = qw(Pagination);
sub show { Pinwheel::Controller::render(action => 'ping') }
sub ping { }
sub error { die 'blah' }
sub bad_template { }
package Controllers::Three;
our @ACTIONS = qw(nonexistent);
our $LAYOUT = 'blah';
package Controllers::Bad;
}
# By default, just suppress error logging.
is($Pinwheel::Controller::error_logger, \&Pinwheel::Controller::default_error_logger, 'error_logger defaults to default_error_logger');
$Pinwheel::Controller::error_logger = sub {};
# Controller info
{
my $c;
# Default helpers come from Helpers::Application and Helpers::<Controller>
$c = Pinwheel::Controller::_get_controller('number_one');
is(scalar(keys %{$c->{helpers}}), 4);
is($c->{helpers}{foo}, \&Helpers::Application::foo);
is($c->{helpers}{yield}, \&Helpers::Application::yield);
is($c->{helpers}{render}, \&Pinwheel::Helpers::render);
is($c->{helpers}{bar}, \&Helpers::NumberOne::bar);
is(scalar(keys %{$c->{actions}}), 1);
is($c->{actions}{show}, \&Controllers::NumberOne::show);
is($c->{layout}, undef);
# Helpers::<Controller> is optional
# Extra helpers can be listed in @HELPERS
$c = Pinwheel::Controller::_get_controller('two');
is(scalar(keys %{$c->{helpers}}), 4);
is($c->{helpers}{foo}, \&Helpers::Application::foo);
is($c->{helpers}{yield}, \&Helpers::Application::yield);
is($c->{helpers}{render}, \&Pinwheel::Helpers::render);
is($c->{helpers}{paginate}, \&Helpers::Pagination::paginate);
is(scalar(keys %{$c->{actions}}), 4);
is($c->{actions}{show}, \&Controllers::Two::show);
is($c->{actions}{ping}, \&Controllers::Two::ping);
is($c->{actions}{error}, \&Controllers::Two::error);
is($c->{actions}{bad_template}, \&Controllers::Two::bad_template);
is($c->{layout}, undef);
# The controller helper has a higher priority than Helpers::Application
$c = Pinwheel::Controller::_get_controller('three');
is(scalar(keys %{$c->{helpers}}), 3);
is($c->{helpers}{yield}, \&Helpers::Application::yield);
is($c->{helpers}{render}, \&Pinwheel::Helpers::render);
is($c->{helpers}{foo}, \&Helpers::Three::foo);
is(scalar(keys %{$c->{actions}}), 0);
is($c->{layout}, 'blah');
# The controller information is cached after the first lookup
$Controllers::Three::LAYOUT = 'wibble';
$c = Pinwheel::Controller::_get_controller('three');
is($c->{layout}, 'blah');
# Bad controller (no @ACTIONS)
$c = Pinwheel::Controller::_get_controller('bad');
ok(!defined($c));
}
# Layout helpers
{
my $helpers;
$helpers = Pinwheel::Controller::_get_layout_helpers();
is(scalar(keys %$helpers), 3);
is($helpers->{yield}, \&Helpers::Application::yield);
is($helpers->{foo}, \&Helpers::Application::foo);
is($helpers->{render}, \&Pinwheel::Helpers::render);
# Cached after first lookup
is($helpers, Pinwheel::Controller::_get_layout_helpers());
}
# Route parameter validator
{
my $params;
# Bad controller values
ok(!Pinwheel::Controller::_check_route_params(undef));
$params = { controller => '0', action => 'show' };
ok(!Pinwheel::Controller::_check_route_params($params));
$params = { controller => '..', action => 'show' };
ok(!Pinwheel::Controller::_check_route_params($params));
# Good controller values
$params = { controller => 'x', action => 'show' };
ok(Pinwheel::Controller::_check_route_params($params));
$params = { controller => 'aaa', action => 'show' };
ok(Pinwheel::Controller::_check_route_params($params));
$params = { controller => 'a0_9', action => 'show' };
ok(Pinwheel::Controller::_check_route_params($params));
# Bad action values
$params = { controller => 'x', action => '0' };
ok(!Pinwheel::Controller::_check_route_params($params));
$params = { controller => 'x', action => '..' };
ok(!Pinwheel::Controller::_check_route_params($params));
# Good action values
$params = { controller => 'x', action => 'x' };
ok(Pinwheel::Controller::_check_route_params($params));
$params = { controller => 'x', action => 'aaa' };
ok(Pinwheel::Controller::_check_route_params($params));
$params = { controller => 'x', action => 'a0_9' };
ok(Pinwheel::Controller::_check_route_params($params));
}
# name_like_this to NameLikeThis converter
{
my $fn = \&Pinwheel::Controller::_make_mixed_case;
is(&$fn('x'), 'X');
is(&$fn('abc'), 'Abc');
is(&$fn('abc_def'), 'AbcDef');
is(&$fn('abc_def_ghi'), 'AbcDefGhi');
is(&$fn('a2z'), 'A2z');
is(&$fn('a_b_c'), 'ABC');
}
# Construct template name (eg, for render)
{
my ($fn, $route);
$fn = sub { Pinwheel::Controller::_make_template_name(@_) };
$route = { controller => 'foo', action => 'bar' };
is(&$fn({}, $route), 'foo/bar');
is(&$fn({action => 'x'}, $route), 'foo/x');
is(&$fn({action => 'x_y'}, $route), 'foo/x_y');
is(&$fn({action => '.'}, $route), undef);
is(&$fn({action => '..'}, $route), undef);
is(&$fn({action => 'x/y'}, $route), undef);
is(&$fn({action => 'x.y'}, $route), undef);
is(&$fn({template => 'a'}, $route), undef);
is(&$fn({template => 'a/b'}, $route), 'a/b');
is(&$fn({template => 'a_b/c_d'}, $route), 'a_b/c_d');
is(&$fn({template => 'a/b/c'}, $route), undef);
is(&$fn({template => 'a/..'}, $route), undef);
is(&$fn({template => 'a/b..'}, $route), undef);
is(&$fn({template => 'a/b.c'}, $route), undef);
is(&$fn({template => '../a'}, $route), undef);
is(&$fn({partial => 'a'}, $route), 'foo/_a');
is(&$fn({partial => 'a/b'}, $route), 'a/_b');
is(&$fn({partial => 'a/_b'}, $route), 'a/__b');
is(&$fn({partial => 'a/b.c'}, $route), undef);
is(&$fn({partial => 'a/b/c'}, $route), undef);
}
# url_for (wrapper around Mapper::generate)
{
my ($request, $url);
$request = {
host => 'www.bbc.co.uk',
path => '/radio4/p/today',
base => '/~paulc'
};
$Pinwheel::Controller::map->reset();
Pinwheel::Controller::connect('r', '/:network/p/:brand');
Pinwheel::Controller::connect('s', '/:foo/x');
Pinwheel::Controller::connect('t', 'http://foo.com/*path');
$url = Pinwheel::Controller::url_for('s', foo => 'a');
is($url, '/a/x');
Pinwheel::Context::set('*Pinwheel::Controller',
request => $request,
route => $Pinwheel::Controller::map->match($request->{path})
);
$url = Pinwheel::Controller::url_for();
is($url, '/~paulc/radio4/p/today');
$url = Pinwheel::Controller::url_for(only_path => 0);
is($url, 'http://www.bbc.co.uk/~paulc/radio4/p/today');
$url = Pinwheel::Controller::url_for(network => 'radio1');
is($url, '/~paulc/radio1/p/today');
$url = Pinwheel::Controller::url_for(brand => 'tomorrow');
is($url, '/~paulc/radio4/p/tomorrow');
$url = Pinwheel::Controller::url_for('s', foo => 'a');
is($url, '/~paulc/a/x');
$url = Pinwheel::Controller::url_for('s', foo => 'a', only_path => 1);
is($url, '/~paulc/a/x');
$url = Pinwheel::Controller::url_for('s', foo => 'a', only_path => 0);
is($url, 'http://www.bbc.co.uk/~paulc/a/x');
$url = Pinwheel::Controller::url_for('t', path => 'a/b/c');
is($url, 'http://foo.com/a/b/c');
$url = Pinwheel::Controller::url_for('x', foo => 'a');
is($url, undef);
}
# Retrieve compiled template
{
my ($t, $t2, $c);
Pinwheel::Controller::set_templates_root("$Bin/../fixtures/tmpl");
# %Pinwheel::Controller::template_cache isn't part of the public API, but these
# tests use the hash.
my $cacheref = \%Pinwheel::Controller::template_cache;
is(scalar(keys %$cacheref), 0, 'template cache starts off empty');
$t = Pinwheel::Controller::_get_template('text/hello.txt');
like($t->({}, {}, {}), qr/^hello$/s);
# Templates are cached in the session's context
$t2 = Pinwheel::Controller::_get_template('text/hello.txt');
is($t, $t2);
is(scalar(keys %$cacheref), 1, 'template cache now non-empty');
# Name must be valid before the filesystem is checked
$t = Pinwheel::Controller::_get_template('text/invisible');
is($t, undef);
$t = Pinwheel::Controller::_get_template('text/invisible.x.y');
is($t, undef);
# Multiple extensions are supported
$t = Pinwheel::Controller::_get_template('text/old.txt');
like($t->({}, {}, {}), qr/^old$/s);
# File exists, but template names must be \w+/\w+\.\w+
ok(!Pinwheel::Controller::_get_template('text/invisible.x.y'));
ok(!Pinwheel::Controller::_get_template('../out_of_reach.txt'));
ok(!Pinwheel::Controller::_get_template('shared/../out_of_reach.txt')); # but is OK via symlink
ok(!Pinwheel::Controller::_get_template('out_of_reach.txt'));
# File exists, but extension ('blah') is unknown
ok(!Pinwheel::Controller::_get_template('text/unknown.txt'));
# File doesn't exist
ok(!Pinwheel::Controller::_get_template('text/nonexistent.txt'));
%$cacheref = ();
Pinwheel::View::Data::_clear_templates();
Pinwheel::Controller::preload_templates();
is(scalar(keys %$cacheref), 22, 'preload_templates loads all valid templates');
# Verify that, just in case File::Find::find returns things outside of
# templates_root, we don't attempt to load them
my $old_find = \&File::Find::find;
my $fake_find = sub {
my ($opts, $dir) = @_;
my $w = $$opts{wanted};
local $_ = "$Bin/../fixtures/out_of_reach.txt.erb";
&$w();
};
{ no warnings 'redefine'; *File::Find::find = $fake_find; }
%$cacheref = ();
Pinwheel::View::Data::_clear_templates();
Pinwheel::Controller::preload_templates();
is(scalar(keys %$cacheref), 0, 'preload_templates validates templates_root');
{ no warnings 'redefine'; *File::Find::find = $old_find; }
}
# Retrieve compiled layout template
{
my ($ctx, $t);
Pinwheel::Controller::set_templates_root("$Bin/../fixtures/tmpl");
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$t = Pinwheel::Controller::_get_layout('html', {});
is($t, Pinwheel::Controller::_get_template('layouts/application.html'));
$t = Pinwheel::Controller::_get_layout('html', {layout => 0});
ok(!$t);
$ctx->{controller} = Pinwheel::Controller::_get_controller('three');
$t = Pinwheel::Controller::_get_layout('html', {});
is($t, Pinwheel::Controller::_get_template('layouts/blah.html'));
delete $ctx->{controller};
$ctx->{route}{controller} = 'one';
$t = Pinwheel::Controller::_get_layout('html', {});
is($t, Pinwheel::Controller::_get_template('layouts/one.html'));
$t = Pinwheel::Controller::_get_layout('blah', {});
ok(!$t);
delete $ctx->{route};
$t = Pinwheel::Controller::_get_layout('html', {partial => 'foo'});
ok(!$t);
$ctx->{route}{controller} = 'text';
$t = Pinwheel::Controller::_get_layout(
'txt', {partial => 'hello', layout => 'wrapper'}
);
is($t, Pinwheel::Controller::_get_template('text/_wrapper.txt'));
delete $ctx->{route};
$t = Pinwheel::Controller::_get_layout(
'txt', {partial => 'text/hello', layout => 'wrapper'}
);
is($t, Pinwheel::Controller::_get_template('text/_wrapper.txt'));
eval { Pinwheel::Controller::_get_layout('html', {layout => 'nonexistent'}) };
like($@, qr/unable to find layouts\/nonexistent/i);
}
# Render a template with optional layout
{
my ($ctx, $renderctx, $fn, $content);
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$renderctx = Pinwheel::Context::get('render');
$fn = sub {
my ($name, $lname, $format, $locals, $globals) = @_;
my ($template, $layout);
$template = Pinwheel::Controller::_get_template("$name.$format");
$layout = Pinwheel::Controller::_get_template("$lname.$format") if ($lname);
$locals ||= {};
$globals ||= {};
Pinwheel::Context::set('template', %$globals);
return Pinwheel::Controller::_render_template($template, $layout, $locals);
};
$content = &$fn('text/hello', undef, 'txt');
like($content, qr/^hello$/s);
$content = &$fn('text/hello', 'text/_wrapper', 'txt');
like($content, qr/^t:hello\s*$/s);
# Locals are visible to the layout too
$content = &$fn('text/local', 'text/_var', 'txt', {var => 'x'});
like($content, qr/^x\s*-x\s*$/s);
$content = &$fn('text/local', undef, 'txt', {var => 'local'});
like($content, qr/^local$/s);
$content = &$fn('text/global', undef, 'txt', {}, {var => 'global'});
like($content, qr/^global$/s);
# Ensure the base helpers are picked up even without a controller
$content = &$fn('text/helper', undef, 'txt');
like($content, qr/^foo$/s);
# Same again, with a controller
$ctx->{controller}{helpers} = Pinwheel::Controller::_get_helpers(['Application']);
$content = &$fn('text/helper', undef, 'txt');
like($content, qr/^foo$/s);
$renderctx->{format} = ['txt'];
$ctx->{route}{controller} = 'text';
$ctx->{controller}{helpers} = Pinwheel::Controller::_get_helpers(['Pinwheel::Helpers']);
$content = &$fn('text/nest', undef, 'txt');
like($content, qr/nest:t:nested\s*$/s);
}
# Render the content part of a response
{
my ($ctx, $renderctx, $s, $f, $obj);
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{route}{controller} = 'text';
$ctx->{route}{action} = 'hello';
$ctx->{controller}{helpers} = Pinwheel::Controller::_get_helpers(['Pinwheel::Helpers']);
$renderctx = Pinwheel::Context::get('render');
$renderctx->{format} = ['txt'];
($s, $f) = Pinwheel::Controller::_render_content({text => 'test'});
is($s, 'test');
is($f, 'txt');
($s, $f) = Pinwheel::Controller::_render_content({text => ''});
is($s, '');
is($f, 'txt');
($s, $f) = Pinwheel::Controller::_render_content({});
like($s, qr/^hello$/s);
is($f, 'txt');
($s, $f) = Pinwheel::Controller::_render_content({layout => 'prefix'});
like($s, qr/^text:hello\s*$/s);
is($f, 'txt');
($s, $f) = Pinwheel::Controller::_render_content({format => 'json'});
like($s, qr/^\("hello"\)\s*$/s);
is($f, 'json');
($s, $f) = Pinwheel::Controller::_render_content({
action => 'nest',
layout => 'prefix'
});
like($s, qr/^text:nest:t:nested\s*$/s);
is($f, 'txt');
($s, $f) = Pinwheel::Controller::_render_content({via => 'hash', format => 'json'});
like($s->to_string($f), qr/{\s*"p":\s*"hello"\s*}/);
is($f, 'json');
($s, $f) = Pinwheel::Controller::_render_content({via => 'hash', format => 'xml'});
like($s->to_string($f), qr/<p>hello<\/p>/);
is($f, 'xml');
Pinwheel::View::Data::_clear_templates();
eval { Pinwheel::Controller::_render_content({partial => '../blah'}) };
like($@, qr/invalid template name/i);
eval { Pinwheel::Controller::_render_content({partial => 'nonexistent'}) };
like($@, qr/unable to find template/i);
}
# Render the response headers
{
my ($fn, $headers);
$fn = sub {
my ($ctx, $raw, %h);
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
delete $ctx->{response}{headers}{'status'};
delete $ctx->{response}{headers}{'content-type'};
delete $ctx->{response}{headers}{'location'};
$raw = Pinwheel::Controller::_render_headers(@_);
$h{$_->[0]} = $_->[1] foreach (values %$raw);
return \%h;
};
Pinwheel::Context::reset();
$headers = &$fn({}, undef);
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'text/html'});
$headers = &$fn({}, 'xml');
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'application/xml'});
$headers = &$fn({}, 'unknown');
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'text/html'});
$headers = &$fn({status => 201}, undef);
is_deeply($headers, {'Status' => 201, 'Content-Type' => 'text/html'});
$headers = &$fn({content_type => 'text/xml'}, undef);
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'text/xml'});
$headers = &$fn({location => '/a/b/c'}, undef);
is($headers->{'Location'}, '/a/b/c');
$headers = &$fn({}, 'xml');
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'application/xml'});
$headers = &$fn({}, 'txt');
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'text/plain'});
set_header('X-Test', 'Blah');
$headers = &$fn({}, 'txt');
is_deeply($headers, {
'Status' => 200,
'Content-Type' => 'text/plain',
'X-Test' => 'Blah',
});
$fn = sub {
my ($raw, %h);
$raw = Pinwheel::Controller::_render_headers(@_);
$h{$_->[0]} = $_->[1] foreach (values %$raw);
return \%h;
};
Pinwheel::Context::reset();
set_header('Content-Type', 'abc/xyz');
$headers = &$fn({}, 'txt');
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'abc/xyz'});
Pinwheel::Context::reset();
set_header('Content-Type', 'abc/xyz');
$headers = &$fn({content_type => 'x'}, 'txt');
is_deeply($headers, {'Status' => 200, 'Content-Type' => 'x'});
Pinwheel::Context::reset();
set_header('Status', 123);
$headers = &$fn({}, 'txt');
is_deeply($headers, {'Status' => 123, 'Content-Type' => 'text/plain'});
Pinwheel::Context::reset();
set_header('Status', 123);
$headers = &$fn({status => 321}, 'txt');
is_deeply($headers, {'Status' => 321, 'Content-Type' => 'text/plain'});
Pinwheel::Context::reset();
set_header('Status', 123);
set_header('sTATUS', 456);
$headers = &$fn({}, 'txt');
is_deeply($headers, {'sTATUS' => 456, 'Content-Type' => 'text/plain'});
Pinwheel::Context::reset();
set_header('Status', 123);
set_header('sTATUS', 456);
$headers = &$fn({status => 321}, 'txt');
is_deeply($headers, {'Status' => 321, 'Content-Type' => 'text/plain'});
}
# URL parameters
{
my $ctx;
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{route} = {
a => 'main',
b => 'index',
c => 'radio4',
d => undef
};
is(Pinwheel::Controller::params('a'), 'main');
is_deeply(Pinwheel::Controller::params(qw(a b c)), ['main', 'index', 'radio4']);
is_deeply(Pinwheel::Controller::params(qw(c a)), ['radio4', 'main']);
is_deeply(Pinwheel::Controller::params(qw(d b)), [undef, 'index']);
is_deeply(Pinwheel::Controller::params(qw(d foo a)), [undef, undef, 'main']);
}
# Query parameters
{
my ($ctx);
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{request}{query} = '';
is(Pinwheel::Controller::query_params('f'), undef);
is_deeply($ctx->{query}, {});
delete $ctx->{query};
$ctx->{request}{query} = undef;
is(Pinwheel::Controller::query_params('g'), undef);
is_deeply($ctx->{query}, {});
delete $ctx->{query};
$ctx->{request}{query} = 'a=b=c+%26+d&x=&f';
is(Pinwheel::Controller::query_params('a'), 'b=c & d');
is(Pinwheel::Controller::query_params('x'), '');
is(Pinwheel::Controller::query_params('f'), '');
is_deeply($ctx->{query}, {a => 'b=c & d', x => '', f => ''});
delete $ctx->{query};
$ctx->{request}{query} = 'a+b%3dc=d+e%26f';
is(Pinwheel::Controller::query_params('a b=c'), 'd e&f');
is_deeply($ctx->{query}, {'a b=c' => 'd e&f'});
delete $ctx->{query};
$ctx->{request}{query} = 'a=1&b=2&c=3';
is_deeply(Pinwheel::Controller::query_params('a', 'c', 'b'), [1, 3, 2]);
is_deeply(Pinwheel::Controller::query_params('c', 'b', 'a'), [3, 2, 1]);
is_deeply($ctx->{query}, {a => 1, b => 2, c => 3});
delete $ctx->{query};
my $warnings = 0;
local $SIG{__WARN__} = sub { ++$warnings };
$ctx->{request}{query} = 'a=1&&b=2';
is_deeply(Pinwheel::Controller::query_params('a', 'b'), [1, 2]);
is($warnings, 0);
delete $ctx->{query};
$warnings = 0;
local $SIG{__WARN__} = sub { ++$warnings };
$ctx->{request}{query} = '&a=1&b=2';
is_deeply(Pinwheel::Controller::query_params('a', 'b'), [1, 2]);
is($warnings, 0);
}
# Cache expiry times
{
my ($render_headers, $ctx, $h);
$render_headers = sub {
my ($raw, %h);
$raw = Pinwheel::Controller::_render_headers(@_);
$h{$_->[0]} = $_->[1] foreach (values %$raw);
return \%h;
};
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{request}{time} = Pinwheel::Model::Time::local(2008, 1, 1)->timestamp;
expires_in(10);
$h = &$render_headers();
is($h->{'Date'}, 'Tue, 01 Jan 2008 00:00:00 GMT');
is($h->{'Expires'}, 'Tue, 01 Jan 2008 00:00:10 GMT');
is($h->{'Cache-Control'}, 'max-age=10');
expires_in(14 * 24 * 60 * 60);
$h = &$render_headers();
is($h->{'Date'}, 'Tue, 01 Jan 2008 00:00:00 GMT');
is($h->{'Expires'}, 'Tue, 15 Jan 2008 00:00:00 GMT');
is($h->{'Cache-Control'}, 'max-age=1209600');
expires_at(Pinwheel::Model::Time::local(2008, 6, 1, 0, 0, 0));
$h = &$render_headers();
is($h->{'Date'}, 'Tue, 01 Jan 2008 00:00:00 GMT');
is($h->{'Expires'}, 'Sat, 31 May 2008 23:00:00 GMT');
is($h->{'Cache-Control'}, 'max-age=13129200');
expires_now();
$h = &$render_headers();
is($h->{'Cache-Control'}, 'no-cache');
is($h->{'Pragma'}, 'no-cache');
}
# Error rendering functions
{
my ($ctx, $renderctx);
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{route} = {controller => 'foo', action => 'bar'};
$ctx->{controller} = Pinwheel::Controller::_get_controller('number_one');
$renderctx = Pinwheel::Context::get('render');
$renderctx->{format} = ['html'];
$ctx->{headers} = $ctx->{response} = undef;
$ctx->{rendering} = 0;
Pinwheel::Controller::render_404('x');
is($ctx->{headers}{'status'}[1], 404);
like($ctx->{content}, qr/^a:4:x\s*$/);
Pinwheel::Controller::render_404('overwrite');
like($ctx->{content}, qr/^a:4:overwrite\s*$/);
$ctx->{headers} = $ctx->{response} = undef;
$ctx->{rendering} = 0;
Pinwheel::Controller::render_500('x');
is($ctx->{headers}{'status'}[1], 500);
like($ctx->{content}, qr/^a:5:x\s*$/);
Pinwheel::Controller::render_500('overwrite');
like($ctx->{content}, qr/^a:5:overwrite\s*$/);
$ctx->{headers} = $ctx->{response} = undef;
$ctx->{rendering} = 0;
Pinwheel::Controller::render_error(403, 'x');
is($ctx->{headers}{'status'}[1], 403);
like($ctx->{content}, qr/^x\s*$/);
Pinwheel::Controller::render_error(403, 'overwrite');
like($ctx->{content}, qr/^overwrite\s*$/);
$ctx->{headers} = $ctx->{response} = undef;
$ctx->{rendering} = 0;
Pinwheel::Controller::render_error(499, 'x');
is($ctx->{headers}{'status'}[1], 500);
is($ctx->{headers}{'content-type'}[1], 'text/html');
like($ctx->{content}, qr/unknown function .*error499\.html/i);
$ctx->{headers} = $ctx->{response} = undef;
$ctx->{rendering} = 0;
$renderctx->{format} = ['boom'];
Pinwheel::Controller::render_500('x');
is($ctx->{headers}{'status'}[1], 500);
is($ctx->{headers}{'content-type'}[1], 'text/plain');
like($ctx->{content}, qr/unknown function .*error500\.boom/i);
}
# Top-level render functions
{
my ($ctx, $renderctx, $s);
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{controller} = Pinwheel::Controller::_get_controller('number_one');
$ctx->{route} = { controller => 'text' };
$renderctx = Pinwheel::Context::get('render');
$renderctx->{format} = ['txt'];
$s = Pinwheel::Controller::render_to_string(template => 'text/hello');
like($s, qr/^hello\s*$/);
$s = Pinwheel::Controller::render_to_string(template => 'text/nest');
like($s, qr/^nest:t:nested\s*$/);
ok(!defined($ctx->{headers}));
$ctx->{headers} = 1;
$s = Pinwheel::Controller::render_to_string(template => 'text/hello');
like($s, qr/^hello\s*$/);
$ctx->{headers} = 1;
is(Pinwheel::Controller::render(template => 'text/hello'), undef);
$ctx->{headers} = undef;
like(Pinwheel::Controller::render(template => 'text/hello'), qr/^hello\s*$/);
$ctx->{headers} = undef;
like(Pinwheel::Controller::render(template => 'text/nest'), qr/^nest:t:nested\s*$/);
}
# Accepts
{
my ($status, $render_error, $accepts, $ctx);
$render_error = \&Pinwheel::Controller::render_error;
{
no warnings 'redefine';
*Pinwheel::Controller::render_error = sub { $status = $_[0] };
}
$accepts = sub { $status = undef; Pinwheel::Controller::accepts(@_) };
Pinwheel::Context::reset();
Pinwheel::Context::set('*Pinwheel::Controller',
route => {
format => 'html',
representation => 'xhtml',
}
);
$ctx = Pinwheel::Context::get('render');
$ctx->{format} = ['foo'];
ok(&$accepts('html'));
is($status, undef);
is(Pinwheel::Controller::format(), 'html');
ok(!&$accepts('rss'));
is($status, 404);
Pinwheel::Controller::set_format_param('representation');
ok(!&$accepts('html'));
is($status, 404);
ok(&$accepts('html', 'xhtml'));
is($status, undef);
is(Pinwheel::Controller::format(), 'xhtml');
Pinwheel::Controller::set_base_format('foo');
Pinwheel::Controller::set_format_param('blah');
ok(&$accepts('foo'));
ok(!&$accepts('html'));
Pinwheel::Controller::set_format_param('format');
Pinwheel::Controller::set_base_format('foo');
ok(&$accepts('rss', 'atom', 'html'));
is($status, undef);
is(Pinwheel::Controller::format(), 'html');
push @{$ctx->{format}}, 'x';
ok(&$accepts('html'));
is($status, undef);
is(Pinwheel::Controller::format(), 'x');
{
no warnings 'redefine';
*Pinwheel::Controller::render_error = $render_error;
}
}
# Responds to
{
my (@formats, @args, $respond_to, $render_fn, $render_error_fn);
$respond_to = sub {
Pinwheel::Context::set('*Pinwheel::Controller', route => {format => shift});
Pinwheel::Controller::respond_to(@_);
};
$render_fn = \&Pinwheel::Controller::render;
$render_error_fn = \&Pinwheel::Controller::render_error;
{
no warnings 'redefine';
*Pinwheel::Controller::render = sub { @args = ('render', @_) };
*Pinwheel::Controller::render_error = sub { @args = ('error', @_) };
}
Pinwheel::Context::set('render', format => ['html']);
@formats = (
'html',
'json' => sub { @args = @{Pinwheel::Context::get('render')->{format}} },
'xml',
);
Pinwheel::Context::reset();
&$respond_to('html', @formats);
is_deeply(\@args, ['render', 'format', 'html']);
Pinwheel::Context::reset();
Pinwheel::Context::set('render', format => ['html']);
&$respond_to(undef, @formats);
is_deeply(\@args, ['render', 'format', 'html']);
Pinwheel::Context::reset();
&$respond_to('json', @formats);
is_deeply(\@args, ['json']);
Pinwheel::Context::reset();
&$respond_to('xml', @formats);
is_deeply(\@args, ['render', 'format', 'xml']);
Pinwheel::Context::reset();
&$respond_to('blah', @formats);
is_deeply([@args[0, 1]], ['error', 404]);
Pinwheel::Context::reset();
Pinwheel::Context::set('*Pinwheel::Controller', route => {blah => 'foo'});
Pinwheel::Controller::set_format_param('blah');
Pinwheel::Controller::respond_to('foo');
is_deeply(\@args, ['render', 'format', 'foo']);
Pinwheel::Controller::set_format_param('format');
{
no warnings 'redefine';
*Pinwheel::Controller::render = $render_fn;
*Pinwheel::Controller::render_error = $render_error_fn;
}
}
# Render format
{
my ($ctx);
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('render');
$ctx->{format} = ['html'];
is(Pinwheel::Controller::format(), 'html');
$ctx->{format} = ['html', 'xml'];
is(Pinwheel::Controller::format(), 'xml');
}
# Redirects
{
my ($ctx, $hdr, @url);
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{request} = {
host => 'www.bbc.co.uk',
path => '/radio4/p/today',
base => '/~paulc'
};
$hdr = sub { $ctx->{headers}{$_[0]}[1] };
$Pinwheel::Controller::map->reset();
Pinwheel::Controller::connect('r', '/:network/p/:brand');
Pinwheel::Controller::connect('fixed', '/fixed_url');
@url = ('r', network => 'r0', brand => 'foo');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to(@url);
is(&$hdr('status'), 302);
is(&$hdr('location'), Pinwheel::Controller::url_for(@url, only_path => 0));
delete $ctx->{headers};
Pinwheel::Controller::redirect_to('fixed');
is(&$hdr('status'), 302);
is(&$hdr('location'), 'http://www.bbc.co.uk/~paulc/fixed_url');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to('fixed', status => 303);
is(&$hdr('status'), 303);
is(&$hdr('location'), 'http://www.bbc.co.uk/~paulc/fixed_url');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to(undef, network => 'r8', brand => 'bar' );
is(&$hdr('status'), 302);
is(&$hdr('location'), 'http://www.bbc.co.uk/~paulc/r8/p/bar');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to('/foo');
is(&$hdr('status'), 302);
is(&$hdr('location'), 'http://www.bbc.co.uk/foo');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to('/foo', undef);
is(&$hdr('status'), 302);
is(&$hdr('location'), 'http://www.bbc.co.uk/foo');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to('http://foo.com/bar');
is(&$hdr('status'), 302);
is(&$hdr('location'), 'http://foo.com/bar');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to('http://foo.com/bar', status => 302);
is(&$hdr('status'), 302);
is(&$hdr('location'), 'http://foo.com/bar');
delete $ctx->{headers};
Pinwheel::Controller::redirect_to('http://foo.com/bar', status => 303);
is(&$hdr('status'), 303);
is(&$hdr('location'), 'http://foo.com/bar');
}
# Dispatch
{
my ($ctx, $renderctx, $request, $i, $headers, $content);
Pinwheel::Context::reset();
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$request = {
host => 'www.bbc.co.uk',
path => '/radio4/p/today',
base => '/~paulc'
};
$ctx->{request} = $request;
$renderctx = Pinwheel::Context::get('render');
$renderctx->{format} = ['html'];
$Pinwheel::Controller::map->reset();
Pinwheel::Controller::connect('/bad_controller', controller => '$foo');
Pinwheel::Controller::connect('/bad_action', action => '$bar');
Pinwheel::Controller::connect('/missing_controller', controller => 'missing');
Pinwheel::Controller::connect('/missing_action', controller => 'two', action => 'x');
Pinwheel::Controller::connect('/ping', controller => 'two', action => 'ping');
Pinwheel::Controller::connect('/show', controller => 'two', action => 'show');
Pinwheel::Controller::connect('/error', controller => 'two', action => 'error');
Pinwheel::Controller::connect('/badt', controller => 'two', action => 'bad_template');
Pinwheel::Controller::connect('/getping',
controller => 'two', action => 'ping',
conditions => {method => 'GET'},
);
$request->{path} = '/bad_controller';
Pinwheel::Controller::_process_request({}, $ctx);
is($ctx->{headers}{'status'}[1], 404);
$ctx->{headers} = $ctx->{response} = undef;
$request->{path} = '/bad_action';
Pinwheel::Controller::_process_request({}, $ctx);
is($ctx->{headers}{'status'}[1], 404);
$ctx->{headers} = $ctx->{response} = undef;
$request->{path} = '/missing_controller';
Pinwheel::Controller::_process_request({}, $ctx);
is($ctx->{headers}{'status'}[1], 404);
$ctx->{headers} = $ctx->{response} = undef;
$request->{path} = '/missing_action';
Pinwheel::Controller::_process_request({}, $ctx);
is($ctx->{headers}{'status'}[1], 404);
$ctx->{headers} = $ctx->{response} = undef;
$request->{path} = '/ping';
Pinwheel::Controller::_process_request({}, $ctx);
is($ctx->{headers}, undef);
Pinwheel::Controller::render();
is($ctx->{headers}{'status'}[1], 200);
is($ctx->{headers}{'content-type'}[1], 'text/html');
like($ctx->{content}, qr/^a:pong\s*$/);
$ctx->{headers} = $ctx->{response} = undef;
$request->{path} = '/ping';
Pinwheel::Controller::_process_request({}, $ctx);
is($ctx->{headers}, undef);
Pinwheel::Controller::render(format => 'txt');
is($ctx->{headers}{'status'}[1], 200);
is($ctx->{headers}{'content-type'}[1], 'text/plain');
like($ctx->{content}, qr/^txtpong\s*$/);
$request->{path} = '/ping';
($headers, $content) = Pinwheel::Controller::dispatch($request);
is($headers->{'status'}[1], 200);
like($content, qr/^a:pong\s*$/s);
$request->{path} = '/show';
($headers, $content) = Pinwheel::Controller::dispatch($request);
is($headers->{'status'}[1], 200);
like($content, qr/^a:pong\s*$/s);
$request->{path} = '/error';
($headers, $content) = Pinwheel::Controller::dispatch($request);
is($headers->{'status'}[1], 500);
$request->{path} = '/badt';
($headers, $content) = Pinwheel::Controller::dispatch($request);
is($headers->{'status'}[1], 500);
$request->{method} = 'POST';
$request->{path} = '/getping';
($headers, $content) = Pinwheel::Controller::dispatch($request);
is($headers->{'status'}[1], 404);
$request->{method} = 'GET';
$request->{path} = '/getping';
($headers, $content) = Pinwheel::Controller::dispatch($request);
is($headers->{'status'}[1], 200);
like($content, qr/^a:pong\s*$/s);
}
# Hooks
{
my ($hooks, $request, $ctx);
$hooks = \%Config::Hooks::;
$request = {
host => 'www.bbc.co.uk',
path => '/ping',
base => '/~paulc'
};
$ctx = Pinwheel::Context::get('*Pinwheel::Controller');
$ctx->{request} = $request;
$hooks->{before_dispatch} = \&Config::Hooks::_before_dispatch;
Pinwheel::Controller::dispatch($request);
is_deeply(\@Config::Hooks::log, [['before_dispatch', $request]]);
delete $hooks->{before_dispatch};
@Config::Hooks::log = ();
Pinwheel::Controller::dispatch($request);
is_deeply(\@Config::Hooks::log, []);
$hooks->{after_dispatch} = \&Config::Hooks::_after_dispatch;
Pinwheel::Controller::dispatch($request);
is($Config::Hooks::log[0][0], 'after_dispatch');
is_deeply($Config::Hooks::log[0][1]{status}, ['Status', 200]);
like(${$Config::Hooks::log[0][2]}, qr/pong/);
@Config::Hooks::log = ();
Pinwheel::Controller::dispatch({%$request, path => '/error'});
is_deeply(\@Config::Hooks::log, []);
delete $hooks->{after_dispatch};
@Config::Hooks::log = ();
Pinwheel::Controller::dispatch($request);
is_deeply(\@Config::Hooks::log, []);
$hooks->{initialise} = \&Config::Hooks::_initialise;
Pinwheel::Controller::initialise();
is_deeply(\@Config::Hooks::log, ['initialise']);
delete $hooks->{initialise};
@Config::Hooks::log = ();
Pinwheel::Controller::_process_request({}, $ctx);
is_deeply(\@Config::Hooks::log, []);
}
# Static paths
{
my $fn = \&Pinwheel::Controller::expand_static_path;
Pinwheel::Context::reset();
Pinwheel::Controller::set_static_root('/some/where');
is(&$fn('x'), '/some/where/x');
is(&$fn('/x'), '/some/where/x');
is(&$fn('//x'), '/some/where/x');
is(&$fn('../x'), undef);
is(&$fn('/../x'), undef);
is(&$fn('/.x'), undef);
is(&$fn('/x/..'), undef);
is(&$fn('/x/../y'), undef);
is(&$fn('x/y/z'), '/some/where/x/y/z');
is(&$fn('x///y///z'), '/some/where/x/y/z');
Pinwheel::Context::set('*Pinwheel::Controller', request => {base => '/foo'});
is(&$fn('foo/x'), '/some/where/x');
is(&$fn('/foo/x'), '/some/where/x');
is(&$fn('/x'), '/some/where/x');
}
# Error logger
{
my @e;
local $Pinwheel::Controller::error_logger = sub { push @e, [@_] };
Pinwheel::Context::reset();
my $ctx = Pinwheel::Context::get('*Pinwheel::Controller');
my $request = {
host => 'www.bbc.co.uk',
path => undef,
base => '/~paulc'
};
$ctx->{request} = $request;
my $renderctx = Pinwheel::Context::get('render');
$renderctx->{format} = ['html'];
$Pinwheel::Controller::map->reset();
Pinwheel::Controller::connect('/bad_controller', controller => '$foo');
Pinwheel::Controller::connect('/ping', controller => 'two', action => 'ping');
Pinwheel::Controller::connect('/error', controller => 'two', action => 'error');
@e = ();
Pinwheel::Controller::dispatch({%$request, path => '/ping'});
is(0+@e, 0, 'successful requests do not call the error_logger');
@e = ();
Pinwheel::Controller::dispatch({%$request, path => '/no_such_route'});
is(0+@e, 1, '404 generates 1 error');
is($e[0][0], 404, '404 calls error_logger with status=404');
is($e[0][2], 0, '404 calls error_logger with depth=0');
@e = ();
Pinwheel::Controller::dispatch({%$request, path => '/error'});
is(0+@e, 1, '500 generates 1 error');
is($e[0][0], 500, '500 calls error_logger with status=404');
is($e[0][2], 1, '500 calls error_logger with depth=1');
local $Pinwheel::Controller::error_logger = \&Pinwheel::Controller::default_error_logger;
my @msg;
my $real_cluck = \&Carp::cluck;
my $my_cluck = sub { push @msg, $_[0] };
{ no warnings 'redefine'; *Carp::cluck = $my_cluck; }
# The default logger should do nothing on 404, 406 etc
@msg = ();
Pinwheel::Controller::dispatch({%$request, path => '/no_such_route'});
is(0+@msg, 0, 'the default logger should do nothing on 404, 406 etc');
# The default logger should call Carp::cluck with a sensible message on 500
@msg = ();
Pinwheel::Controller::dispatch({%$request, path => '/error'});
is(0+@msg, 1, 'the default logger call Carp::cluck for 500 errors');
{ no warnings 'redefine'; *Carp::cluck = $real_cluck; }
}
# request_time
{
my ($headers, $content);
my $request;
my $inner_request_time;
my $inner_request_time_model;
{
package Controllers::RT;
our @ACTIONS = qw( show );
sub show {
my $t0 = time;
sleep 1 until time() > $t0;
$inner_request_time = Pinwheel::Controller::request_time();
$inner_request_time_model = Pinwheel::Controller::request_time_model();
Pinwheel::Controller::render(text => "");
}
}
$Pinwheel::Controller::map->reset();
Pinwheel::Controller::connect('/request_time', controller=> 'r_t', action => 'show');
my $run_test = sub {
my ($t) = @_;
$request = {
host => 'localhost',
path => '/request_time',
time => $t,
};
my $now = time;
$inner_request_time = 'x';
($headers, $content) = Pinwheel::Controller::dispatch($request);
if ($t)
{
is($inner_request_time, $t);
is($inner_request_time_model->timestamp, $t);
} else {
# dispatch defaults time to now
my $now2 = $now + 1; # iffy race condition avoidance
like($inner_request_time, qr/^($now|$now2)$/);
is($inner_request_time_model->timestamp, $inner_request_time);
}
};
&$run_test(time());
&$run_test(time()+100);
&$run_test(time()-100);
# dispatch defaults time to now
&$run_test(0);
&$run_test(undef);
}
{
print "# EXPORT tests\n";
eval 'package SomeFoo; use Pinwheel::Controller; 1';
die $@ if $@;
my @by_default = qw(
url_for
redirect_to
render
render_to_string
render_404
render_500
set_header
expires_in
expires_at
expires_now
accepts
respond_to
params
query_params
);
my @subs;
my $stash = \%SomeFoo::;
while (my ($name, $glob) = each %$stash)
{
defined &$glob or next;
push @subs, $name;
}
is(
join(" ", sort @subs),
join(" ", sort @by_default),
'default export list'
);
eval 'package SomeFoo; use Pinwheel::Controller qw(
connect
dispatch
format
set_base_format
set_format_param
expand_static_path
set_static_root
set_templates_root
request_time
request_time_model
)';
is($@, '', 'additional exports');
}