'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); } }