<?php
declare(strict_types=1);
namespace GNexus\Synapse\Tests\EnvelopeBuilder;
use GNexus\Synapse\Envelope\EnvelopeBuilder;
use GNexus\Synapse\Exception\ValidationException;
use GNexus\Synapse\Tests\FakeLogger;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class EnvelopeBuilderTest extends TestCase
{
public function testBuildsMinimalEnvelope(): void
{
self::assertSame(
['source' => 's', 'subject' => 'x', 'action' => 'y'],
EnvelopeBuilder::build('s', 'x', 'y'),
);
// сервер extra="forbid": None/пустых полей в конверте нет
$env = EnvelopeBuilder::build('s', 'x', 'y', payload: null, dedupKey: '', ttlSeconds: null);
self::assertSame(['source' => 's', 'subject' => 'x', 'action' => 'y'], $env);
}
public function testBuildsFullEnvelope(): void
{
$ts = new \DateTimeImmutable('2026-10-03 12:00:00', new \DateTimeZone('UTC'));
$env = EnvelopeBuilder::build('s', 'x', 'y', 'high', ['user_id' => '42'], 'd1', 60, $ts);
self::assertSame([
'source' => 's', 'subject' => 'x', 'action' => 'y', 'priority' => 'high',
'payload' => ['user_id' => '42'], 'dedup_key' => 'd1', 'ttl_seconds' => 60,
'scheduled_at' => '2026-10-03T12:00:00+00:00',
], $env);
// дефолтный priority не шлётся
self::assertSame(
['source' => 's', 'subject' => 'x', 'action' => 'y'],
EnvelopeBuilder::build('s', 'x', 'y', 'normal'),
);
}
public static function badNamesProvider(): iterable
{
yield 'uppercase' => ['Bugtrail'];
yield 'space' => ['a b'];
yield 'leading dash' => ['-lead'];
yield 'trailing dash' => ['trail-'];
yield 'empty' => [''];
yield 'too long' => [str_repeat('a', 65)];
yield 'cyrillic' => ['спецсимволы'];
}
#[DataProvider('badNamesProvider')]
public function testRejectsBadNames(string $bad): void
{
foreach ([[$bad, 'x', 'y'], ['s', $bad, 'y'], ['s', 'x', $bad]] as $args) {
try {
EnvelopeBuilder::build(...$args);
self::fail("ожидалась ValidationException для '$args[0]', '$args[1]', '$args[2]'");
} catch (ValidationException $ex) {
self::assertIsString($ex->detail);
}
}
}
public function testRejectsPriorityTtlDedup(): void
{
try {
EnvelopeBuilder::build('s', 'x', 'y', 'urgent');
self::fail('ожидалась ValidationException');
} catch (ValidationException $ex) {
self::assertStringContainsString('priority', $ex->detail);
}
foreach ([0, -5, 604801] as $ttl) {
try {
EnvelopeBuilder::build('s', 'x', 'y', ttlSeconds: $ttl);
self::fail("ожидалась ValidationException для ttlSeconds=$ttl");
} catch (ValidationException $ex) {
self::assertStringContainsString('ttlSeconds', $ex->detail);
}
}
self::assertSame(604800, EnvelopeBuilder::build('s', 'x', 'y', ttlSeconds: 604800)['ttl_seconds']);
try {
EnvelopeBuilder::build('s', 'x', 'y', dedupKey: str_repeat('d', 256));
self::fail('ожидалась ValidationException');
} catch (ValidationException $ex) {
self::assertStringContainsString('dedup_key', $ex->detail);
}
}
public function testRejectsListPayload(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('словар');
EnvelopeBuilder::build('s', 'x', 'y', payload: ['не', 'словарь']);
}
public function testWarnsOnBadUserId(): void
{
$logs = new FakeLogger();
EnvelopeBuilder::warnBadUserId(['user_id' => '42'], $logs);
EnvelopeBuilder::warnBadUserId(['user_id' => 42], $logs);
self::assertCount(0, $logs->warnings());
EnvelopeBuilder::warnBadUserId(['user_id' => ''], $logs);
EnvelopeBuilder::warnBadUserId(['user_id' => null], $logs);
EnvelopeBuilder::warnBadUserId([], $logs);
self::assertCount(3, $logs->warnings());
}
}