Files
fossbilling-mailgateway/tests/validation.php
T

193 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
require dirname(__DIR__, 3) . '/vendor/autoload.php';
define('PATH_CONFIG', __DIR__ . '/config.php');
define('DEBUG', false);
require dirname(__DIR__) . '/RuntimeConfig.php';
require dirname(__DIR__) . '/DependencyInstaller.php';
require dirname(__DIR__) . '/Service.php';
require dirname(__DIR__) . '/InboundPolicy.php';
require dirname(__DIR__) . '/NotificationCooldown.php';
require dirname(__DIR__) . '/NotificationEmailProxy.php';
require dirname(__DIR__) . '/Settings.php';
require dirname(__DIR__) . '/Api/Admin.php';
$service = new \Box\Mod\Mailgateway\Service();
$valid = ['sender_email' => 'customer@example.com', 'subject' => 'Test', 'content' => 'Test body'];
$cases = [
[],
array_replace($valid, ['sender_email' => ['customer@example.com']]),
array_replace($valid, ['sender_email' => 'Name <customer@example.com>']),
array_replace($valid, ['subject' => "Header\r\nInjected"]),
array_replace($valid, ['subject' => str_repeat('x', 256)]),
array_replace($valid, ['subject' => "\xFF"]),
array_replace($valid, ['content' => str_repeat('x', 65536)]),
array_replace($valid, ['content' => "\xFF"]),
array_replace($valid, ['content' => "\0"]),
array_replace($valid, ['sender_name' => ['Invalid']]),
array_replace($valid, ['sender_name' => "Name\nInjected"]),
array_replace($valid, ['sender_name' => str_repeat('x', 256)]),
];
foreach ($cases as $index => $data) {
try {
$service->import($data);
throw new \RuntimeException('Invalid input accepted: ' . $index);
} catch (\FOSSBilling\InformationException) {
// Must reject before accessing any database services.
}
}
// The production API exposes only the settings page operations.
$methods = array_filter((new ReflectionClass(\Box\Mod\Mailgateway\Api\Admin::class))->getMethods(ReflectionMethod::IS_PUBLIC),
fn ($method) => $method->getDeclaringClass()->getName() === \Box\Mod\Mailgateway\Api\Admin::class);
$names = array_map(fn ($method) => $method->getName(), $methods);
sort($names);
if ($names !== ['install_dependencies', 'mailbox_delete', 'notifications_setup', 'settings_get', 'settings_save', 'setup', 'test_connection']) {
throw new RuntimeException('Unexpected production API endpoints');
}
echo 'Passed ', count($cases), " invalid-input checks and production API surface check.\n";
// Exercise the import orchestration with real ticket entities and a simulated
// Support autoresponse, without requiring a database or sending email.
$client = new class extends \Model_Client {
public function __get($property)
{
return $property === 'id' ? 7 : null;
}
};
foreach (['on_hold' => 1, 'open' => 0, 'closed' => 0] as $status => $expectedFlushes) {
$ticket = new \Box\Mod\Support\Entity\SupportTicket();
$ticket->setStatus($status);
$helpdesk = new \Box\Mod\Support\Entity\Helpdesk();
$support = new class($ticket, $helpdesk, $client) {
public function __construct(public $ticket, public $helpdesk, public $client) {}
public function getHelpdeskRepository() { return $this; }
public function findBy($criteria, $order, $limit) { return [$this->helpdesk]; }
public function ticketCreateForClient($client, $helpdesk, $data): int
{
if ($client !== $this->client || $helpdesk !== $this->helpdesk || $data['content'] !== 'Test body') {
throw new \RuntimeException('Incorrect customer or message passed to Support');
}
return 42;
}
public function getTicketById(int $id) { return $this->ticket; }
public function toApiArray($ticket, $deep, $identity)
{
if ($identity !== $this->client) {
throw new \RuntimeException('Notification uses incorrect customer');
}
return ['id' => 42];
}
};
$em = new class {
public int $flushes = 0;
public function flush(): void { ++$this->flushes; }
};
$di = new \Pimple\Container();
$di['dbal'] = new class {
public function fetchFirstColumn($sql, $params) { return [7]; }
};
$di['db'] = new class($client) {
public function __construct(public $client) {}
public function getExistingModelById($type, $id) { return $this->client; }
};
$di['em'] = $em;
$di['mailgateway_settings'] = new class {
public function get(): array { return ['support_helpdesk_id' => null]; }
};
$di['mailgateway_notification_cooldown'] = new class {
public function run($sender, $queue): bool { return $queue() !== false; }
};
$di['auth'] = new class {
public function isClientLoggedIn(): bool { return false; }
};
$email = new class {
public array $messages = [];
public function sendTemplate($data) { $this->messages[] = $data; }
};
$di['mod_service'] = $di->protect(fn ($module) => $module === 'email' ? $email : $support);
$service->setDi($di);
$result = $service->import($valid);
$expectedStatus = $status === 'on_hold' ? 'open' : $status;
if ($ticket->getStatus() !== $expectedStatus || $em->flushes !== $expectedFlushes
|| $result['ticket_id'] !== 42 || $result['client_id'] !== 7) {
throw new \RuntimeException('Import status regression: ' . $status);
}
if ($email->messages !== [['to_client' => 7, 'code' => 'mod_support_ticket_open', 'ticket' => ['id' => 42]]]) {
throw new \RuntimeException('Customer acknowledgement missing or incorrect');
}
}
echo "Passed autoresponse status check, existing Open/Closed status checks, and customer identity forwarding.\n";
echo "Passed customer acknowledgement recipient, template, and ticket-number checks.\n";
$guestSupport = new class {
public array $received = [];
public bool $enabled = true;
public function getHelpdeskRepository() { return $this; }
public function findBy($criteria, $order, $limit)
{
$helpdesk = new \Box\Mod\Support\Entity\Helpdesk();
(new \ReflectionProperty($helpdesk, 'id'))->setValue($helpdesk, 3);
return [$helpdesk];
}
public function ticketCreateForGuest($data): string
{
if (!$this->enabled) {
throw new \FOSSBilling\InformationException('Guest tickets disabled');
}
$this->received[] = $data;
return 'private-access-hash';
}
public function findOneByHash($hash)
{
if ($hash !== 'private-access-hash') {
throw new \RuntimeException('Guest hash lookup incorrect');
}
$ticket = new \Box\Mod\Support\Entity\SupportTicket();
(new \ReflectionProperty($ticket, 'id'))->setValue($ticket, 43);
return $ticket;
}
};
$guestDi = new \Pimple\Container();
$lookup = new class {
public array $ids = [];
public function fetchFirstColumn($sql, $params) { return $this->ids; }
};
$guestDi['dbal'] = $lookup;
$guestDi['mailgateway_settings'] = new class {
public function get(): array { return ['support_helpdesk_id' => null]; }
};
$guestDi['mod_service'] = $guestDi->protect(fn ($module) => $guestSupport);
$service->setDi($guestDi);
foreach ([null, 'Guest Sender'] as $name) {
$data = $valid;
if ($name !== null) {
$data['sender_name'] = $name;
}
$result = $service->import($data);
$received = $guestSupport->received[array_key_last($guestSupport->received)];
if ($result !== ['ticket_id' => 43, 'client_id' => null, 'support_helpdesk_id' => 3]
|| $received !== ['name' => $name ?? $valid['sender_email'], 'email' => $valid['sender_email'],
'subject' => 'Test', 'content' => 'Test body', 'support_helpdesk_id' => 3]) {
throw new \RuntimeException('Guest import or attribution incorrect');
}
}
$guestSupport->enabled = false;
try {
$service->import($valid);
throw new \RuntimeException('Disabled guest policy bypassed');
} catch (\FOSSBilling\InformationException) {
}
$lookup->ids = [7, 8];
try {
$service->import($valid);
throw new \RuntimeException('Ambiguous customer accepted');
} catch (\FOSSBilling\InformationException) {
}
if (count($guestSupport->received) !== 2) {
throw new \RuntimeException('Unexpected guest creation');
}
echo "Passed guest routing, sender name fallback, helpdesk mapping, private hash handling, disabled guest policy, and ambiguous customer rejection.\n";