|
|
@@ -30,7 +30,12 @@ sub operation {
|
|
|
|
|
|
sub document {
|
|
|
my (%paths) = @_;
|
|
|
- return { openapi => '3.0.3', info => { title => 'test', version => '1' }, paths => \%paths };
|
|
|
+ return {
|
|
|
+ openapi => '3.0.3',
|
|
|
+ info => { title => 'test', version => '1' },
|
|
|
+ components => { schemas => { Count => { type => 'object', properties => { count => { type => 'integer' } } } } },
|
|
|
+ paths => \%paths,
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
my $router = TPSGI::Router->new(
|
|
|
@@ -58,6 +63,32 @@ my $router = TPSGI::Router->new(
|
|
|
},
|
|
|
),
|
|
|
},
|
|
|
+ '/upload' => {
|
|
|
+ post => operation(
|
|
|
+ requestBody => {
|
|
|
+ content => {
|
|
|
+ 'multipart/form-data' => {
|
|
|
+ schema => {
|
|
|
+ type => 'object',
|
|
|
+ properties => { count => { type => 'integer' }, file => { type => 'string', format => 'binary' } },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ 'application/x-www-form-urlencoded' => { schema => { '$ref' => '#/components/schemas/Count' } },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ '/open' => {
|
|
|
+ post => operation(
|
|
|
+ requestBody => {
|
|
|
+ content => {
|
|
|
+ 'application/x-www-form-urlencoded' => {
|
|
|
+ schema => { type => 'object', additionalProperties => true, properties => { count => { type => 'integer' } } },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ },
|
|
|
'/search' => {
|
|
|
get => operation(
|
|
|
parameters => [
|
|
|
@@ -180,8 +211,11 @@ subtest 'validation' => sub {
|
|
|
( $status, @errors ) = $router->validate( $route, query => { limit => 1, junk => 1 } );
|
|
|
is( $status, 400, 'an undeclared query key is a 400' );
|
|
|
like( \@errors, [qr/\/junk:/], 'and named' );
|
|
|
- is( [ $router->validate( $route, query => { limit => 1, route => '/x' } ) ], [], 'except a key that tPSGI sets itself' );
|
|
|
- is( [ $router->validate( $router->match( 'GET', '/loose' ), query => { anything => 1 } ) ], [], 'and an operation that declares nothing takes anything' );
|
|
|
+ is( [ $router->validate( $route, query => { limit => 1, route => '/x' } ) ], [], 'except a key that tPSGI sets itself' );
|
|
|
+ ( $status, @errors ) = $router->validate( $router->match( 'GET', '/loose' ), query => { anything => 1 } );
|
|
|
+ is( $status, 400, 'and so is any key of the query, for an operation that declares nothing' );
|
|
|
+ like( \@errors, [qr/\/anything:/], 'named too' );
|
|
|
+ is( [ $router->validate( $router->match( 'GET', '/loose' ), query => {} ) ], [], 'which takes an empty query' );
|
|
|
|
|
|
$route = $router->match( 'POST', '/form' );
|
|
|
my $form_type = 'application/x-www-form-urlencoded';
|
|
|
@@ -194,6 +228,35 @@ subtest 'validation' => sub {
|
|
|
is( [ $router->validate( $route, body => { count => 3 }, content_type => "$form_type; charset=UTF-8" ) ], [], 'and parameters of the media type do not count' );
|
|
|
};
|
|
|
|
|
|
+subtest 'the names of a body' => sub {
|
|
|
+ my $form_type = 'application/x-www-form-urlencoded';
|
|
|
+ my $multipart = 'multipart/form-data; boundary=x';
|
|
|
+
|
|
|
+ my ( $status, @errors ) = $router->validate( $router->match( 'GET', '/loose' ), body => { count => 3 }, content_type => $form_type );
|
|
|
+ is( $status, 400, 'a field, to an operation with no requestBody, is a 400' );
|
|
|
+ like( \@errors, [qr/\/body\/count:[ ]the[ ]operation[ ]takes[ ]no[ ]body/], 'for that reason' );
|
|
|
+ ( $status, @errors ) = $router->validate( $router->match( 'GET', '/loose' ), body => {}, uploads => ['file'], content_type => $multipart );
|
|
|
+ is( $status, 400, 'and so is a file' );
|
|
|
+ like( \@errors, [qr/\/body\/file:/], 'named' );
|
|
|
+ is( [ $router->validate( $router->match( 'GET', '/loose' ), body => {}, content_type => $form_type ) ], [], 'but an empty body is not' );
|
|
|
+
|
|
|
+ my $route = $router->match( 'POST', '/upload' );
|
|
|
+ is( [ $router->validate( $route, body => { count => 3 }, uploads => ['file'], content_type => $multipart ) ], [], 'a field and a file that the schema names pass' );
|
|
|
+ ( $status, @errors ) = $router->validate( $route, body => { count => 3, junk => 1 }, content_type => $multipart );
|
|
|
+ is( $status, 400, 'a field that a schema without additionalProperties does not name is a 400' );
|
|
|
+ like( \@errors, [qr/\/body\/junk:[ ]the[ ]schema[ ]of[ ]multipart\/form-data/], 'which names the media type' );
|
|
|
+ ( $status, @errors ) = $router->validate( $route, body => { count => 3 }, uploads => ['other'], content_type => $multipart );
|
|
|
+ is( $status, 400, 'and so is a file that it does not name' );
|
|
|
+ like( \@errors, [qr/\/body\/other:/], 'named' );
|
|
|
+
|
|
|
+ ( $status, @errors ) = $router->validate( $route, body => { count => 3, file => 'x' }, content_type => $form_type );
|
|
|
+ is( $status, 400, 'the names are those of the media type that was sent, here a $ref' );
|
|
|
+ like( \@errors, [qr/\/body\/file:[ ]the[ ]schema[ ]of[ ]\Q$form_type\E/], 'which does not name the file' );
|
|
|
+ is( [ $router->validate( $route, body => { count => 3 }, content_type => $form_type ) ], [], 'and does name the count' );
|
|
|
+
|
|
|
+ is( [ $router->validate( $router->match( 'POST', '/open' ), body => { count => 3, junk => 1 }, content_type => $form_type ) ], [], 'a schema with additionalProperties takes what it says it takes' );
|
|
|
+};
|
|
|
+
|
|
|
subtest 'precedence' => sub {
|
|
|
my @order = map { $_->{template} } grep { index( $_->{template}, '/sitemap/' ) == 0 || index( $_->{template}, '/lib/' ) == 0 } $router->routes();
|
|
|
is( \@order, [qw{/lib/index /sitemap/static.xml /sitemap/{map}.xml.gz /sitemap/{map}.xml /lib/{module+}}], 'literal routes first, then by segment, then by literal length' );
|