First unit tests, misc fixes

This commit is contained in:
Chris 2024-03-12 01:45:21 +01:00
parent b3476881e1
commit 8be2e81054
8 changed files with 173 additions and 22 deletions

View File

@ -53,7 +53,7 @@ if (isset($opts['C'])) {
key: foo.key key: foo.key
publish: publish:
overwrite_id: false overwrite_ids: false
reject_duplicates: true reject_duplicates: true
subscribe: subscribe:

View File

@ -21,7 +21,7 @@ server:
publish: publish:
# Assign a UUID to published messages even if one is already set in the message # Assign a UUID to published messages even if one is already set in the message
overwrite_id: false overwrite_ids: false
# Reject messages with previously seen IDs # Reject messages with previously seen IDs
reject_duplicates: true reject_duplicates: true

View File

@ -23,11 +23,11 @@ class Message
public function __construct( public function __construct(
array $topic, array $topic,
?string $type, ?string $data=null,
?string $data, ?string $type=null,
?bool $private, ?bool $private=null,
?string $id, ?string $id=null,
?int $retry ?int $retry=null
) )
{ {
$this->topic = $topic; $this->topic = $topic;
@ -60,7 +60,7 @@ class Message
topic: (array)$data['topic'], topic: (array)$data['topic'],
type: $data['type']??null, type: $data['type']??null,
data: $data['data']??null, data: $data['data']??null,
private: match ($data['private']??null) { "on" => true, null => null, default => false }, private: match ($data['private']??null) { "on" => true, true => true, null => null, default => false },
id: $data['id']??null, id: $data['id']??null,
retry: $data['retry']??null, retry: $data['retry']??null,
); );

View File

@ -7,4 +7,6 @@ interface SubscriberInterface
public function deliver(Message $message): void; public function deliver(Message $message): void;
public function isAuthorized(): bool; public function isAuthorized(): bool;
public function getPayload(): ?array;
} }

View File

@ -108,5 +108,19 @@ class Configuration
return $this; return $this;
} }
public function getOverwriteMessageIds(): bool
{
return $this->config['publish.overwrite_ids']??true;
}
public function getRejectDuplicateMessages(): bool
{
return $this->config['publish.reject_duplicates']??true;
}
public function getDuplicateIdHistorySize(): int
{
return 50;
}
} }

View File

@ -2,6 +2,7 @@
namespace NoccyLabs\Mercureact\Http\Middleware; namespace NoccyLabs\Mercureact\Http\Middleware;
use LDAP\Result;
use NoccyLabs\Mercureact\Broker\Message; use NoccyLabs\Mercureact\Broker\Message;
use NoccyLabs\Mercureact\Broker\SseSubscriber; use NoccyLabs\Mercureact\Broker\SseSubscriber;
use NoccyLabs\Mercureact\Broker\TopicManager; use NoccyLabs\Mercureact\Broker\TopicManager;
@ -24,6 +25,10 @@ class MercureHandler
{ {
private LoopInterface $loop; private LoopInterface $loop;
private array $seenMessageIds = [];
private int $seenIdHistorySize = 100;
public function __construct( public function __construct(
private Configuration $config, private Configuration $config,
private TopicManager $topicManager, private TopicManager $topicManager,
@ -31,6 +36,7 @@ class MercureHandler
) )
{ {
$this->loop = $loop ?? Loop::get(); $this->loop = $loop ?? Loop::get();
$this->seenIdHistorySize = $this->config->getDuplicateIdHistorySize();
} }
/** /**
@ -166,9 +172,16 @@ class MercureHandler
); );
} }
if ($this->config->getRejectDuplicateMessages() && !empty($data['id'])) {
if (in_array($data['id'], $this->seenMessageIds)) {
return Response::plaintext("Duplicate message id")->withStatus(Response::STATUS_BAD_REQUEST);
}
array_push($this->seenMessageIds, $data['id']);
$this->seenMessageIds = array_slice($this->seenMessageIds, -100, 100);
}
// Put an id in there if none already // Put an id in there if none already
// TODO add a configurable for this if (empty($data['id']) || $this->config->getOverwriteMessageIds()) {
if (!isset($data['id'])) {
$data['id'] = (string)Uuid::v7(); $data['id'] = (string)Uuid::v7();
} }
@ -176,7 +189,7 @@ class MercureHandler
$message = Message::fromData($data); $message = Message::fromData($data);
$this->loop->futureTick(function () use ($message) { $this->loop->futureTick(function () use ($message) {
$this->publishMercureMessage($message); $this->topicManager->publish($message);
}); });
return Response::plaintext("urn:uuid:".$message->id."\n"); return Response::plaintext("urn:uuid:".$message->id."\n");
@ -201,15 +214,4 @@ class MercureHandler
return ($matched == count($topic)); return ($matched == count($topic));
} }
/**
*
*
* @param Message $message
* @return void
*/
private function publishMercureMessage(Message $message): void
{
$this->topicManager->publish($message);
}
} }

View File

@ -0,0 +1,70 @@
<?php
namespace NoccyLabs\Mercureact\Broker;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(Message::class)]
class MessageTest extends \PHPUnit\Framework\TestCase
{
public function testCreatingMessageFromData()
{
$message = Message::fromData([
'id' => '1a',
'topic' => '2b',
'data' => '3c',
'type' => '4d',
'retry' => 42,
'private' => true,
]);
$this->assertEquals("1a", $message->id);
$this->assertEquals(["2b"], $message->topic);
$this->assertEquals("3c", $message->data);
$this->assertEquals("4d", $message->type);
$this->assertEquals(42, $message->retry);
$this->assertEquals(true, $message->private);
$message = Message::fromData([
'id' => '1a',
'topic' => ['2b'],
'data' => '3c',
'type' => '4d',
'retry' => 42,
'private' => true,
]);
$this->assertEquals("1a", $message->id);
$this->assertEquals(["2b"], $message->topic);
$this->assertEquals("3c", $message->data);
$this->assertEquals("4d", $message->type);
$this->assertEquals(42, $message->retry);
$this->assertEquals(true, $message->private);
}
public function testCreatingSseFromMessage()
{
$message = Message::fromData([
'id' => '1a',
'topic' => '2b',
'data' => '3c',
'type' => '4d',
'retry' => 42,
'private' => true,
]);
$sse = $message->toString();
$this->assertEquals(<<<EXPECT
event: 4d
retry: 42
id: 1a
data: 3c
EXPECT, $sse);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace NoccyLabs\Mercureact\Broker;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(Topic::class)]
class TopicTest extends \PHPUnit\Framework\TestCase
{
public function testPublicMessagesAreDeliveredToAllSubscribers()
{
$authorizedSubscriber = new class implements SubscriberInterface {
public array $messages = [];
public function isAuthorized():bool { return true; }
public function deliver(Message $message):void { $this->messages[] = $message; }
public function getPayload(): ?array { return null; }
};
$unauthorizedSubscriber = new class implements SubscriberInterface {
public array $messages = [];
public function isAuthorized():bool { return false; }
public function deliver(Message $message):void { $this->messages[] = $message; }
public function getPayload(): ?array { return null; }
};
$topic = new Topic("foo");
$topic->addSubscriber($authorizedSubscriber);
$topic->addSubscriber($unauthorizedSubscriber);
$message = new Message(topic:["foo"], data:"test", private:false);
$topic->publish($message);
$this->assertEquals(1, count($authorizedSubscriber->messages));
$this->assertEquals(1, count($unauthorizedSubscriber->messages));
}
public function testPrivateMessagesAreNotDeliveredToUnauthorizedSubscribers()
{
$authorizedSubscriber = new class implements SubscriberInterface {
public array $messages = [];
public function isAuthorized():bool { return true; }
public function deliver(Message $message):void { $this->messages[] = $message; }
public function getPayload(): ?array { return null; }
};
$unauthorizedSubscriber = new class implements SubscriberInterface {
public array $messages = [];
public function isAuthorized():bool { return false; }
public function deliver(Message $message):void { $this->messages[] = $message; }
public function getPayload(): ?array { return null; }
};
$topic = new Topic("foo");
$topic->addSubscriber($authorizedSubscriber);
$topic->addSubscriber($unauthorizedSubscriber);
$message = new Message(topic:["foo"], data:"test", private:true);
$topic->publish($message);
$this->assertEquals(1, count($authorizedSubscriber->messages));
$this->assertEquals(0, count($unauthorizedSubscriber->messages));
}
}