Manage clients, testimonials and talks through the API
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,14 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
use App\Models\Page;
|
||||
use App\Models\Project;
|
||||
use App\Models\Service;
|
||||
use App\Models\Talk;
|
||||
use App\Models\Technology;
|
||||
use App\Models\Testimonial;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -33,7 +36,7 @@ use Tests\VerifiesOpenApiContract;
|
||||
* test exercises behaviour, not shape.
|
||||
*
|
||||
* Unlike the public listings, an admin index and its matching show endpoint
|
||||
* send the same full record — none of the four resources here has a
|
||||
* send the same full record — none of the resources here has a
|
||||
* "lightweight for a listing" projection — so both are held to the stricter
|
||||
* rule throughout: everything documented and not `nullable` must be present.
|
||||
*/
|
||||
@@ -72,6 +75,9 @@ class AdminApiContractTest extends TestCase
|
||||
'projects' => ['path' => '/v1/projects'],
|
||||
'members' => ['path' => '/v1/members'],
|
||||
'services' => ['path' => '/v1/services'],
|
||||
'clients' => ['path' => '/v1/clients'],
|
||||
'testimonials' => ['path' => '/v1/testimonials'],
|
||||
'talks' => ['path' => '/v1/talks'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -84,6 +90,9 @@ class AdminApiContractTest extends TestCase
|
||||
$project = Project::factory()->create();
|
||||
$member = Member::factory()->create();
|
||||
$service = Service::factory()->create();
|
||||
$client = Client::factory()->create();
|
||||
$testimonial = Testimonial::factory()->create();
|
||||
$talk = Talk::factory()->create();
|
||||
|
||||
// One request, one documented path — the URL a real caller hits and
|
||||
// the path key that describes it, side by side rather than derived
|
||||
@@ -102,6 +111,12 @@ class AdminApiContractTest extends TestCase
|
||||
'member' => ["/v1/members/{$member->id}", '/v1/members/{member}'],
|
||||
'services' => ['/v1/services', '/v1/services'],
|
||||
'service' => ["/v1/services/{$service->id}", '/v1/services/{service}'],
|
||||
'clients' => ['/v1/clients', '/v1/clients'],
|
||||
'client' => ["/v1/clients/{$client->id}", '/v1/clients/{client}'],
|
||||
'testimonials' => ['/v1/testimonials', '/v1/testimonials'],
|
||||
'testimonial' => ["/v1/testimonials/{$testimonial->id}", '/v1/testimonials/{testimonial}'],
|
||||
'talks' => ['/v1/talks', '/v1/talks'],
|
||||
'talk' => ["/v1/talks/{$talk->id}", '/v1/talks/{talk}'],
|
||||
'users' => ['/v1/users', '/v1/users'],
|
||||
'user' => ["/v1/users/{$this->caller->id}", '/v1/users/{user}'],
|
||||
'me' => ['/v1/me', '/v1/me'],
|
||||
|
||||
166
tests/Feature/Api/ShowcaseAdminApiTest.php
Normal file
166
tests/Feature/Api/ShowcaseAdminApiTest.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
use App\Models\Talk;
|
||||
use App\Models\Testimonial;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FakesPireon;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Clients, testimonials and talks through the admin API: the rest of what the
|
||||
* landing shows, so it can be loaded without the panel.
|
||||
*/
|
||||
class ShowcaseAdminApiTest 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_client_is_created_with_its_sector_in_several_languages(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$this->postJson('/api/v1/clients?lang=en&with=translations', [
|
||||
'name' => 'Asociación Vecinal',
|
||||
'slug' => 'asociacion-vecinal',
|
||||
'sector' => ['es' => 'Tercer sector', 'en' => 'Non-profit'],
|
||||
'url' => 'https://vecinos.example.org',
|
||||
'is_public' => true,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.sector', 'Non-profit')
|
||||
->assertJsonPath('data.translations.sector.es', 'Tercer sector')
|
||||
->assertJsonPath('data.projects_count', 0);
|
||||
|
||||
$this->postJson('/api/v1/clients', ['name' => 'Otra', 'slug' => 'asociacion-vecinal'])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('slug');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_deleted_client_can_be_brought_back(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$client = Client::factory()->create();
|
||||
|
||||
$this->deleteJson("/api/v1/clients/{$client->id}")->assertNoContent();
|
||||
$this->assertSoftDeleted($client);
|
||||
|
||||
$this->postJson("/api/v1/clients/{$client->id}/restore")->assertOk();
|
||||
$this->assertNotSoftDeleted($client);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_testimonial_keeps_the_language_it_was_given_in(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$client = Client::factory()->create();
|
||||
|
||||
$this->postJson('/api/v1/testimonials?lang=es', [
|
||||
'quote' => ['en' => 'They listened first.', 'es' => 'Primero escucharon.'],
|
||||
'original_locale' => 'en',
|
||||
'author_name' => 'Grace Hopper',
|
||||
'author_role' => ['es' => 'Coordinadora'],
|
||||
'client_id' => $client->id,
|
||||
'given_at' => '2026-05-01',
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.quote', 'Primero escucharon.')
|
||||
->assertJsonPath('data.original_locale', 'en')
|
||||
->assertJsonPath('data.client_id', $client->id)
|
||||
->assertJsonPath('data.given_at', '2026-05-01');
|
||||
|
||||
$this->assertSame('They listened first.', Testimonial::query()->sole()->getTranslation('quote', 'en'));
|
||||
|
||||
$this->postJson('/api/v1/testimonials', [
|
||||
'quote' => ['es' => 'x'],
|
||||
'original_locale' => 'tlh',
|
||||
'author_name' => 'Worf',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('original_locale');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_talk_is_filtered_by_who_gave_it_and_deleted_for_good(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$member = Member::factory()->create();
|
||||
|
||||
$id = $this->postJson('/api/v1/talks', [
|
||||
'title' => 'Angular sin miedo',
|
||||
'language' => 'es',
|
||||
'summary' => ['es' => 'Señales y SSR.', 'en' => 'Signals and SSR.'],
|
||||
'event' => 'Commit Conf',
|
||||
'held_at' => '2026-04-20',
|
||||
'member_id' => $member->id,
|
||||
'video_url' => 'https://video.example.org/1',
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.title', 'Angular sin miedo')
|
||||
->json('data.id');
|
||||
|
||||
Talk::factory()->create();
|
||||
|
||||
$this->getJson("/api/v1/talks?member_id={$member->id}&lang=en")
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.summary', 'Signals and SSR.');
|
||||
|
||||
$this->deleteJson("/api/v1/talks/{$id}")->assertNoContent();
|
||||
$this->assertDatabaseMissing('talks', ['id' => $id]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_writer_can_read_them_but_not_write_them(): void
|
||||
{
|
||||
$this->actingWithRole('writer');
|
||||
|
||||
$client = Client::factory()->create();
|
||||
|
||||
$this->getJson('/api/v1/clients')->assertOk();
|
||||
$this->getJson('/api/v1/testimonials')->assertOk();
|
||||
$this->getJson('/api/v1/talks')->assertOk();
|
||||
|
||||
$this->patchJson("/api/v1/clients/{$client->id}", ['name' => 'Otro'])->assertForbidden();
|
||||
$this->postJson('/api/v1/talks', ['title' => 'X', 'language' => 'es', 'event' => 'Y'])->assertForbidden();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_logo_is_uploaded_for_a_client(): void
|
||||
{
|
||||
$this->actingWithRole('writer');
|
||||
|
||||
// Uploading for a client takes permission to write clients.
|
||||
$this->postJson('/api/v1/media', ['url' => 'https://example.org/a.png', 'for' => 'clients'])
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user