Files
construprogress/tests/Feature/Api/MobileApiTest.php
T

192 lines
6.7 KiB
PHP
Raw Normal View History

<?php
namespace Tests\Feature\Api;
use App\Models\Phase;
use App\Models\Project;
use App\Models\ProgressUpdate;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;
class MobileApiTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Permission::findOrCreate('update progress');
Permission::findOrCreate('manage all');
}
private function makeProject(?User $member = null): Project
{
$project = Project::create([
'reference' => 'TEST-1',
'name' => 'Proyecto Test',
'address' => 'Calle Falsa 123',
'lat' => 40.0,
'lng' => -3.0,
'start_date' => now()->toDateString(),
'end_date_estimated' => now()->addMonths(6)->toDateString(),
'status' => 'in_progress',
'created_by' => $member?->id ?? User::factory()->create()->id,
]);
if ($member) {
$project->users()->attach($member->id, ['role_in_project' => 'supervisor']);
}
return $project;
}
private function makePhase(Project $project): Phase
{
return Phase::create([
'project_id' => $project->id,
'name' => 'Fase 1',
'order' => 1,
'color' => '#3b82f6',
'progress_percent' => 0,
]);
}
// ── Auth ───────────────────────────────────────────────────────────────────
public function test_login_returns_a_token(): void
{
$user = User::factory()->create();
$res = $this->postJson('/api/v1/login', [
'email' => $user->email,
'password' => 'password',
'device_name' => 'Pixel-Test',
]);
$res->assertOk()->assertJsonStructure(['token', 'user' => ['id', 'email', 'permissions']]);
$this->assertDatabaseHas('devices', ['user_id' => $user->id, 'name' => 'Pixel-Test']);
}
public function test_login_fails_with_wrong_password(): void
{
$user = User::factory()->create();
$this->postJson('/api/v1/login', [
'email' => $user->email,
'password' => 'incorrecta',
'device_name' => 'Pixel-Test',
])->assertStatus(422);
}
public function test_me_requires_authentication(): void
{
$this->getJson('/api/v1/me')->assertStatus(401);
}
public function test_me_returns_user_with_token(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user, ['mobile-sync']);
$this->getJson('/api/v1/me')
->assertOk()
->assertJsonPath('user.id', $user->id);
}
// ── Pull ─────────────────────────────────────────────────────────────────────
public function test_projects_index_only_returns_accessible_projects(): void
{
$user = User::factory()->create();
$mine = $this->makeProject($user);
$other = $this->makeProject(); // not a member
Sanctum::actingAs($user, ['mobile-sync']);
$res = $this->getJson('/api/v1/projects')->assertOk();
$ids = collect($res->json('projects'))->pluck('id');
$this->assertTrue($ids->contains($mine->id));
$this->assertFalse($ids->contains($other->id));
}
public function test_bundle_is_forbidden_for_non_member(): void
{
$user = User::factory()->create();
$project = $this->makeProject(); // user is not a member
Sanctum::actingAs($user, ['mobile-sync']);
$this->getJson("/api/v1/projects/{$project->id}/bundle")->assertStatus(403);
}
public function test_bundle_returns_structure_for_member(): void
{
$user = User::factory()->create();
$project = $this->makeProject($user);
$phase = $this->makePhase($project);
Sanctum::actingAs($user, ['mobile-sync']);
$this->getJson("/api/v1/projects/{$project->id}/bundle")
->assertOk()
->assertJsonPath('project.id', $project->id)
->assertJsonPath('phases.0.id', $phase->id);
}
// ── Push (sync) ──────────────────────────────────────────────────────────────
public function test_sync_creates_progress_update_and_is_idempotent(): void
{
$user = User::factory()->create();
$user->givePermissionTo('update progress');
$project = $this->makeProject($user);
$phase = $this->makePhase($project);
$uuid = (string) Str::uuid();
Sanctum::actingAs($user, ['mobile-sync']);
$payload = ['operations' => [[
'entity' => 'progress_update', 'op' => 'create', 'uuid' => $uuid,
'client_updated_at' => now()->toIso8601String(),
'data' => ['phase_id' => $phase->id, 'progress' => 75, 'comment' => 'Avance'],
]]];
// First push → applied
$this->postJson('/api/v1/sync', $payload)
->assertOk()
->assertJsonPath('results.0.status', 'applied');
$this->assertDatabaseHas('progress_updates', ['uuid' => $uuid, 'progress_percent' => 75, 'user_id' => $user->id]);
$this->assertEquals(75, $phase->fresh()->progress_percent);
// Re-send same uuid → duplicate (no second row)
$this->postJson('/api/v1/sync', $payload)
->assertOk()
->assertJsonPath('results.0.status', 'duplicate');
$this->assertEquals(1, ProgressUpdate::where('uuid', $uuid)->count());
}
public function test_sync_returns_error_without_permission(): void
{
$user = User::factory()->create(); // member but WITHOUT 'update progress'
$project = $this->makeProject($user);
$phase = $this->makePhase($project);
Sanctum::actingAs($user, ['mobile-sync']);
$this->postJson('/api/v1/sync', ['operations' => [[
'entity' => 'progress_update', 'op' => 'create', 'uuid' => (string) Str::uuid(),
'data' => ['phase_id' => $phase->id, 'progress' => 50],
]]])
->assertOk()
->assertJsonPath('results.0.status', 'error');
$this->assertDatabaseCount('progress_updates', 0);
}
}