Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
NotificationOutboxContractTests
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
3 / 3
3
100.00% covered (success)
100.00%
1 / 1
 createOutbox
n/a
0 / 0
n/a
0 / 0
0
 pending
n/a
0 / 0
n/a
0 / 0
0
 testEnqueuedNotificationsAreKeptInOrderWithTheirIdentity
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
1
 testQueuedBodiesAreSnapshots
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 testPendingReadsAreSnapshotsToo
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Testing\Contract;
6
7use DateTimeImmutable;
8use LambdaTwelve\OneRecord\Api\Notification;
9use LambdaTwelve\OneRecord\Api\NotificationEventType;
10use LambdaTwelve\OneRecord\Rdf\Iri;
11use LambdaTwelve\OneRecord\Server\Spi\NotificationOutbox;
12use LambdaTwelve\OneRecord\Server\Spi\OutboundNotification;
13use LambdaTwelve\OneRecord\Vocabulary\Generated\Cargo;
14
15/**
16 * The tests behind NotificationOutboxContract, as a trait, for hosts whose test cases must
17 * extend a framework base class (Laravel's Testbench, Drupal's KernelTestBase)
18 * and so cannot extend the abstract contract. Use it in any PHPUnit TestCase
19 * and implement the abstract hook(s); the abstract class is this trait on a
20 * bare TestCase.
21 */
22trait NotificationOutboxContractTests
23{
24    protected const string CONTRACT_OBJECT = 'https://1r.example.com/logistics-objects/p1';
25    protected const string CONTRACT_PARTNER = 'https://1r.partner.example/logistics-objects/partner';
26
27    abstract protected function createOutbox(): NotificationOutbox;
28
29    /**
30     * What the host would pick up for delivery, oldest first, without removing it.
31     *
32     * @return list<OutboundNotification>
33     */
34    abstract protected function pending(NotificationOutbox $outbox): array;
35
36    public function testEnqueuedNotificationsAreKeptInOrderWithTheirIdentity(): void
37    {
38        $outbox = $this->createOutbox();
39        self::assertSame([], $this->pending($outbox));
40
41        $first = new OutboundNotification(
42            new Iri(self::CONTRACT_PARTNER),
43            new Notification(NotificationEventType::LogisticsObjectUpdated, new Iri(self::CONTRACT_OBJECT), Cargo::Piece, new Iri('https://1r.example.com/action-requests/c1'), [Cargo::goodsDescription]),
44            new DateTimeImmutable('2026-10-02T12:00:00.000Z'),
45            'n-1',
46        );
47        $second = new OutboundNotification(
48            new Iri(self::CONTRACT_PARTNER),
49            new Notification(NotificationEventType::LogisticsEventReceived, new Iri(self::CONTRACT_OBJECT), Cargo::Piece, null, [], [new Iri(self::CONTRACT_OBJECT . '/logistics-events/e1')]),
50            new DateTimeImmutable('2026-10-02T12:00:01.000Z'),
51            'n-2',
52        );
53        $outbox->enqueue($first);
54        $outbox->enqueue($second);
55
56        $pending = $this->pending($outbox);
57        self::assertSame(['n-1', 'n-2'], array_map(static fn(OutboundNotification $n): string => $n->id, $pending));
58        self::assertSame(self::CONTRACT_PARTNER, $pending[0]->recipient->value);
59        self::assertSame('https://1r.partner.example/notifications', $pending[0]->suggestedEndpoint());
60        self::assertSame('2026-10-02T12:00:00.000+00:00', $pending[0]->createdAt->format(DATE_RFC3339_EXTENDED));
61        self::assertSame(NotificationEventType::LogisticsObjectUpdated, $pending[0]->notification->eventType);
62        self::assertSame([Cargo::goodsDescription], $pending[0]->notification->changedProperties);
63        self::assertSame('https://1r.example.com/action-requests/c1', $pending[0]->notification->triggeredBy?->value);
64        self::assertSame(self::CONTRACT_OBJECT . '/logistics-events/e1', $pending[1]->notification->logisticsEvents[0]->value);
65        self::assertSame('api:Notification', $pending[1]->notification->toJsonLd()['@type'], 'the document is ready to send');
66    }
67
68    public function testQueuedBodiesAreSnapshots(): void
69    {
70        $outbox = $this->createOutbox();
71        $piece = \LambdaTwelve\OneRecord\Model\Builder\ObjectBuilder::of(Cargo::Piece)->set(Cargo::goodsDescription, 'SECRET')->build(new Iri(self::CONTRACT_OBJECT));
72        $outbox->enqueue(new OutboundNotification(new Iri(self::CONTRACT_PARTNER), new Notification(NotificationEventType::LogisticsObjectCreated, $piece->iri, Cargo::Piece, null, [], [], $piece), new DateTimeImmutable('2026-10-02T12:00:00Z'), 'n-1'));
73
74        // What the caller does with its object afterwards does not change what will be delivered (R2-005).
75        $piece->graph->add(new \LambdaTwelve\OneRecord\Rdf\Triple($piece->iri, new Iri(Cargo::goodsDescription), \LambdaTwelve\OneRecord\Rdf\Literal::string('after-queue')));
76        $pending = $this->pending($outbox);
77        self::assertCount(1, $pending);
78        $json = json_encode($pending[0]->notification->toJsonLd(), JSON_THROW_ON_ERROR);
79        self::assertStringContainsString('SECRET', $json);
80        self::assertStringNotContainsString('after-queue', $json);
81    }
82
83    public function testPendingReadsAreSnapshotsToo(): void
84    {
85        $outbox = $this->createOutbox();
86        $piece = \LambdaTwelve\OneRecord\Model\Builder\ObjectBuilder::of(Cargo::Piece)->set(Cargo::goodsDescription, 'SECRET')->build(new Iri(self::CONTRACT_OBJECT));
87        $outbox->enqueue(new OutboundNotification(new Iri(self::CONTRACT_PARTNER), new Notification(NotificationEventType::LogisticsObjectCreated, $piece->iri, Cargo::Piece, null, [], [], $piece), new DateTimeImmutable('2026-10-02T12:00:00Z'), 'n-1'));
88
89        // Editing what a read handed out must not change what is delivered later under the same id (R3-003).
90        $first = $this->pending($outbox);
91        self::assertCount(1, $first);
92        $first[0]->notification->body?->graph->add(new \LambdaTwelve\OneRecord\Rdf\Triple($piece->iri, new Iri(Cargo::goodsDescription), \LambdaTwelve\OneRecord\Rdf\Literal::string('CHANGED-AFTER-READ')));
93        $second = $this->pending($outbox);
94        self::assertSame('n-1', $second[0]->id);
95        $json = json_encode($second[0]->notification->toJsonLd(), JSON_THROW_ON_ERROR);
96        self::assertStringContainsString('SECRET', $json);
97        self::assertStringNotContainsString('CHANGED-AFTER-READ', $json);
98    }
99}