Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_DATABASE" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
Expand Down
12 changes: 10 additions & 2 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,13 @@
Route::get('/author/{author}', [PostController::class, 'authorPosts'])->name('author.posts');
Route::get('/category/{category}', [PostController::class, 'categoryPosts'])->name('category.posts');
Route::get('/tag/{slug}', [PostController::class, 'tagPosts'])->name('tag.posts');
Route::get('/{year}/{month}', [PostController::class, 'yearMonthPosts'])->name('year-month.posts');
Route::get('/{year}/{month}/{slug}', [PostController::class, 'show'])->name('post');
// Mirrors the WordPress permalink structure (%year%/%monthnum%/%postname%).
// Without the constraints these two act as catch-alls for any 2- and
// 3-segment path, answering 200 with an empty archive instead of 404.
Route::get('/{year}/{month}', [PostController::class, 'yearMonthPosts'])
->where(['year' => '[0-9]{4}', 'month' => '0[1-9]|1[0-2]'])
->name('year-month.posts');

Route::get('/{year}/{month}/{slug}', [PostController::class, 'show'])
->where(['year' => '[0-9]{4}', 'month' => '0[1-9]|1[0-2]'])
->name('post');
55 changes: 55 additions & 0 deletions tests/Feature/ArchiveRouteConstraintsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/**
* These assertions never reach a controller: an unmatched route is rejected by
* the router itself, so no WordPress table is required to run them.
*/
it('rejects non-numeric archive paths', function (string $path) {
$this->get($path)->assertNotFound();
})->with([
'/foo/bar',
'/abcd/ef',
'/wp-admin/setup-config.php',
'/assets/app',
]);

it('rejects out-of-range months', function (string $path) {
$this->get($path)->assertNotFound();
})->with([
'/2026/00',
'/2026/13',
'/2026/99',
'/2026/7', // month must be zero padded, as WordPress emits it
'/2026/007',
]);

it('rejects malformed years', function (string $path) {
$this->get($path)->assertNotFound();
})->with([
'/26/07',
'/20260/07',
'/2026a/07',
]);

it('rejects the same malformed paths on the single post route', function (string $path) {
$this->get($path)->assertNotFound();
})->with([
'/foo/bar/some-slug',
'/2026/13/some-slug',
'/2026/7/some-slug',
]);

it('still matches well formed archive paths', function () {
// Reaching the controller means the route matched. The controller then hits
// wp_posts, which does not exist here, so anything other than 404 proves
// the constraint let the request through.
$this->get('/2026/07')->assertStatus(500);
});

it('does not shadow the named static routes', function () {
expect(route('year-month.posts', ['year' => '2026', 'month' => '07']))
->toEndWith('/2026/07');

expect(route('post', ['year' => '2026', 'month' => '07', 'slug' => 'meu-post']))
->toEndWith('/2026/07/meu-post');
});