init
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user