TCMS.t 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. #!/usr/bin/env perl
  2. # The OpenAPI document that TCMS.pm hands tPSGI, made from tCMS's own routing
  3. # table.
  4. use 5.040;
  5. use warnings;
  6. use FindBin;
  7. use lib "$FindBin::Bin/../lib";
  8. use Test::More;
  9. use Test::MockModule qw{strict};
  10. use JSON::MaybeXS ();
  11. use JSON::Validator::Schema::OpenAPIv3 ();
  12. our ( $REPO, $ROOT, $OLDCWD );
  13. BEGIN {
  14. require Cwd;
  15. require File::Copy;
  16. require File::Path;
  17. require File::Temp;
  18. $REPO = Cwd::abs_path("$FindBin::Bin/..");
  19. $OLDCWD = Cwd::getcwd();
  20. $ROOT = File::Temp::tempdir( 'tcms-routes-XXXXXX', TMPDIR => 1, CLEANUP => 1 );
  21. # TCMS.pm builds its document at file scope, against whatever cwd is
  22. # current when it loads, and every path in tCMS is relative to that.
  23. $ENV{HOME} = $ROOT;
  24. File::Path::make_path( map { "$ROOT/$_" } qw{config schema data/files logs totp www/statics www/themes} );
  25. File::Copy::copy( "$REPO/schema/$_", "$ROOT/schema/$_" ) or die $! for qw{auth.schema flatfile.schema log.schema};
  26. open( my $fh, '>', "$ROOT/config/default.cfg" ) or die $!;
  27. print {$fh} "[general]\n data_model=FlatFile\n hostname=routes.test.test\n";
  28. close $fh;
  29. chdir($ROOT) or die "could not chdir to the sandbox: $!";
  30. }
  31. END {
  32. chdir($OLDCWD) if $OLDCWD;
  33. }
  34. # The routes of posts, as the tag index of a FlatFile site gives them: by the
  35. # name of their callback.
  36. require_ok('Trog::SQLite::TagIndex') or BAIL_OUT("Can't load the tag index");
  37. my $index = Test::MockModule->new('Trog::SQLite::TagIndex');
  38. $index->redefine(
  39. routes => sub {
  40. return (
  41. '/posts/p-one' => { method => 'GET', callback => 'Trog::Routes::HTML::posts' },
  42. '/posts/{curly}' => { method => 'GET', callback => 'Trog::Routes::HTML::posts' },
  43. );
  44. }
  45. );
  46. # What a post's route runs, which the routes of posts name.
  47. require_ok('Trog::Routes::HTML') or BAIL_OUT("Can't load the routes");
  48. my $html = Test::MockModule->new('Trog::Routes::HTML');
  49. $html->redefine( posts => sub { return [ 200, [], ['the post'] ] } );
  50. require_ok('TCMS') or BAIL_OUT("Can't load the dispatcher");
  51. my %openapi = %TCMS::openapi;
  52. # Checked as JSON reads it, since tPSGI is not a prerequisite of tCMS: perl's
  53. # booleans become JSON's, and a callback becomes null.
  54. my $json = JSON::MaybeXS->new( allow_unknown => 1, canonical => 1 );
  55. my $schema = JSON::Validator::Schema::OpenAPIv3->new( $json->decode( $json->encode( \%openapi ) ) );
  56. my $paths = $openapi{paths};
  57. subtest 'the document' => sub {
  58. is_deeply( $schema->errors, [], 'is valid OpenAPI 3' ) or diag explain $schema->errors;
  59. ok( !exists $paths->{default}, 'and has no path for the setup route, which is not one' );
  60. ok( !( grep { m/[(]/ } keys(%$paths) ), 'nor any regex left' ) or diag explain [ grep { m/[(]/ } keys(%$paths) ];
  61. };
  62. subtest 'an operation' => sub {
  63. my $manual = $paths->{'/lib/{module+}'}{get};
  64. ok( $manual, 'the manual of a module is a GET of /lib/{module+}' );
  65. is( ref $manual->{'x-tpsgi-callback'}, 'CODE', 'with a callback' );
  66. is( $manual->{'x-tcms-auth'}, 1, 'and its auth as x-tcms-auth' );
  67. is_deeply(
  68. $manual->{parameters},
  69. [ { name => 'module+', in => 'path', required => true, schema => { type => 'string' } } ],
  70. 'and each variable of the template as a required path parameter'
  71. );
  72. my $totp = $paths->{'/totp'}{get};
  73. is( $totp->{'x-tcms-totp_exempt'}, 1, 'totp_exempt is x-tcms-totp_exempt' );
  74. is_deeply( $paths->{'/sitemap/{map}.xml'}{get}{'x-tpsgi-data'}, { xml => 1 }, 'and data is x-tpsgi-data' );
  75. ok( $paths->{'/auth'}{post} && !$paths->{'/auth'}{get}, 'the method is the key of the operation' );
  76. };
  77. subtest 'the routes of posts' => sub {
  78. is( ref $paths->{'/posts/p-one'}{get}{'x-tpsgi-callback'}, 'CODE', 'a post named by its callback is routed' );
  79. ok( $paths->{'/secure/posts/p-one'}{get}, 'and so is its /secure copy' );
  80. # The flag that says setup is done, or the default route answers instead.
  81. open( my $fh, '>', 'config/setup' ) or die $!;
  82. close $fh;
  83. my $res = $paths->{'/posts/p-one'}{get}{'x-tpsgi-callback'}->( { log_dir => "$ROOT/logs", verbose => 0 }, { route => '/posts/p-one', dispatcher => $paths->{'/posts/p-one'}{get} } );
  84. is_deeply( $res->[2], ['the post'], 'and it runs the sub that the name names' );
  85. ok( !exists $paths->{'/posts/{curly}'}, 'but a post whose path has a brace is not, since it would be a template' );
  86. };
  87. subtest 'validation' => sub {
  88. my $token = '0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0';
  89. is_deeply( [ $schema->validate_request( [ get => '/api/auth_change_request/{token}' ], { path => { token => $token } } ) ], [], 'a token that is a UUID is taken' );
  90. ok( scalar $schema->validate_request( [ get => '/api/auth_change_request/{token}' ], { path => { token => "$token|x" } } ), 'and one with more after it is not' );
  91. is_deeply( [ $schema->validate_request( [ get => '/api/requests_per' ], { query => { period => 'week', num_periods => 2 } } ) ], [], 'a period that requests_per knows is taken' );
  92. ok( scalar $schema->validate_request( [ get => '/api/requests_per' ], { query => { period => 'fortnight' } } ), 'and one it does not is refused' );
  93. ok( scalar $schema->validate_request( [ get => '/api/requests_per' ], { query => { num_periods => 'many' } } ), 'as is a count that is not a number' );
  94. };
  95. subtest 'robots' => sub {
  96. my $banned;
  97. my $renderer = Test::MockModule->new('Trog::Renderer');
  98. $renderer->redefine( render => sub { my ( $class, %args ) = @_; $banned = $args{data}{banned}; return [ 200, [], [''] ] } );
  99. TCMS::robots( { domain => 'routes.test.test' } );
  100. ok( ( grep { $_ eq '/avatar/*' } @$banned ), 'a template with no robot_name is disallowed with * for its variable' );
  101. ok( ( grep { $_ eq '/guest/reprovision/log/' } @$banned ), 'and one with a robot_name by that' );
  102. ok( !( grep { m/[{}]/ } @$banned ), 'and no template is written as it is' );
  103. };
  104. done_testing();