Step 6: Pest integration tests for webhook ingestion and background job
This commit is contained in:
@@ -107,10 +107,16 @@ class ProcessIncomingEmail implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (! empty($bodyHtml)) {
|
if (! empty($bodyHtml)) {
|
||||||
$stripped = strip_tags($bodyHtml);
|
// Replace all HTML tags with spaces to prevent words from running together
|
||||||
$stripped = preg_replace('/\s+/', ' ', $stripped);
|
$html = preg_replace('/<[^>]*>/', ' ', $bodyHtml);
|
||||||
|
|
||||||
return mb_substr(trim($stripped), 0, 500);
|
// Decode HTML entities (e.g. , &)
|
||||||
|
$decoded = html_entity_decode($html ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
|
||||||
|
// Collapse multiple spaces into a single space
|
||||||
|
$stripped = preg_replace('/\s+/', ' ', $decoded);
|
||||||
|
|
||||||
|
return mb_substr(trim($stripped ?? ''), 0, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@@ -126,12 +132,13 @@ class ProcessIncomingEmail implements ShouldQueue
|
|||||||
Cache::rememberForever('mongodb_ttl_index_ensured', function () {
|
Cache::rememberForever('mongodb_ttl_index_ensured', function () {
|
||||||
$ttlSeconds = config('services.mailops.email_body_ttl_seconds', 259200);
|
$ttlSeconds = config('services.mailops.email_body_ttl_seconds', 259200);
|
||||||
|
|
||||||
/** @var Collection $collection */
|
EmailBody::raw(function ($collection) use ($ttlSeconds) {
|
||||||
$collection = (new EmailBody)->getCollection();
|
/* @var \MongoDB\Collection $collection */
|
||||||
$collection->createIndex(
|
$collection->createIndex(
|
||||||
['created_at' => 1],
|
['created_at' => 1],
|
||||||
['expireAfterSeconds' => $ttlSeconds, 'name' => 'ttl_created_at']
|
['expireAfterSeconds' => $ttlSeconds, 'name' => 'ttl_created_at']
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
91
tests/Feature/ProcessIncomingEmailTest.php
Normal file
91
tests/Feature/ProcessIncomingEmailTest.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Events\NewEmailReceived;
|
||||||
|
use App\Jobs\ProcessIncomingEmail;
|
||||||
|
use App\Models\Email;
|
||||||
|
use App\Models\EmailBody;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
||||||
|
it('stores incoming email in MariaDB and MongoDB, and broadcasts event', function () {
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$hash = 'test-hash-'.time();
|
||||||
|
$payload = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'metadata' => [
|
||||||
|
'recipientEmail' => 'test@imail.app',
|
||||||
|
'recipientName' => 'Test User',
|
||||||
|
'senderEmail' => 'sender@example.com',
|
||||||
|
'senderName' => 'Sender Name',
|
||||||
|
'domain' => 'imail.app',
|
||||||
|
'subject' => 'Test Subject',
|
||||||
|
'received_at' => now()->toIso8601String(),
|
||||||
|
'attachmentSize' => 1024,
|
||||||
|
'attachments' => [
|
||||||
|
['filename' => 'test.pdf', 'mimeType' => 'application/pdf', 'size' => 1024],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'bodyText' => 'This is the plain text body format.',
|
||||||
|
'bodyHtml' => '<html><body><p>This is the HTML body format.</p></body></html>',
|
||||||
|
];
|
||||||
|
|
||||||
|
$job = new ProcessIncomingEmail($payload);
|
||||||
|
$job->handle();
|
||||||
|
|
||||||
|
// Verify MariaDB storage
|
||||||
|
$this->assertDatabaseHas('emails', [
|
||||||
|
'unique_id_hash' => $hash,
|
||||||
|
'recipient_email' => 'test@imail.app',
|
||||||
|
'domain' => 'imail.app',
|
||||||
|
'subject' => 'Test Subject',
|
||||||
|
'preview' => 'This is the plain text body format.',
|
||||||
|
'attachment_size' => 1024,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$email = Email::where('unique_id_hash', $hash)->first();
|
||||||
|
expect($email->attachments_json)->toHaveCount(1)
|
||||||
|
->and($email->attachments_json[0]['filename'])->toBe('test.pdf');
|
||||||
|
|
||||||
|
// Verify MongoDB storage
|
||||||
|
$body = EmailBody::where('unique_id_hash', $hash)->first();
|
||||||
|
expect($body)->not->toBeNull()
|
||||||
|
->and($body->body_text)->toBe('This is the plain text body format.')
|
||||||
|
->and($body->body_html)->toBe('<html><body><p>This is the HTML body format.</p></body></html>');
|
||||||
|
|
||||||
|
// Verify Broadcast Event
|
||||||
|
Event::assertDispatched(NewEmailReceived::class, function ($event) use ($hash) {
|
||||||
|
return $event->email->unique_id_hash === $hash;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup MongoDB (MariaDB is handled by RefreshDatabase if used, but let's be safe)
|
||||||
|
$body->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates preview from stripped HTML if text body is missing', function () {
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$hash = 'test-hash-html-only-'.time();
|
||||||
|
$payload = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'metadata' => [
|
||||||
|
'recipientEmail' => 'test2@imail.app',
|
||||||
|
'senderEmail' => 'sender2@example.com',
|
||||||
|
'domain' => 'imail.app',
|
||||||
|
'received_at' => now()->toIso8601String(),
|
||||||
|
],
|
||||||
|
'bodyText' => null,
|
||||||
|
'bodyHtml' => '<html><body><h1>Welcome</h1><p>This is a <strong>strong</strong> test.</p> <br> <p>Footer</p></body></html>',
|
||||||
|
];
|
||||||
|
|
||||||
|
$job = new ProcessIncomingEmail($payload);
|
||||||
|
$job->handle();
|
||||||
|
|
||||||
|
// Verify MariaDB storage preview logic
|
||||||
|
$this->assertDatabaseHas('emails', [
|
||||||
|
'unique_id_hash' => $hash,
|
||||||
|
'preview' => 'Welcome This is a strong test. Footer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cleanup MongoDB
|
||||||
|
EmailBody::where('unique_id_hash', $hash)->delete();
|
||||||
|
});
|
||||||
78
tests/Feature/WebhookIngestionTest.php
Normal file
78
tests/Feature/WebhookIngestionTest.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Jobs\ProcessIncomingEmail;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Config::set('services.mailops.webhook_secret', 'test-secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects webhooks without a secret token', function () {
|
||||||
|
$response = $this->postJson('/api/webhooks/incoming_email', [
|
||||||
|
'hash' => 'dummy-hash',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(401)
|
||||||
|
->assertJson(['error' => 'Unauthorized']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects webhooks with an invalid secret token', function () {
|
||||||
|
$response = $this->postJson('/api/webhooks/incoming_email', [
|
||||||
|
'hash' => 'dummy-hash',
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer wrong-secret',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts valid webhooks and dispatches the processing job', function () {
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'hash' => 'test-unique-hash-12345',
|
||||||
|
'metadata' => [
|
||||||
|
'recipientEmail' => 'test@imail.app',
|
||||||
|
'recipientName' => 'Test User',
|
||||||
|
'senderEmail' => 'sender@example.com',
|
||||||
|
'senderName' => 'Sender Name',
|
||||||
|
'domain' => 'imail.app',
|
||||||
|
'subject' => 'Test Subject',
|
||||||
|
'received_at' => now()->toIso8601String(),
|
||||||
|
'attachmentSize' => 0,
|
||||||
|
'attachments' => [],
|
||||||
|
],
|
||||||
|
'bodyText' => 'This is a test email body.',
|
||||||
|
'bodyHtml' => '<p>This is a test email body.</p>',
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/webhooks/incoming_email', $payload, [
|
||||||
|
'Authorization' => 'Bearer test-secret',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(200)
|
||||||
|
->assertJson(['status' => 'queued']);
|
||||||
|
|
||||||
|
Queue::assertPushed(ProcessIncomingEmail::class, function ($job) use ($payload) {
|
||||||
|
return $job->payload['hash'] === $payload['hash'];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates required payload fields', function () {
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/webhooks/incoming_email', [
|
||||||
|
// Missing hash and other required fields
|
||||||
|
'metadata' => [
|
||||||
|
'recipientEmail' => 'test@imail.app',
|
||||||
|
],
|
||||||
|
], [
|
||||||
|
'Authorization' => 'Bearer test-secret',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422)
|
||||||
|
->assertJsonValidationErrors(['hash', 'metadata.senderEmail', 'metadata.domain', 'metadata.received_at']);
|
||||||
|
|
||||||
|
Queue::assertNothingPushed();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user