create(); $workspace = Workspace::factory()->create(); $workspace->users()->attach($user->id, ['role' => $role, 'permissions' => []]); $declarations = Declaration::factory() ->forWorkspace($workspace) ->count($declarationCount) ->create(['status' => DeclarationStatus::EnAttenteClient]); session(['current_workspace_id' => $workspace->id]); return [$user, $workspace, $declarations]; } // ── AC1,3,4: Owner/Manager can bulk-notify clients ────── test('owner can bulk-notify clients — emails queued, activity logged, success flash', function () { Mail::fake(); [$user, $workspace, $declarations] = setupBulkNotifyTest('owner'); $ids = $declarations->pluck('id')->all(); $response = $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => $ids, ]); $response->assertRedirect(); $response->assertSessionHas('flash', [ 'type' => 'success', 'message' => '3 notifications envoyées', ]); Mail::assertQueued(DeclarationFileRequestMail::class, 3); }); test('manager can bulk-notify clients — same behavior as owner', function () { Mail::fake(); [$user, $workspace, $declarations] = setupBulkNotifyTest('manager'); $ids = $declarations->pluck('id')->all(); $response = $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => $ids, ]); $response->assertRedirect(); $response->assertSessionHas('flash', [ 'type' => 'success', 'message' => '3 notifications envoyées', ]); Mail::assertQueued(DeclarationFileRequestMail::class, 3); }); // ── AC5: Worker cannot access bulk-notify (consistent with canEdit, canDelete, canNudge) ── test('worker is forbidden from bulk-notify endpoint', function () { Mail::fake(); $worker = User::factory()->create(); $workspace = Workspace::factory()->create(); $workspace->users()->attach($worker->id, ['role' => 'worker', 'permissions' => []]); $declaration = Declaration::factory()->forWorkspace($workspace)->create([ 'status' => DeclarationStatus::EnAttenteClient, 'assigned_to' => $worker->id, ]); session(['current_workspace_id' => $workspace->id]); $response = $this->actingAs($worker)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => [$declaration->id], ]); $response->assertForbidden(); Mail::assertNothingQueued(); }); // ── AC3: Rejects declarations not in en_attente_client status ── test('rejects declarations not in en_attente_client status', function () { Mail::fake(); [$user, $workspace, $declarations] = setupBulkNotifyTest('owner', 2); // Change one declaration to a different status $declarations[0]->update(['status' => DeclarationStatus::EnCours]); $ids = $declarations->pluck('id')->all(); $response = $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => $ids, ]); $response->assertRedirect(); $response->assertSessionHas('flash', [ 'type' => 'success', 'message' => '1 notifications envoyées', ]); // Only the one remaining en_attente_client declaration should get an email Mail::assertQueued(DeclarationFileRequestMail::class, 1); }); // ── AC4: Workspace boundary enforcement ── test('workspace boundary enforcement — declarations from other workspace ignored', function () { Mail::fake(); [$user, $workspace, $declarations] = setupBulkNotifyTest('owner', 1); $otherWorkspace = Workspace::factory()->create(); $otherDeclaration = Declaration::factory()->forWorkspace($otherWorkspace)->create([ 'status' => DeclarationStatus::EnAttenteClient, ]); $response = $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => [$declarations[0]->id, $otherDeclaration->id], ]); $response->assertRedirect(); // Only 1 email (from own workspace), the other is filtered out Mail::assertQueued(DeclarationFileRequestMail::class, 1); }); // ── AC4: Activity log records bulk operation ── test('activity log records bulk operation with count and actor', function () { Mail::fake(); [$user, $workspace, $declarations] = setupBulkNotifyTest('owner', 2); $ids = $declarations->pluck('id')->all(); $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => $ids, ]); $activity = Activity::query() ->where('subject_type', Workspace::class) ->where('subject_id', $workspace->id) ->where('causer_id', $user->id) ->where('description', 'bulk_client_notification') ->first(); expect($activity)->not->toBeNull(); expect($activity->properties['count'])->toBe(2); expect($activity->properties['declaration_ids'])->toHaveCount(2); }); // ── Throttle middleware prevents abuse ── test('throttle middleware prevents abuse — 429 after 5 requests', function () { Mail::fake(); [$user, $workspace, $declarations] = setupBulkNotifyTest('owner', 1); $ids = $declarations->pluck('id')->all(); // Send 5 requests (within the throttle limit) for ($i = 0; $i < 5; $i++) { $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => $ids, ]); } // 6th request should be throttled $response = $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => $ids, ]); $response->assertStatus(429); }); // ── Unauthenticated user cannot bulk-notify ── test('unauthenticated user cannot bulk-notify — redirected to login', function () { $response = $this->post(route('declarations.bulk-notify'), [ 'declaration_ids' => [1], ]); $response->assertRedirect(route('login')); }); // ── Validation: empty declaration_ids rejected ── test('validation rejects empty declaration_ids', function () { [$user, $workspace] = setupBulkNotifyTest('owner', 0); $response = $this->actingAs($user)->post(route('declarations.bulk-notify'), [ 'declaration_ids' => [], ]); $response->assertSessionHasErrors('declaration_ids'); });