Skip to content

Commit e9ec798

Browse files
authored
Merge pull request #4914 from mhsdesign/bugfix/4298-dublicated-contentstream-in-import-and-export
BUGFIX: Duplicated content stream in import and export
2 parents 65bff1f + 75e53d0 commit e9ec798

File tree

12 files changed

+466
-2103
lines changed

12 files changed

+466
-2103
lines changed

.composer.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,20 @@
3636
"test:behavioral": [
3737
"@test:behat-cli -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist",
3838
"@test:behat-cli -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist",
39-
"../../flow doctrine:migrate --quiet; ../../flow cr:setup",
40-
"@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml",
4139
"@test:behat-cli -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist",
42-
"@test:behat-cli -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist"
40+
"@test:behat-cli -c Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist",
41+
"@test:behat-cli -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist",
42+
"../../flow doctrine:migrate --quiet; ../../flow cr:setup",
43+
"@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml"
4344
],
4445
"test:behavioral:stop-on-failure": [
4546
"@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist",
4647
"@test:behat-cli -vvv --stop-on-failure -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist",
47-
"../../flow doctrine:migrate --quiet; ../../flow cr:setup",
48-
"@test:behat-cli -vvv --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml",
4948
"@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist",
50-
"@test:behat-cli -vvv --stop-on-failure -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist"
49+
"@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.Export/Tests/Behavior/behat.yml.dist",
50+
"@test:behat-cli -vvv --stop-on-failure -c Neos.TimeableNodeVisibility/Tests/Behavior/behat.yml.dist",
51+
"../../flow doctrine:migrate --quiet; ../../flow cr:setup",
52+
"@test:behat-cli -vvv --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml"
5153
],
5254
"test": [
5355
"@test:unit",
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Neos.ContentGraph.DoctrineDbalAdapter package.
5+
*
6+
* (c) Contributors of the Neos Project - www.neos.io
7+
*
8+
* This package is Open Source Software. For the full copyright and license
9+
* information, please view the LICENSE file which was distributed with this
10+
* source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap;
16+
17+
use Behat\Gherkin\Node\PyStringNode;
18+
use Behat\Gherkin\Node\TableNode;
19+
use League\Flysystem\Filesystem;
20+
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
21+
use Neos\ContentGraph\DoctrineDbalAdapter\DoctrineDbalContentGraphProjectionFactory;
22+
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryDependencies;
23+
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface;
24+
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
25+
use Neos\ContentRepository\Export\Event\ValueObject\ExportedEvents;
26+
use Neos\ContentRepository\Export\ProcessorResult;
27+
use Neos\ContentRepository\Export\Processors\EventExportProcessor;
28+
use Neos\ContentRepository\Export\Processors\EventStoreImportProcessor;
29+
use Neos\ContentRepository\Export\Severity;
30+
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables;
31+
use PHPUnit\Framework\Assert;
32+
33+
/**
34+
* @todo move this class somewhere where its autoloaded
35+
*/
36+
trait CrImportExportTrait
37+
{
38+
use CRTestSuiteRuntimeVariables;
39+
40+
private Filesystem $crImportExportTrait_filesystem;
41+
42+
private ?ProcessorResult $crImportExportTrait_lastMigrationResult = null;
43+
44+
/** @var array<string> */
45+
private array $crImportExportTrait_loggedErrors = [];
46+
47+
/** @var array<string> */
48+
private array $crImportExportTrait_loggedWarnings = [];
49+
50+
public function setupCrImportExportTrait()
51+
{
52+
$this->crImportExportTrait_filesystem = new Filesystem(new InMemoryFilesystemAdapter());
53+
}
54+
55+
/**
56+
* @When /^the events are exported$/
57+
*/
58+
public function theEventsAreExportedIExpectTheFollowingJsonl()
59+
{
60+
$eventExporter = $this->getContentRepositoryService(
61+
new class ($this->crImportExportTrait_filesystem) implements ContentRepositoryServiceFactoryInterface {
62+
public function __construct(private readonly Filesystem $filesystem)
63+
{
64+
}
65+
public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): EventExportProcessor {
66+
return new EventExportProcessor(
67+
$this->filesystem,
68+
$serviceFactoryDependencies->contentRepository->getWorkspaceFinder(),
69+
$serviceFactoryDependencies->eventStore
70+
);
71+
}
72+
}
73+
);
74+
assert($eventExporter instanceof EventExportProcessor);
75+
76+
$eventExporter->onMessage(function (Severity $severity, string $message) {
77+
if ($severity === Severity::ERROR) {
78+
$this->crImportExportTrait_loggedErrors[] = $message;
79+
} elseif ($severity === Severity::WARNING) {
80+
$this->crImportExportTrait_loggedWarnings[] = $message;
81+
}
82+
});
83+
$this->crImportExportTrait_lastMigrationResult = $eventExporter->run();
84+
}
85+
86+
/**
87+
* @When /^I import the events\.jsonl(?: into "([^"]*)")?$/
88+
*/
89+
public function iImportTheFollowingJson(?string $contentStreamId = null)
90+
{
91+
$eventImporter = $this->getContentRepositoryService(
92+
new class ($this->crImportExportTrait_filesystem, $contentStreamId ? ContentStreamId::fromString($contentStreamId) : null) implements ContentRepositoryServiceFactoryInterface {
93+
public function __construct(
94+
private readonly Filesystem $filesystem,
95+
private readonly ?ContentStreamId $contentStreamId
96+
) {
97+
}
98+
public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): EventStoreImportProcessor {
99+
return new EventStoreImportProcessor(
100+
false,
101+
$this->filesystem,
102+
$serviceFactoryDependencies->eventStore,
103+
$serviceFactoryDependencies->eventNormalizer,
104+
$this->contentStreamId
105+
);
106+
}
107+
}
108+
);
109+
assert($eventImporter instanceof EventStoreImportProcessor);
110+
111+
$eventImporter->onMessage(function (Severity $severity, string $message) {
112+
if ($severity === Severity::ERROR) {
113+
$this->crImportExportTrait_loggedErrors[] = $message;
114+
} elseif ($severity === Severity::WARNING) {
115+
$this->crImportExportTrait_loggedWarnings[] = $message;
116+
}
117+
});
118+
$this->crImportExportTrait_lastMigrationResult = $eventImporter->run();
119+
}
120+
121+
/**
122+
* @Given /^using the following events\.jsonl:$/
123+
*/
124+
public function usingTheFollowingEventsJsonl(PyStringNode $string)
125+
{
126+
$this->crImportExportTrait_filesystem->write('events.jsonl', $string->getRaw());
127+
}
128+
129+
/**
130+
* @AfterScenario
131+
*/
132+
public function failIfLastMigrationHasErrors(): void
133+
{
134+
if ($this->crImportExportTrait_lastMigrationResult !== null && $this->crImportExportTrait_lastMigrationResult->severity === Severity::ERROR) {
135+
throw new \RuntimeException(sprintf('The last migration run led to an error: %s', $this->crImportExportTrait_lastMigrationResult->message));
136+
}
137+
if ($this->crImportExportTrait_loggedErrors !== []) {
138+
throw new \RuntimeException(sprintf('The last migration run logged %d error%s', count($this->crImportExportTrait_loggedErrors), count($this->crImportExportTrait_loggedErrors) === 1 ? '' : 's'));
139+
}
140+
}
141+
142+
/**
143+
* @Then I expect the following jsonl:
144+
*/
145+
public function iExpectTheFollowingJsonL(PyStringNode $string): void
146+
{
147+
if (!$this->crImportExportTrait_filesystem->has('events.jsonl')) {
148+
Assert::fail('No events were exported');
149+
}
150+
151+
$jsonL = $this->crImportExportTrait_filesystem->read('events.jsonl');
152+
153+
$exportedEvents = ExportedEvents::fromJsonl($jsonL);
154+
$eventsWithoutRandomIds = [];
155+
156+
foreach ($exportedEvents as $exportedEvent) {
157+
// we have to remove the event id in \Neos\ContentRepository\Core\Feature\Common\NodeAggregateEventPublisher::enrichWithCommand
158+
// and the initiatingTimestamp to make the events diff able
159+
$eventsWithoutRandomIds[] = $exportedEvent
160+
->withIdentifier('random-event-uuid')
161+
->processMetadata(function (array $metadata) {
162+
$metadata['initiatingTimestamp'] = 'random-time';
163+
return $metadata;
164+
});
165+
}
166+
167+
Assert::assertSame($string->getRaw(), ExportedEvents::fromIterable($eventsWithoutRandomIds)->toJsonl());
168+
}
169+
170+
/**
171+
* @Then I expect the following errors to be logged
172+
*/
173+
public function iExpectTheFollowingErrorsToBeLogged(TableNode $table): void
174+
{
175+
Assert::assertSame($table->getColumn(0), $this->crImportExportTrait_loggedErrors, 'Expected logged errors do not match');
176+
$this->crImportExportTrait_loggedErrors = [];
177+
}
178+
179+
/**
180+
* @Then I expect the following warnings to be logged
181+
*/
182+
public function iExpectTheFollowingWarningsToBeLogged(TableNode $table): void
183+
{
184+
Assert::assertSame($table->getColumn(0), $this->crImportExportTrait_loggedWarnings, 'Expected logged warnings do not match');
185+
$this->crImportExportTrait_loggedWarnings = [];
186+
}
187+
188+
/**
189+
* @Then I expect a MigrationError
190+
* @Then I expect a MigrationError with the message
191+
*/
192+
public function iExpectAMigrationErrorWithTheMessage(PyStringNode $expectedMessage = null): void
193+
{
194+
Assert::assertNotNull($this->crImportExportTrait_lastMigrationResult, 'Expected the previous migration to contain errors, but no migration has been executed');
195+
Assert::assertSame(Severity::ERROR, $this->crImportExportTrait_lastMigrationResult->severity, sprintf('Expected the previous migration to contain errors, but it ended with severity "%s"', $this->crImportExportTrait_lastMigrationResult->severity->name));
196+
if ($expectedMessage !== null) {
197+
Assert::assertSame($expectedMessage->getRaw(), $this->crImportExportTrait_lastMigrationResult->message);
198+
}
199+
$this->crImportExportTrait_lastMigrationResult = null;
200+
}
201+
202+
/**
203+
* @template T of object
204+
* @param class-string<T> $className
205+
*
206+
* @return T
207+
*/
208+
abstract private function getObject(string $className): object;
209+
210+
protected function getTableNamePrefix(): string
211+
{
212+
return DoctrineDbalContentGraphProjectionFactory::graphProjectionTableNamePrefix(
213+
$this->currentContentRepository->id
214+
);
215+
}
216+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/*
5+
* This file is part of the Neos.ContentRepository package.
6+
*
7+
* (c) Contributors of the Neos Project - www.neos.io
8+
*
9+
* This package is Open Source Software. For the full copyright and license
10+
* information, please view the LICENSE file which was distributed with this
11+
* source code.
12+
*/
13+
14+
require_once(__DIR__ . '/CrImportExportTrait.php');
15+
16+
use Behat\Behat\Context\Context as BehatContext;
17+
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
18+
use Neos\Behat\FlowBootstrapTrait;
19+
use Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap\CrImportExportTrait;
20+
use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\CRBehavioralTestsSubjectProvider;
21+
use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinPyStringNodeBasedNodeTypeManagerFactory;
22+
use Neos\ContentRepository\BehavioralTests\TestSuite\Behavior\GherkinTableNodeBasedContentDimensionSourceFactory;
23+
use Neos\ContentRepository\Core\ContentRepository;
24+
use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
25+
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface;
26+
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
27+
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteTrait;
28+
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
29+
30+
/**
31+
* Features context
32+
*/
33+
class FeatureContext implements BehatContext
34+
{
35+
use FlowBootstrapTrait;
36+
use CrImportExportTrait;
37+
use CRTestSuiteTrait;
38+
use CRBehavioralTestsSubjectProvider;
39+
40+
protected ContentRepositoryRegistry $contentRepositoryRegistry;
41+
42+
public function __construct()
43+
{
44+
self::bootstrapFlow();
45+
$this->contentRepositoryRegistry = $this->getObject(ContentRepositoryRegistry::class);
46+
47+
$this->setupCRTestSuiteTrait();
48+
$this->setupCrImportExportTrait();
49+
}
50+
51+
/**
52+
* @BeforeScenario
53+
*/
54+
public function resetContentRepositoryComponents(BeforeScenarioScope $scope): void
55+
{
56+
GherkinTableNodeBasedContentDimensionSourceFactory::reset();
57+
GherkinPyStringNodeBasedNodeTypeManagerFactory::reset();
58+
}
59+
60+
protected function getContentRepositoryService(
61+
ContentRepositoryServiceFactoryInterface $factory
62+
): ContentRepositoryServiceInterface {
63+
return $this->contentRepositoryRegistry->buildService(
64+
$this->currentContentRepository->id,
65+
$factory
66+
);
67+
}
68+
69+
protected function createContentRepository(
70+
ContentRepositoryId $contentRepositoryId
71+
): ContentRepository {
72+
$this->contentRepositoryRegistry->resetFactoryInstance($contentRepositoryId);
73+
$contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId);
74+
GherkinTableNodeBasedContentDimensionSourceFactory::reset();
75+
GherkinPyStringNodeBasedNodeTypeManagerFactory::reset();
76+
77+
return $contentRepository;
78+
}
79+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
@contentrepository
2+
Feature: As a user of the CR I want to export the event stream
3+
Background:
4+
Given using the following content dimensions:
5+
| Identifier | Values | Generalizations |
6+
| language | de, gsw, fr | gsw->de |
7+
And using the following node types:
8+
"""yaml
9+
'Neos.ContentRepository.Testing:Document': []
10+
"""
11+
And using identifier "default", I define a content repository
12+
And I am in content repository "default"
13+
And the command CreateRootWorkspace is executed with payload:
14+
| Key | Value |
15+
| workspaceName | "live" |
16+
| workspaceTitle | "Live" |
17+
| workspaceDescription | "The live workspace" |
18+
| newContentStreamId | "cs-identifier" |
19+
And the graph projection is fully up to date
20+
And the command CreateRootNodeAggregateWithNode is executed with payload:
21+
| Key | Value |
22+
| contentStreamId | "cs-identifier" |
23+
| nodeAggregateId | "lady-eleonode-rootford" |
24+
| nodeTypeName | "Neos.ContentRepository:Root" |
25+
And the event NodeAggregateWithNodeWasCreated was published with payload:
26+
| Key | Value |
27+
| contentStreamId | "cs-identifier" |
28+
| nodeAggregateId | "nody-mc-nodeface" |
29+
| nodeTypeName | "Neos.ContentRepository.Testing:Document" |
30+
| originDimensionSpacePoint | {"language":"de"} |
31+
| coveredDimensionSpacePoints | [{"language":"de"},{"language":"gsw"},{"language":"fr"}] |
32+
| parentNodeAggregateId | "lady-eleonode-rootford" |
33+
| nodeName | "child-document" |
34+
| nodeAggregateClassification | "regular" |
35+
And the graph projection is fully up to date
36+
37+
Scenario: Export the event stream
38+
Then I expect exactly 3 events to be published on stream with prefix "ContentStream:cs-identifier"
39+
When the events are exported
40+
Then I expect the following jsonl:
41+
"""
42+
{"identifier":"random-event-uuid","type":"RootNodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"cs-identifier","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","coveredDimensionSpacePoints":[{"language":"de"},{"language":"gsw"},{"language":"fr"}],"nodeAggregateClassification":"root"},"metadata":{"commandClass":"Neos\\ContentRepository\\Core\\Feature\\RootNodeCreation\\Command\\CreateRootNodeAggregateWithNode","commandPayload":{"contentStreamId":"cs-identifier","nodeAggregateId":"lady-eleonode-rootford","nodeTypeName":"Neos.ContentRepository:Root","tetheredDescendantNodeAggregateIds":[]},"initiatingUserId":"system","initiatingTimestamp":"random-time"}}
43+
{"identifier":"random-event-uuid","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"cs-identifier","nodeAggregateId":"nody-mc-nodeface","nodeTypeName":"Neos.ContentRepository.Testing:Document","originDimensionSpacePoint":{"language":"de"},"coveredDimensionSpacePoints":[{"language":"de"},{"language":"gsw"},{"language":"fr"}],"parentNodeAggregateId":"lady-eleonode-rootford","nodeName":"child-document","initialPropertyValues":[],"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":{"initiatingTimestamp":"random-time"}}
44+
45+
"""

0 commit comments

Comments
 (0)