This commit is contained in:
Yangxt
2026-09-30 11:11:54 +08:00
parent cb9b7a4bd4
commit 2fb57ec490
160 changed files with 14764 additions and 53 deletions
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class ApiExceptionTest extends TestCase
{
public function test_unknown_api_route_uses_the_failure_envelope(): void
{
config(['app.debug' => true]);
$response = $this->getJson('/api/v1/does-not-exist');
$response->assertNotFound()
->assertExactJson([
'success' => false,
'message' => '资源不存在',
'errors' => [],
]);
}
public function test_validation_errors_use_the_failure_envelope(): void
{
Route::post('/api/v1/_validation_probe', function (Request $request) {
$request->validate(['name' => ['required', 'string']]);
return ApiResponse::success();
});
$response = $this->postJson('/api/v1/_validation_probe', []);
$response->assertStatus(422)
->assertJsonPath('success', false)
->assertJsonPath('message', '参数错误')
->assertJsonStructure(['errors' => ['name']]);
}
public function test_method_not_allowed_uses_the_failure_envelope(): void
{
$this->postJson('/api/v1/health')
->assertStatus(405)
->assertJsonPath('success', false)
->assertJsonPath('message', '方法不允许');
}
public function test_paginated_payload_uses_meta(): void
{
$paginator = new LengthAwarePaginator(
items: [['sku_code' => 'A-1']],
total: 21,
perPage: 20,
currentPage: 1,
);
$payload = ApiResponse::paginated($paginator)->getData(true);
$this->assertSame([
'success' => true,
'message' => 'OK',
'data' => [
['sku_code' => 'A-1'],
],
'meta' => [
'current_page' => 1,
'per_page' => 20,
'total' => 21,
'last_page' => 2,
],
], $payload);
}
public function test_per_page_is_capped(): void
{
$this->assertSame(20, ApiResponse::perPage(null));
$this->assertSame(20, ApiResponse::perPage(0));
$this->assertSame(50, ApiResponse::perPage('50'));
$this->assertSame(100, ApiResponse::perPage(500));
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Tests\TestCase;
class HealthTest extends TestCase
{
public function test_health_returns_the_standard_envelope(): void
{
$response = $this->getJson('/api/v1/health');
$response->assertOk()
->assertExactJson([
'success' => true,
'message' => 'OK',
'data' => [
'status' => 'ok',
],
]);
$response->assertHeader('X-Request-Id');
}
public function test_health_keeps_a_valid_request_id(): void
{
$response = $this->withHeader('X-Request-Id', 'req_123')
->getJson('/api/v1/health');
$response->assertOk();
$response->assertHeader('X-Request-Id', 'req_123');
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Support\MissingTenantContextException;
use App\Support\TenantContext;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Tests\Fixtures\TenantProbe;
use Tests\TestCase;
class TenantScopeTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Schema::dropIfExists('tenant_probes');
Schema::create('tenant_probes', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code', 32);
$table->string('name');
$table->timestamps();
});
}
protected function tearDown(): void
{
Schema::dropIfExists('tenant_probes');
parent::tearDown();
}
public function test_queries_only_return_the_current_tenant(): void
{
$context = app(TenantContext::class);
$context->set('tenant-a');
TenantProbe::query()->create(['name' => 'alpha']);
$context->set('tenant-b');
TenantProbe::query()->create(['name' => 'beta']);
$context->set('tenant-a');
$this->assertSame(['alpha'], TenantProbe::query()->pluck('name')->all());
}
public function test_create_always_uses_the_current_tenant(): void
{
app(TenantContext::class)->set('tenant-a');
TenantProbe::query()->create([
'name' => 'alpha',
'tenant_code' => 'tenant-b',
]);
$this->assertDatabaseHas('tenant_probes', [
'name' => 'alpha',
'tenant_code' => 'tenant-a',
]);
}
public function test_tenant_code_cannot_be_reassigned(): void
{
app(TenantContext::class)->set('tenant-a');
$row = TenantProbe::query()->create(['name' => 'alpha']);
$row->tenant_code = 'tenant-b';
$row->name = 'renamed';
$row->save();
$this->assertDatabaseHas('tenant_probes', [
'id' => $row->id,
'tenant_code' => 'tenant-a',
'name' => 'renamed',
]);
}
public function test_query_without_context_fails_closed(): void
{
$this->expectException(MissingTenantContextException::class);
TenantProbe::query()->get();
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Tests\Fixtures;
use App\Models\Concerns\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
class TenantProbe extends Model
{
use BelongsToTenant;
protected $table = 'tenant_probes';
protected $guarded = [];
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Support\MissingTenantContextException;
use App\Support\TenantContext;
use InvalidArgumentException;
use Tests\TestCase;
class TenantContextTest extends TestCase
{
public function test_code_requires_a_tenant(): void
{
$this->expectException(MissingTenantContextException::class);
app(TenantContext::class)->code();
}
public function test_empty_tenant_code_is_rejected(): void
{
$this->expectException(InvalidArgumentException::class);
app(TenantContext::class)->set('');
}
public function test_cache_and_lock_keys_include_the_tenant(): void
{
$context = app(TenantContext::class);
$context->set('acme');
$this->assertSame('tenant:acme:sku:1', $context->cacheKey('sku:1'));
$this->assertSame('lock:tenant:acme:inventory:1', $context->lockKey('inventory:1'));
}
public function test_run_restores_the_previous_tenant(): void
{
$context = app(TenantContext::class);
$context->set('tenant-a');
$seen = $context->run('tenant-b', fn (): string => $context->code());
$this->assertSame('tenant-b', $seen);
$this->assertSame('tenant-a', $context->code());
}
public function test_run_clears_context_when_none_was_set(): void
{
$context = app(TenantContext::class);
$context->run('tenant-b', fn (): string => $context->code());
$this->assertFalse($context->has());
}
}