Skip to content

Commit 70d0650

Browse files
committed
add more tests
1 parent efeaa02 commit 70d0650

7 files changed

Lines changed: 255 additions & 16 deletions

File tree

src/DecisionModel/StoreDecisionModelBuilder.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
final class StoreDecisionModelBuilder implements DecisionModelBuilder
1414
{
1515
public function __construct(
16-
private AppendStore $store,
16+
private readonly AppendStore $store,
1717
) {
1818
}
1919

@@ -31,15 +31,15 @@ public function build(
3131
$highestId = 0;
3232

3333
foreach ($stream as $message) {
34-
$highestId = $stream->index();
34+
$highestId = $stream->index() ?? 0;
3535
$state = $projection->apply($state, $message);
3636
}
3737

3838
return new DecisionModel(
3939
$state,
4040
new AppendCondition(
4141
$query,
42-
$highestId ?? 0,
42+
$highestId,
4343
),
4444
);
4545
}

src/DecisionModel/StoreEventAppender.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ public function append(
3030
AppendCondition|null $appendCondition = null,
3131
string|null $streamName = null,
3232
): void {
33+
if ($events === []) {
34+
return;
35+
}
36+
3337
$messages = array_map(
3438
fn (object $event) => Message::create($event)
3539
->withHeader(new StreamNameHeader($streamName ?? $this->defaultStreamName))

src/Projection/BasicProjection.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ abstract class BasicProjection implements Projection, SubQueryProvider
2929
/** @var array<class-string, string>|null $applyMethods */
3030
private array|null $applyMethods = null;
3131

32+
private SubQuery|null $subQuery = null;
33+
3234
/**
3335
* @param S $state
3436
*
@@ -53,12 +55,16 @@ public function apply(mixed $state, Message $message): mixed
5355

5456
public function subQuery(): SubQuery
5557
{
56-
return new SubQuery(
57-
$this->tagFilter(),
58-
$this->eventTypeFilter(),
59-
$this->streamName(),
60-
$this->lastEventIsEnough(),
61-
);
58+
if ($this->subQuery === null) {
59+
$this->subQuery = new SubQuery(
60+
$this->tagFilter(),
61+
$this->eventTypeFilter(),
62+
$this->streamName(),
63+
$this->lastEventIsEnough(),
64+
);
65+
}
66+
67+
return $this->subQuery;
6268
}
6369

6470
/** @return list<class-string> */

src/Projection/CompositeProjection.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use Patchlevel\EventSourcing\Message\Message;
88
use Patchlevel\EventSourcing\Store\Query;
9-
use Patchlevel\EventSourcing\Store\SubQuery;
109

1110
use function array_map;
1211

@@ -24,13 +23,11 @@ public function query(): Query
2423
$subQueries = [];
2524

2625
foreach ($this->projections as $projection) {
27-
if ($projection instanceof SubQueryProvider) {
28-
$subQueries[] = $projection->subQuery();
29-
30-
continue;
26+
if (!$projection instanceof SubQueryProvider) {
27+
return new Query();
3128
}
3229

33-
$subQueries[] = new SubQuery();
30+
$subQueries[] = $projection->subQuery();
3431
}
3532

3633
$query = new Query(...$subQueries);

src/Store/AppendCondition.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
final class AppendCondition
99
{
1010
public function __construct(
11-
public readonly Query $query,
11+
public readonly Query $query = new Query(),
1212
public readonly int|null $highestSequenceNumber = null,
1313
) {
1414
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Unit\DecisionModel;
6+
7+
use Patchlevel\EventSourcing\DecisionModel\StoreDecisionModelBuilder;
8+
use Patchlevel\EventSourcing\Message\Message;
9+
use Patchlevel\EventSourcing\Store\AppendCondition;
10+
use Patchlevel\EventSourcing\Store\AppendStore;
11+
use Patchlevel\EventSourcing\Store\ArrayStream;
12+
use Patchlevel\EventSourcing\Store\Header\TagsHeader;
13+
use Patchlevel\EventSourcing\Store\Query;
14+
use Patchlevel\EventSourcing\Store\SubQuery;
15+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email;
16+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\IncrementProjection;
17+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated;
18+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId;
19+
use PHPUnit\Framework\Attributes\CoversClass;
20+
use PHPUnit\Framework\TestCase;
21+
22+
#[CoversClass(StoreDecisionModelBuilder::class)]
23+
final class StoreDecisionModelBuilderTest extends TestCase
24+
{
25+
public function testEmpty(): void
26+
{
27+
$store = $this->createMock(AppendStore::class);
28+
$store->expects($this->once())->method('query')->with(new Query())->willReturn(new ArrayStream([]));
29+
30+
$builder = new StoreDecisionModelBuilder($store);
31+
32+
$state = $builder->build([]);
33+
34+
self::assertEquals([], $state->state);
35+
self::assertEquals(new AppendCondition(new Query()), $state->appendCondition);
36+
}
37+
38+
public function testWithProjections(): void
39+
{
40+
$store = $this->createMock(AppendStore::class);
41+
42+
$message = new Message(
43+
new ProfileCreated(
44+
ProfileId::fromString('1'),
45+
Email::fromString('info@patchlevel.de'),
46+
),
47+
);
48+
$message = $message->withHeader(new TagsHeader(['foo']));
49+
50+
$expectedQuery = new Query(
51+
new SubQuery(
52+
['foo'],
53+
[ProfileCreated::class],
54+
),
55+
);
56+
57+
$store->expects($this->once())->method('query')->with($expectedQuery)->willReturn(new ArrayStream([$message]));
58+
59+
$builder = new StoreDecisionModelBuilder($store);
60+
61+
$state = $builder->build([
62+
'counter' => new IncrementProjection(0, ['foo']),
63+
]);
64+
65+
self::assertEquals(['counter' => 1], $state->state);
66+
67+
self::assertEquals(new AppendCondition($expectedQuery, 1), $state->appendCondition);
68+
}
69+
70+
public function testEmptyStream(): void
71+
{
72+
$store = $this->createMock(AppendStore::class);
73+
74+
$expectedQuery = new Query(
75+
new SubQuery(
76+
['foo'],
77+
[ProfileCreated::class],
78+
),
79+
);
80+
81+
$store->expects($this->once())->method('query')->with($expectedQuery)->willReturn(new ArrayStream());
82+
83+
$builder = new StoreDecisionModelBuilder($store);
84+
85+
$state = $builder->build([
86+
'counter' => new IncrementProjection(0, ['foo']),
87+
]);
88+
89+
self::assertEquals(['counter' => 0], $state->state);
90+
91+
self::assertEquals(new AppendCondition($expectedQuery, 0), $state->appendCondition);
92+
}
93+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Unit\DecisionModel;
6+
7+
use Patchlevel\EventSourcing\DecisionModel\StoreEventAppender;
8+
use Patchlevel\EventSourcing\Message\Message;
9+
use Patchlevel\EventSourcing\Serializer\EventTagExtractor;
10+
use Patchlevel\EventSourcing\Store\AppendCondition;
11+
use Patchlevel\EventSourcing\Store\AppendStore;
12+
use Patchlevel\EventSourcing\Store\Header\StreamNameHeader;
13+
use Patchlevel\EventSourcing\Store\Header\TagsHeader;
14+
use Patchlevel\EventSourcing\Store\Query;
15+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email;
16+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated;
17+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId;
18+
use PHPUnit\Framework\Attributes\CoversClass;
19+
use PHPUnit\Framework\TestCase;
20+
21+
#[CoversClass(StoreEventAppender::class)]
22+
final class StoreEventAppenderTest extends TestCase
23+
{
24+
public function testAppendNothing(): void
25+
{
26+
$store = $this->createMock(AppendStore::class);
27+
$store->expects($this->never())->method('append');
28+
29+
$appender = new StoreEventAppender($store);
30+
31+
$appender->append([]);
32+
}
33+
34+
public function testAppendEventsWithoutCondition(): void
35+
{
36+
$expectedEvent = new ProfileCreated(
37+
ProfileId::fromString('1'),
38+
Email::fromString('info@patchlevel.de'),
39+
);
40+
41+
$expectedMessage = (new Message($expectedEvent))
42+
->withHeader(new TagsHeader([]))
43+
->withHeader(new StreamNameHeader('main'));
44+
45+
$store = $this->createMock(AppendStore::class);
46+
$store->expects($this->once())->method('append')->with([$expectedMessage], null);
47+
48+
$appender = new StoreEventAppender($store);
49+
50+
$appender->append([$expectedEvent]);
51+
}
52+
53+
public function testAppendEventsWithCondition(): void
54+
{
55+
$appendCondition = new AppendCondition(new Query(), 5);
56+
57+
$event = new ProfileCreated(
58+
ProfileId::fromString('1'),
59+
Email::fromString('info@patchlevel.de'),
60+
);
61+
62+
$message = (new Message($event))
63+
->withHeader(new TagsHeader([]))
64+
->withHeader(new StreamNameHeader('main'));
65+
66+
$store = $this->createMock(AppendStore::class);
67+
$store->expects($this->once())->method('append')->with([$message], $appendCondition);
68+
69+
$appender = new StoreEventAppender($store);
70+
71+
$appender->append([$event], $appendCondition);
72+
}
73+
74+
public function testChangeDefaultStreamName(): void
75+
{
76+
$appendCondition = new AppendCondition(new Query(), 5);
77+
78+
$event = new ProfileCreated(
79+
ProfileId::fromString('1'),
80+
Email::fromString('info@patchlevel.de'),
81+
);
82+
83+
$message = (new Message($event))
84+
->withHeader(new TagsHeader([]))
85+
->withHeader(new StreamNameHeader('foo'));
86+
87+
$store = $this->createMock(AppendStore::class);
88+
$store->expects($this->once())->method('append')->with([$message], $appendCondition);
89+
90+
$appender = new StoreEventAppender($store, defaultStreamName: 'foo');
91+
92+
$appender->append([$event], $appendCondition);
93+
}
94+
95+
public function testChangeStreamName(): void
96+
{
97+
$appendCondition = new AppendCondition(new Query(), 5);
98+
99+
$event = new ProfileCreated(
100+
ProfileId::fromString('1'),
101+
Email::fromString('info@patchlevel.de'),
102+
);
103+
104+
$message = (new Message($event))
105+
->withHeader(new TagsHeader([]))
106+
->withHeader(new StreamNameHeader('bar'));
107+
108+
$store = $this->createMock(AppendStore::class);
109+
$store->expects($this->once())->method('append')->with([$message], $appendCondition);
110+
111+
$appender = new StoreEventAppender($store, defaultStreamName: 'foo');
112+
113+
$appender->append([$event], $appendCondition, 'bar');
114+
}
115+
116+
public function testExtractTags(): void
117+
{
118+
$appendCondition = new AppendCondition(new Query(), 5);
119+
120+
$event = new ProfileCreated(
121+
ProfileId::fromString('1'),
122+
Email::fromString('info@patchlevel.de'),
123+
);
124+
125+
$message = (new Message($event))
126+
->withHeader(new TagsHeader(['profile:1']))
127+
->withHeader(new StreamNameHeader('main'));
128+
129+
$store = $this->createMock(AppendStore::class);
130+
$store->expects($this->once())->method('append')->with([$message], $appendCondition);
131+
132+
$tagExtractor = $this->createMock(EventTagExtractor::class);
133+
$tagExtractor->expects($this->once())->method('extract')->with($event)->willReturn(['profile:1']);
134+
135+
$appender = new StoreEventAppender($store, $tagExtractor);
136+
137+
$appender->append([$event], $appendCondition);
138+
}
139+
}

0 commit comments

Comments
 (0)