feat: 前台 .shtml 后缀 canonical + strict_types 全量启用 + 对外 API(Sanctum+Scramble 文档)+ pm2 ecosystem + GitHub Actions CI + 插件/主题/架构/API 文档 + REDIS_PREFIX 说明

This commit is contained in:
ak
2026-08-11 19:37:33 +08:00
parent 1c8a801238
commit fc6624cf84
244 changed files with 1902 additions and 97 deletions
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace Tests\Feature;
use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ApiTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed();
$user = User::create(['name' => '作者', 'email' => 'w@t.com', 'password' => bcrypt('x')]);
$cat = Category::create(['name' => '技术', 'slug' => 'tech']);
Post::create([
'user_id' => $user->id,
'category_id' => $cat->id,
'title' => 'API 测试文章',
'slug' => 'api-post',
'content' => '**加粗** 内容',
'content_format' => 'markdown',
'status' => 'published',
'published_at' => now(),
]);
}
public function test_site_endpoint(): void
{
$this->getJson('/api/site')->assertOk()->assertJsonPath('api_version', '1.0');
}
public function test_posts_list_and_detail(): void
{
$this->getJson('/api/posts')->assertOk()->assertJsonCount(1, 'data');
$this->getJson('/api/posts/api-post')
->assertOk()
->assertJsonPath('title', 'API 测试文章')
->assertJsonPath('category.name', '技术');
}
public function test_posts_search_and_filters(): void
{
$this->getJson('/api/posts?q=API')->assertOk()->assertJsonCount(1, 'data');
$this->getJson('/api/posts?category=tech')->assertOk()->assertJsonCount(1, 'data');
$this->getJson('/api/posts?q=不存在的词')->assertOk()->assertJsonCount(0, 'data');
}
public function test_categories_and_tags(): void
{
$this->getJson('/api/categories')->assertOk()->assertJsonCount(1, 'data');
$this->getJson('/api/tags')->assertOk();
}
public function test_store_comment(): void
{
$post = Post::where('slug', 'api-post')->first();
$this->postJson('/api/comments', [
'post_id' => $post->id,
'author_name' => 'API 访客',
'content' => '来自 API 的评论',
'website' => '',
])->assertCreated()->assertJsonPath('data.status', 'published');
$this->assertDatabaseHas('comments', ['author_name' => 'API 访客']);
}
public function test_comment_honeypot_rejected(): void
{
$post = Post::where('slug', 'api-post')->first();
$this->postJson('/api/comments', [
'post_id' => $post->id,
'author_name' => '机器人',
'content' => 'spam',
'website' => 'http://spam.example.com',
])->assertStatus(422);
}
public function test_me_requires_token(): void
{
$this->getJson('/api/me')->assertUnauthorized();
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ class ExampleTest extends TestCase
$this->seed();
$this->get('/')->assertStatus(200);
$this->get('/archives')->assertStatus(200);
$this->get('/archives.shtml')->assertStatus(200);
$this->get('/rss.xml')->assertStatus(200);
$this->get('/sitemap.xml')->assertStatus(200);
$this->get('/robots.txt')->assertStatus(200);
+30 -12
View File
@@ -41,25 +41,43 @@ class LegacyRoutesTest extends TestCase
public function test_rewrite_style_urls_redirect(): void
{
$this->get('/show-12.html')->assertRedirect('/posts/legacy-post');
$this->get('/show-12-1.html')->assertRedirect('/posts/legacy-post');
$this->get('/show-12.html')->assertRedirect('/posts/legacy-post.shtml');
$this->get('/show-12-1.html')->assertRedirect('/posts/legacy-post.shtml');
$this->get('/category-1.html')->assertRedirect();
$this->get('/archives-'.now()->format('Ymd').'.html')->assertRedirect();
$this->get('/tagslist.html')->assertRedirect('/tags');
$this->get('/links.shtml')->assertRedirect('/links');
$this->get('/login.shtml')->assertRedirect('/login');
$this->get('/reg.shtml')->assertRedirect('/register');
$this->get('/tagslist.html')->assertRedirect('/tags.shtml');
$this->get('/reg.shtml')->assertRedirect('/register.shtml');
}
public function test_shtml_canonical_pages_render(): void
{
$this->get('/links.shtml')->assertOk();
$this->get('/login.shtml')->assertOk();
$this->get('/comments.shtml')->assertOk();
$this->get('/search.shtml')->assertOk();
$this->get('/tags.shtml')->assertOk();
$this->get('/archives.shtml')->assertOk();
$this->get('/posts/legacy-post.shtml')->assertOk();
}
public function test_no_suffix_urls_redirect_to_shtml(): void
{
$this->get('/links')->assertRedirect('/links.shtml');
$this->get('/login')->assertRedirect('/login.shtml');
$this->get('/comments')->assertRedirect('/comments.shtml');
$this->get('/archives')->assertRedirect('/archives.shtml');
$this->get('/posts/legacy-post')->assertRedirect('/posts/legacy-post.shtml');
}
public function test_query_style_urls_redirect(): void
{
$this->get('/?action=show&id=12')->assertRedirect('/posts/legacy-post');
$this->get('/?action=show&id=12')->assertRedirect('/posts/legacy-post.shtml');
$this->get('/?action=category&cid=1')->assertRedirect();
$this->get('/?action=index&setdate='.now()->format('Ym'))->assertRedirect();
$this->get('/?action=search&keyword=laravel')->assertRedirect('/search?q=laravel');
$this->get('/?action=links')->assertRedirect('/links');
$this->get('/?action=archives')->assertRedirect('/archives');
$this->get('/?action=comments')->assertRedirect('/comments');
$this->get('/?action=search&keyword=laravel')->assertRedirect('/search.shtml?q=laravel');
$this->get('/?action=links')->assertRedirect('/links.shtml');
$this->get('/?action=archives')->assertRedirect('/archives.shtml');
$this->get('/?action=comments')->assertRedirect('/comments.shtml');
}
public function test_trackback_endpoints_are_gone(): void
@@ -86,6 +104,6 @@ class LegacyRoutesTest extends TestCase
$tag = Tag::create(['name' => 'laravel-tag', 'type' => 'post']);
Setting::set('legacy_tags', [1 => $tag->id]);
$this->get('/?action=tags&id=1')->assertRedirect('/tags/laravel-tag');
$this->get('/?action=tags&id=1')->assertRedirect('/tags/laravel-tag.shtml');
}
}
+2 -2
View File
@@ -87,7 +87,7 @@ class MembershipFlowTest extends TestCase
]);
// 游客:付费部分被替换
$this->get('/posts/paid-post')
$this->get('/posts/paid-post.shtml')
->assertOk()
->assertDontSee('付费隐藏内容')
->assertSee('付费内容已隐藏');
@@ -97,7 +97,7 @@ class MembershipFlowTest extends TestCase
$payment = Payment::query()->where('user_id', $this->user->id)->first();
$this->actingAs($this->user)->post('/pay/sandbox/'.$payment->order_no.'/confirm');
$this->actingAs($this->user)->get('/posts/paid-post')
$this->actingAs($this->user)->get('/posts/paid-post.shtml')
->assertOk()
->assertSee('付费隐藏内容');
}
+2 -2
View File
@@ -37,14 +37,14 @@ class ThemeTest extends TestCase
{
app(\App\Blog\Support\ThemeManager::class)->activate('sablog');
$this->get('/')->assertOk()->assertSee('outmain');
$this->get('/posts/theme-test')->assertOk()->assertSee('outmain');
$this->get('/posts/theme-test.shtml')->assertOk()->assertSee('outmain');
}
public function test_modern_theme_renders_modern_layout(): void
{
app(\App\Blog\Support\ThemeManager::class)->activate('modern');
$this->get('/')->assertOk()->assertSee('m-header');
$this->get('/posts/theme-test')->assertOk()->assertSee('m-header');
$this->get('/posts/theme-test.shtml')->assertOk()->assertSee('m-header');
}
public function test_theme_assets_are_served(): void