Manage calls and the inbox through the API
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Call;
|
||||
use App\Models\Category;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
@@ -78,6 +80,8 @@ class AdminApiContractTest extends TestCase
|
||||
'clients' => ['path' => '/v1/clients'],
|
||||
'testimonials' => ['path' => '/v1/testimonials'],
|
||||
'talks' => ['path' => '/v1/talks'],
|
||||
'calls' => ['path' => '/v1/calls'],
|
||||
'applications' => ['path' => '/v1/applications'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -93,6 +97,8 @@ class AdminApiContractTest extends TestCase
|
||||
$client = Client::factory()->create();
|
||||
$testimonial = Testimonial::factory()->create();
|
||||
$talk = Talk::factory()->create();
|
||||
$call = Call::factory()->create();
|
||||
$application = Application::factory()->for($call)->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
|
||||
@@ -117,6 +123,10 @@ class AdminApiContractTest extends TestCase
|
||||
'testimonial' => ["/v1/testimonials/{$testimonial->id}", '/v1/testimonials/{testimonial}'],
|
||||
'talks' => ['/v1/talks', '/v1/talks'],
|
||||
'talk' => ["/v1/talks/{$talk->id}", '/v1/talks/{talk}'],
|
||||
'calls' => ['/v1/calls', '/v1/calls'],
|
||||
'call' => ["/v1/calls/{$call->id}", '/v1/calls/{call}'],
|
||||
'applications' => ['/v1/applications', '/v1/applications'],
|
||||
'application' => ["/v1/applications/{$application->id}", '/v1/applications/{application}'],
|
||||
'users' => ['/v1/users', '/v1/users'],
|
||||
'user' => ["/v1/users/{$this->caller->id}", '/v1/users/{user}'],
|
||||
'me' => ['/v1/me', '/v1/me'],
|
||||
|
||||
180
tests/Feature/Api/CallsAndInboxApiTest.php
Normal file
180
tests/Feature/Api/CallsAndInboxApiTest.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Enums\ApplicationStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\Call;
|
||||
use App\Models\Technology;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FakesPireon;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Calls for contributors and the inbox they fill, through the admin API.
|
||||
*/
|
||||
class CallsAndInboxApiTest 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_call_is_opened_with_its_stack(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$technology = Technology::factory()->create();
|
||||
|
||||
$this->postJson('/api/v1/calls?lang=es', [
|
||||
'title' => ['es' => 'Backend en Laravel', 'en' => 'Laravel backend'],
|
||||
'slug' => ['es' => 'backend-laravel', 'en' => 'laravel-backend'],
|
||||
'hours_per_week' => 4,
|
||||
'duration_weeks' => 8,
|
||||
'seniority' => 'mid',
|
||||
'closes_at' => '2026-12-01',
|
||||
'technology_ids' => [$technology->id],
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.title', 'Backend en Laravel')
|
||||
->assertJsonPath('data.status', 'open')
|
||||
->assertJsonPath('data.technology_ids', [$technology->id])
|
||||
->assertJsonPath('data.applications_count', 0);
|
||||
|
||||
$this->postJson('/api/v1/calls', [
|
||||
'title' => ['es' => 'Otra'],
|
||||
'slug' => ['es' => 'backend-laravel'],
|
||||
'hours_per_week' => 60,
|
||||
'seniority' => 'guru',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['slug', 'hours_per_week', 'seniority']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_call_is_closed_and_then_deleted(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$call = Call::factory()->create();
|
||||
|
||||
$this->patchJson("/api/v1/calls/{$call->id}", ['status' => 'filled'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.status', 'filled');
|
||||
|
||||
$this->getJson('/api/v1/calls?status=open')->assertJsonCount(0, 'data');
|
||||
|
||||
$this->deleteJson("/api/v1/calls/{$call->id}")->assertNoContent();
|
||||
$this->assertModelMissing($call);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_inbox_leaves_spam_out_unless_asked(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
Application::factory()->create(['name' => 'Ada']);
|
||||
Application::factory()->create(['status' => ApplicationStatus::Spam]);
|
||||
|
||||
$this->getJson('/api/v1/applications')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.name', 'Ada')
|
||||
->assertJsonMissingPath('data.0.ip_hash');
|
||||
|
||||
$this->getJson('/api/v1/applications?status=spam')->assertJsonCount(1, 'data');
|
||||
$this->getJson('/api/v1/applications?search=ada')->assertJsonCount(1, 'data');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function handling_an_application_records_who_did_it(): void
|
||||
{
|
||||
$editor = $this->actingWithRole('editor');
|
||||
|
||||
$application = Application::factory()->create();
|
||||
|
||||
$this->patchJson("/api/v1/applications/{$application->id}", [
|
||||
'status' => 'contacted',
|
||||
'notes' => 'Llamada el jueves.',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.status', 'contacted')
|
||||
->assertJsonPath('data.notes', 'Llamada el jueves.')
|
||||
->assertJsonPath('data.handled_by.id', $editor->id);
|
||||
|
||||
$this->assertNotNull($application->fresh()->handled_at);
|
||||
|
||||
$this->patchJson("/api/v1/applications/{$application->id}", ['status' => 'new'])
|
||||
->assertJsonPath('data.handled_by', null)
|
||||
->assertJsonPath('data.handled_at', null);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function what_the_sender_wrote_cannot_be_changed(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$application = Application::factory()->create(['email' => 'real@example.org']);
|
||||
|
||||
$this->patchJson("/api/v1/applications/{$application->id}", ['email' => 'fake@example.org', 'status' => 'accepted'])
|
||||
->assertOk();
|
||||
|
||||
$this->assertSame('real@example.org', $application->fresh()->email);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function nobody_creates_an_application_here(): void
|
||||
{
|
||||
$this->actingWithRole('admin');
|
||||
|
||||
$this->postJson('/api/v1/applications', ['name' => 'X'])->assertMethodNotAllowed();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_writer_cannot_read_the_inbox(): void
|
||||
{
|
||||
$this->actingWithRole('writer');
|
||||
|
||||
$application = Application::factory()->create();
|
||||
|
||||
$this->getJson('/api/v1/applications')->assertForbidden();
|
||||
$this->getJson("/api/v1/applications/{$application->id}")->assertForbidden();
|
||||
|
||||
// Calls are theirs to read, like on the panel.
|
||||
$this->getJson('/api/v1/calls')->assertOk();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_deleted_application_can_be_brought_back(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$application = Application::factory()->create();
|
||||
|
||||
$this->deleteJson("/api/v1/applications/{$application->id}")->assertNoContent();
|
||||
$this->assertSoftDeleted($application);
|
||||
|
||||
$this->postJson("/api/v1/applications/{$application->id}/restore")->assertOk();
|
||||
$this->assertNotSoftDeleted($application);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user