Each operation is dispatched as a request of its own, so a batch is held to the same validation and permissions as the calls it contains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 lines
6.3 KiB
PHP
182 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Api;
|
|
|
|
use App\Models\Member;
|
|
use App\Models\Technology;
|
|
use App\Models\User;
|
|
use Database\Seeders\RolePermissionSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\FakesPireon;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Batches and imports: several admin calls in one, each still validated and
|
|
* authorised on its own.
|
|
*/
|
|
class BatchApiTest extends TestCase
|
|
{
|
|
use FakesPireon, RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->seed(RolePermissionSeeder::class);
|
|
}
|
|
|
|
protected function actingWithRole(string $role): User
|
|
{
|
|
$user = User::factory()->create();
|
|
$user->syncRoles([$role]);
|
|
|
|
$this->withHeaders($this->apiHeadersFor($user));
|
|
|
|
return $user;
|
|
}
|
|
|
|
#[Test]
|
|
public function a_batch_runs_each_operation_and_reports_each_answer(): void
|
|
{
|
|
$this->actingWithRole('editor');
|
|
|
|
$member = Member::factory()->create();
|
|
|
|
$this->postJson('/api/v1/batch', [
|
|
'operations' => [
|
|
['method' => 'POST', 'path' => '/v1/technologies', 'body' => ['name' => 'Vue', 'slug' => 'vue']],
|
|
['method' => 'PATCH', 'path' => "/v1/members/{$member->id}", 'body' => ['github_username' => 'ada']],
|
|
['method' => 'POST', 'path' => '/v1/technologies', 'body' => ['name' => '']],
|
|
['method' => 'GET', 'path' => '/v1/me'],
|
|
],
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('data.0.status', 201)
|
|
->assertJsonPath('data.0.body.data.slug', 'vue')
|
|
->assertJsonPath('data.1.status', 200)
|
|
->assertJsonPath('data.2.status', 422)
|
|
->assertJsonPath('data.3.status', 200)
|
|
->assertJsonPath('meta.committed', true)
|
|
->assertJsonPath('meta.failed', 1);
|
|
|
|
$this->assertSame('ada', $member->fresh()->github_username);
|
|
$this->assertTrue(Technology::query()->where('slug', 'vue')->exists());
|
|
}
|
|
|
|
#[Test]
|
|
public function an_atomic_batch_keeps_nothing_when_one_fails(): void
|
|
{
|
|
$this->actingWithRole('editor');
|
|
|
|
$this->postJson('/api/v1/batch', [
|
|
'atomic' => true,
|
|
'operations' => [
|
|
['method' => 'POST', 'path' => '/v1/technologies', 'body' => ['name' => 'Vue', 'slug' => 'vue']],
|
|
['method' => 'POST', 'path' => '/v1/technologies', 'body' => ['name' => '']],
|
|
['method' => 'POST', 'path' => '/v1/technologies', 'body' => ['name' => 'Never', 'slug' => 'never']],
|
|
],
|
|
])
|
|
->assertOk()
|
|
->assertJsonCount(2, 'data')
|
|
->assertJsonPath('meta.committed', false);
|
|
|
|
$this->assertSame(0, Technology::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function a_batch_cannot_do_what_its_caller_cannot(): void
|
|
{
|
|
$this->actingWithRole('viewer');
|
|
|
|
$this->postJson('/api/v1/batch', [
|
|
'operations' => [['method' => 'POST', 'path' => '/v1/technologies', 'body' => ['name' => 'Vue', 'slug' => 'vue']]],
|
|
])->assertJsonPath('data.0.status', 403);
|
|
|
|
$this->assertSame(0, Technology::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function batches_do_not_nest_or_leave_the_admin_api(): void
|
|
{
|
|
$this->actingWithRole('editor');
|
|
|
|
foreach (['/v1/batch', '/v1/import/pages', '/v1/auth/token', '/public/v1/site', '/v1/../public/v1/site'] as $path) {
|
|
$this->postJson('/api/v1/batch', ['operations' => [['method' => 'POST', 'path' => $path]]])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors('operations.0.path');
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function an_import_creates_new_rows_and_updates_matching_ones(): void
|
|
{
|
|
$this->actingWithRole('editor');
|
|
|
|
Technology::factory()->create(['name' => 'Angular', 'slug' => 'angular', 'website' => null]);
|
|
|
|
$this->postJson('/api/v1/import/technologies', [
|
|
'match' => 'slug',
|
|
'rows' => [
|
|
['name' => 'Angular', 'slug' => 'angular', 'website' => 'https://angular.dev'],
|
|
['name' => 'Svelte', 'slug' => 'svelte', 'description' => ['es' => 'Compilador', 'en' => 'Compiler']],
|
|
],
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('data.0.method', 'PATCH')
|
|
->assertJsonPath('data.1.method', 'POST')
|
|
->assertJsonPath('meta.succeeded', 2);
|
|
|
|
$this->assertSame('https://angular.dev', Technology::query()->where('slug', 'angular')->value('website'));
|
|
$this->assertSame(2, Technology::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function a_csv_import_reads_languages_lists_and_booleans(): void
|
|
{
|
|
$this->actingWithRole('editor');
|
|
|
|
[$vue, $php] = Technology::factory()->count(2)->create();
|
|
|
|
$csv = "\xEF\xBB\xBFname,slug,headline.es,headline.en,technology_ids,is_public,github_username\n"
|
|
."Ada Lovelace,ada,Backend,Backend dev,{$vue->id}|{$php->id},true,\n"
|
|
."Linus,linus,Sistemas,,,false,linus\n";
|
|
|
|
$this->post('/api/v1/import/members', [
|
|
'file' => UploadedFile::fake()->createWithContent('members.csv', $csv),
|
|
], ['Accept' => 'application/json'])
|
|
->assertOk()
|
|
->assertJsonPath('meta.failed', 0);
|
|
|
|
$ada = Member::query()->where('slug', 'ada')->sole();
|
|
|
|
$this->assertSame('Backend dev', $ada->getTranslation('headline', 'en'));
|
|
$this->assertCount(2, $ada->technologies);
|
|
$this->assertTrue($ada->is_public);
|
|
$this->assertFalse(Member::query()->where('slug', 'linus')->sole()->is_public);
|
|
}
|
|
|
|
#[Test]
|
|
public function a_dry_run_validates_everything_and_keeps_nothing(): void
|
|
{
|
|
$this->actingWithRole('editor');
|
|
|
|
$this->postJson('/api/v1/import/technologies', [
|
|
'dry_run' => true,
|
|
'rows' => [
|
|
['name' => 'Vue', 'slug' => 'vue'],
|
|
['name' => 'Svelte', 'slug' => 'svelte'],
|
|
],
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('meta.dry_run', true)
|
|
->assertJsonPath('meta.committed', false)
|
|
->assertJsonPath('meta.succeeded', 2);
|
|
|
|
$this->assertSame(0, Technology::query()->count());
|
|
}
|
|
}
|