validate([ 'name' => ' Site Photo.JPG ', 'mime_type' => 'IMAGE/JPEG', 'size' => '2048', 'client_visible' => 'yes', 'client_approved' => 'true', ]); attachment_notification_assert_same(true, $validAttachment['valid'], 'A safe approved image attachment should validate.'); attachment_notification_assert_same('Site Photo.JPG', $validAttachment['name'], 'Attachment names should be trimmed without changing case.'); attachment_notification_assert_same('jpg', $validAttachment['extension'], 'Attachment extensions should normalize to lowercase.'); attachment_notification_assert_same('image/jpeg', $validAttachment['mime_type'], 'Attachment MIME types should normalize to lowercase.'); attachment_notification_assert_same(2048, $validAttachment['size_bytes'], 'Attachment sizes should normalize to bytes.'); attachment_notification_assert_same(true, $validAttachment['client_visible'], 'Client visibility should normalize to boolean.'); foreach ([ ['name' => '../secret.pdf', 'mime_type' => 'application/pdf', 'size_bytes' => 10], ['name' => 'invoice.php.jpg', 'mime_type' => 'image/jpeg', 'size_bytes' => 10], ['name' => 'photo.jpg', 'mime_type' => 'application/x-php', 'size_bytes' => 10], ['name' => 'photo.jpg', 'mime_type' => 'image/jpeg', 'size_bytes' => 5_000_001], ['name' => 'photo.jpg', 'mime_type' => 'image/jpeg', 'size_bytes' => 10, 'client_visible' => true, 'client_approved' => false], ] as $invalidPayload) { attachment_notification_assert_same(false, $attachments->validate($invalidPayload)['valid'], 'Unsafe attachment metadata should be rejected.'); } $notifications = new NotificationRecord(); $notification = $notifications->validate([ 'type' => ' JOBCARD_STATUS_CHANGED ', 'recipients' => [' Support@Example.com ', 'support@example.com', 'client@example.com'], 'is_read' => '0', 'deduplication_key' => ' Jobcard:42:Status:closed ', ]); attachment_notification_assert_same(true, $notification['valid'], 'A valid notification should validate.'); attachment_notification_assert_same('jobcard_status_changed', $notification['type'], 'Notification types should normalize to lowercase snake case.'); attachment_notification_assert_same(['support@example.com', 'client@example.com'], $notification['recipients'], 'Recipients should normalize, lowercase and de-duplicate.'); attachment_notification_assert_same(false, $notification['is_read'], 'Unread notification state should normalize to false.'); attachment_notification_assert_same('jobcard:42:status:closed', $notification['deduplication_key'], 'Deduplication keys should normalize case and whitespace.'); $invalidNotification = $notifications->validate([ 'type' => 'unknown-event', 'recipients' => ['not-an-email'], 'is_read' => 'maybe', 'deduplication_key' => '', ]); attachment_notification_assert_same(false, $invalidNotification['valid'], 'Invalid notification metadata should be rejected.'); foreach (['type', 'recipients', 'is_read', 'deduplication_key'] as $field) { if (!isset($invalidNotification['errors'][$field])) { throw new RuntimeException("Expected validation error for {$field}."); } } printf("Attachment and notification tests: 7 passed\n");