Publication initiale de RelaisSMS : documentation et sources nettoyees

This commit is contained in:
2026-10-21 09:29:39 +02:00
commit ac186b03b1
158 changed files with 24818 additions and 0 deletions
+358
View File
@@ -0,0 +1,358 @@
<?php
namespace Tests\Feature;
use App\Contracts\Sms\SmsProviderInterface;
use App\Models\AppSetting;
use App\Models\Contact;
use App\Models\SmsCampaign;
use App\Models\User;
use App\Services\Sms\OvhSmsProvider;
use App\Services\Sms\SmsCampaignSender;
use App\Services\Sms\SmsSendResult;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class SmsCampaignSendTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Carbon::setTestNow();
parent::tearDown();
}
public function test_other_provider_receives_links_unchanged(): void
{
config(['relais_sms.default_settings.active_sms_provider' => 'other']);
$message = 'Reunion https://zoom.us/j/123';
$provider = $this->mock(SmsProviderInterface::class);
$provider->shouldReceive('send')->once()->withArgs(fn (SmsCampaign $campaign, array $recipients): bool => $campaign->message === $message)
->andReturn(new SmsSendResult(serviceName: 'other', providerJobId: '1', requestPayload: ['message' => $message], responsePayload: []));
$campaign = $this->app->make(SmsCampaignSender::class)->send(User::factory()->create(), $message, []);
$this->assertSame($message, $campaign->message);
}
public function test_history_keeps_the_message_actually_transmitted_by_ovh(): void
{
config([
'relais_sms.ovh.sms_short_code' => true,
'relais_sms.ovh.sms_sender' => '',
'relais_sms.ovh.sms_service_name' => 'sms-test',
'relais_sms.ovh.application_key' => 'test',
'relais_sms.ovh.application_secret' => 'test',
'relais_sms.ovh.consumer_key' => 'test',
]);
$client = new Client(['handler' => HandlerStack::create(new MockHandler([
new Response(200, [], '123456'),
new Response(200, [], '{"ids":[]}'),
]))]);
$this->app->instance(SmsProviderInterface::class, new OvhSmsProvider($client));
$campaign = $this->app->make(SmsCampaignSender::class)->send(User::factory()->create(), 'Reunion https://zoom.us/j/123 demain', []);
$this->assertSame('Reunion demain', $campaign->message);
$this->assertSame('Reunion demain', $campaign->request_payload['message']);
}
public function test_authenticated_user_can_send_sms_campaign_and_history_is_recorded(): void
{
$this->app->bind(SmsProviderInterface::class, fn (): SmsProviderInterface => new class implements SmsProviderInterface
{
public function send(SmsCampaign $campaign, array $recipients): SmsSendResult
{
return new SmsSendResult(
serviceName: 'sms-test',
providerJobId: 'job-123',
requestPayload: ['receivers' => array_column($recipients, 'number')],
responsePayload: ['ids' => ['message-123']],
providerMessageIds: ['message-123'],
);
}
});
$user = User::factory()->create();
$contact = $this->createSmsContact();
$response = $this->actingAs($user)
->post(route('sms-campaigns.store'), [
'contact_ids' => [$contact->id],
'message' => 'Message de test',
]);
$response->assertRedirect();
$this->assertDatabaseHas('sms_campaigns', [
'message' => 'Message de test',
'status' => 'sent',
'recipients_count' => 1,
'sent_count' => 1,
'provider_service_name' => 'sms-test',
'provider_job_id' => 'job-123',
]);
$this->assertDatabaseHas('sms_deliveries', [
'contact_id' => $contact->id,
'number' => '+33611111111',
'status' => 'sent',
'provider_message_id' => 'message-123',
]);
$this->actingAs($user)
->get(route('sms-campaigns.index'))
->assertOk()
->assertSee('Message de test');
}
public function test_sms_campaign_message_uses_configured_header_and_footer_when_selected(): void
{
AppSetting::putString('sms_message_header', 'AFPMA');
AppSetting::putString('sms_message_footer', 'Envoyer STOP SMS pour ne plus etre contacte.');
$this->app->bind(SmsProviderInterface::class, fn (): SmsProviderInterface => new class implements SmsProviderInterface
{
public function send(SmsCampaign $campaign, array $recipients): SmsSendResult
{
return new SmsSendResult(
serviceName: 'sms-test',
providerJobId: 'job-headers',
requestPayload: ['message' => $campaign->message],
responsePayload: ['ids' => ['message-headers']],
providerMessageIds: ['message-headers'],
);
}
});
$user = User::factory()->create();
$contact = $this->createSmsContact();
$this->actingAs($user)
->post(route('sms-campaigns.store'), [
'contact_ids' => [$contact->id],
'message' => 'Message de test',
'include_sms_header' => '1',
'include_sms_footer' => '1',
])
->assertRedirect();
$this->assertDatabaseHas('sms_campaigns', [
'message' => "AFPMA\nMessage de test\nEnvoyer STOP SMS pour ne plus etre contacte.",
'provider_job_id' => 'job-headers',
]);
}
public function test_sms_campaign_message_can_remove_configured_footer_in_composition(): void
{
AppSetting::putString('sms_message_header', 'AFPMA');
AppSetting::putString('sms_message_footer', 'Envoyer STOP SMS pour ne plus etre contacte.');
$this->app->bind(SmsProviderInterface::class, fn (): SmsProviderInterface => new class implements SmsProviderInterface
{
public function send(SmsCampaign $campaign, array $recipients): SmsSendResult
{
return new SmsSendResult(
serviceName: 'sms-test',
providerJobId: 'job-no-footer',
requestPayload: ['message' => $campaign->message],
responsePayload: ['ids' => ['message-no-footer']],
providerMessageIds: ['message-no-footer'],
);
}
});
$user = User::factory()->create();
$contact = $this->createSmsContact();
$this->actingAs($user)
->post(route('sms-campaigns.store'), [
'contact_ids' => [$contact->id],
'message' => 'Message de test',
'include_sms_header' => '1',
'include_sms_footer' => '0',
])
->assertRedirect();
$this->assertDatabaseHas('sms_campaigns', [
'message' => "AFPMA\nMessage de test",
'provider_job_id' => 'job-no-footer',
]);
}
public function test_authenticated_user_can_schedule_sms_campaign_without_sending_immediately(): void
{
Carbon::setTestNow(Carbon::parse('2026-06-12 10:00:00', 'UTC'));
$provider = new class implements SmsProviderInterface
{
public int $calls = 0;
public function send(SmsCampaign $campaign, array $recipients): SmsSendResult
{
$this->calls++;
return new SmsSendResult(
serviceName: 'sms-test',
providerJobId: 'job-123',
requestPayload: ['receivers' => array_column($recipients, 'number')],
responsePayload: ['ids' => ['message-123']],
providerMessageIds: ['message-123'],
);
}
};
$this->app->instance(SmsProviderInterface::class, $provider);
$user = User::factory()->create();
$contact = $this->createSmsContact();
$response = $this->actingAs($user)
->post(route('sms-campaigns.store'), [
'contact_ids' => [$contact->id],
'message' => 'Message programme',
'send_mode' => 'scheduled',
'scheduled_at' => '2026-06-12T12:05',
]);
$response->assertRedirect();
$this->assertSame(0, $provider->calls);
$this->assertDatabaseHas('sms_campaigns', [
'message' => 'Message programme',
'status' => 'scheduled',
'recipients_count' => 1,
'sent_count' => 0,
]);
$this->assertDatabaseHas('sms_deliveries', [
'contact_id' => $contact->id,
'number' => '+33611111111',
'status' => 'queued',
]);
}
public function test_scheduled_campaign_can_be_canceled_before_processing(): void
{
Carbon::setTestNow(Carbon::parse('2026-06-12 10:00:00', 'UTC'));
$provider = new class implements SmsProviderInterface
{
public int $calls = 0;
public function send(SmsCampaign $campaign, array $recipients): SmsSendResult
{
$this->calls++;
return new SmsSendResult('sms-test', 'job-123', [], []);
}
};
$this->app->instance(SmsProviderInterface::class, $provider);
$user = User::factory()->create();
$contact = $this->createSmsContact();
$this->actingAs($user)
->post(route('sms-campaigns.store'), [
'contact_ids' => [$contact->id],
'message' => 'Message annulable',
'send_mode' => 'scheduled',
'scheduled_at' => '2026-06-12T12:05',
]);
$campaign = SmsCampaign::query()->firstOrFail();
$this->actingAs($user)
->patch(route('sms-campaigns.cancel', $campaign))
->assertRedirect(route('sms-campaigns.show', $campaign));
$this->assertSame(0, $provider->calls);
$this->assertDatabaseHas('sms_campaigns', [
'id' => $campaign->id,
'status' => 'canceled',
]);
$this->assertDatabaseHas('sms_deliveries', [
'sms_campaign_id' => $campaign->id,
'status' => 'canceled',
]);
}
public function test_due_scheduled_campaign_is_sent_by_scheduler_command(): void
{
Carbon::setTestNow(Carbon::parse('2026-06-12 10:00:00', 'UTC'));
Log::spy();
$provider = new class implements SmsProviderInterface
{
public int $calls = 0;
public function send(SmsCampaign $campaign, array $recipients): SmsSendResult
{
$this->calls++;
return new SmsSendResult(
serviceName: 'sms-test',
providerJobId: 'job-456',
requestPayload: ['receivers' => array_column($recipients, 'number')],
responsePayload: ['ids' => ['message-456']],
providerMessageIds: ['message-456'],
);
}
};
$this->app->instance(SmsProviderInterface::class, $provider);
$user = User::factory()->create();
$contact = $this->createSmsContact();
$this->actingAs($user)
->post(route('sms-campaigns.store'), [
'contact_ids' => [$contact->id],
'message' => 'Message automatique',
'send_mode' => 'scheduled',
'scheduled_at' => '2026-06-12T12:05',
]);
Carbon::setTestNow(Carbon::parse('2026-06-12 10:05:00', 'UTC'));
$this->artisan('sms-campaigns:send-due')
->expectsOutput('1 campagne(s) SMS programmee(s) prise(s) en charge.')
->assertExitCode(0);
$this->assertSame(1, $provider->calls);
$this->assertDatabaseHas('sms_campaigns', [
'message' => 'Message automatique',
'status' => 'sent',
'sent_count' => 1,
'provider_job_id' => 'job-456',
]);
$this->assertDatabaseHas('sms_deliveries', [
'number' => '+33611111111',
'status' => 'sent',
'provider_message_id' => 'message-456',
]);
Log::shouldHaveReceived('info')
->once()
->with('SMS campaign status changed from scheduled to sending.', \Mockery::type('array'));
}
private function createSmsContact(): Contact
{
$contact = Contact::query()->create([
'source' => 'local',
'first_name' => 'Marie',
'last_name' => 'Durand',
]);
$contact->phoneNumbers()->create([
'raw_number' => '06 11 11 11 11',
'normalized_number' => '+33611111111',
'country' => 'FR',
'is_valid' => true,
'is_mobile' => true,
'is_primary' => true,
]);
return $contact;
}
}