Skip to content

Commit

Permalink
TASK: Test ProjectionTransactionTrait when using external projections
Browse files Browse the repository at this point in the history
CASE: $this->dbal->isTransactionActive() === false
  • Loading branch information
mhsdesign committed Dec 3, 2024
1 parent 4a7b058 commit c47d181
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
*/
abstract class AbstractSubscriptionEngineTestCase extends TestCase // we don't use Flows functional test case as it would reset the database afterwards
{
protected static ContentRepositoryId $contentRepositoryId;

protected ContentRepository $contentRepository;

protected SubscriptionEngine $subscriptionEngine;
Expand All @@ -56,16 +58,20 @@ abstract class AbstractSubscriptionEngineTestCase extends TestCase // we don't u

protected CatchUpHookInterface&MockObject $catchupHookForFakeProjection;

public static function setUpBeforeClass(): void
{
static::$contentRepositoryId = ContentRepositoryId::fromString('t_subscription');
}

public function setUp(): void
{
if ($this->getObject(Connection::class)->getDatabasePlatform() instanceof PostgreSQLPlatform) {
$this->markTestSkipped('TODO: The content graph is not available in postgres currently: https://github.com/neos/neos-development-collection/issues/3855');
}
$contentRepositoryId = ContentRepositoryId::fromString('t_subscription');

$this->resetDatabase(
$this->getObject(Connection::class),
$contentRepositoryId,
self::$contentRepositoryId,
keepSchema: true
);

Expand All @@ -78,10 +84,12 @@ public function setUp(): void
$this->fakeProjection
);

$this->secondFakeProjection = new DebugEventProjection(
sprintf('cr_%s_debug_projection', $contentRepositoryId->value),
$this->getObject(Connection::class)
);
if (!isset($this->secondFakeProjection)) {
$this->secondFakeProjection = new DebugEventProjection(
sprintf('cr_%s_debug_projection', self::$contentRepositoryId->value),
$this->getObject(Connection::class)
);
}

FakeProjectionFactory::setProjection(
'second',
Expand All @@ -98,9 +106,9 @@ public function setUp(): void
FakeNodeTypeManagerFactory::setConfiguration([]);
FakeContentDimensionSourceFactory::setWithoutDimensions();

$this->getObject(ContentRepositoryRegistry::class)->resetFactoryInstance($contentRepositoryId);
$this->getObject(ContentRepositoryRegistry::class)->resetFactoryInstance(self::$contentRepositoryId);

$this->setupContentRepositoryDependencies($contentRepositoryId);
$this->setupContentRepositoryDependencies(self::$contentRepositoryId);
}

final protected function setupContentRepositoryDependencies(ContentRepositoryId $contentRepositoryId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\BehavioralTests\Tests\Functional\Subscription;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManagerInterface;
use Neos\ContentRepository\BehavioralTests\TestSuite\DebugEventProjection;

final class ExternalProjectionErrorTest extends AbstractSubscriptionEngineTestCase
{
use ProjectionRollbackTestTrait;

static Connection $secondConnection;

/** @before */
public function injectExternalFakeProjection(): void
{
$entityManager = $this->getObject(EntityManagerInterface::class);

if (!isset(self::$secondConnection)) {
self::$secondConnection = DriverManager::getConnection(
$entityManager->getConnection()->getParams(),
$entityManager->getConfiguration(),
$entityManager->getEventManager()
);
}

$this->secondFakeProjection = new DebugEventProjection(
sprintf('cr_%s_debug_projection', self::$contentRepositoryId->value),
self::$secondConnection
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use Neos\ContentRepository\Core\Subscription\Engine\Errors;
use Neos\ContentRepository\Core\Subscription\Engine\ProcessedResult;
use Neos\ContentRepository\Core\Subscription\Engine\Result;
use Neos\ContentRepository\Core\Subscription\Engine\SubscriptionEngineCriteria;
use Neos\ContentRepository\Core\Subscription\ProjectionSubscriptionStatus;
use Neos\ContentRepository\Core\Subscription\SubscriptionError;
use Neos\ContentRepository\Core\Subscription\SubscriptionId;
Expand All @@ -23,12 +22,15 @@

final class ProjectionErrorTest extends AbstractSubscriptionEngineTestCase
{
use ProjectionRollbackTestTrait;

/** @test */
public function projectionWithError()
{
$this->eventStore->setup();
$this->fakeProjection->expects(self::once())->method('setUp');
$this->subscriptionEngine->setup();
$result = $this->subscriptionEngine->setup();
self::assertNull($result->errors);
$this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok());
$result = $this->subscriptionEngine->boot();
self::assertEquals(ProcessedResult::success(0), $result);
Expand Down Expand Up @@ -84,8 +86,10 @@ public function fixFailedProjectionViaReset()
$this->fakeProjection->expects(self::any())->method('status')->willReturn(ProjectionStatus::ok());
$this->fakeProjection->expects(self::once())->method('resetState');
$this->fakeProjection->expects(self::exactly(2))->method('apply');
$this->subscriptionEngine->setup();
$this->subscriptionEngine->boot();
$result = $this->subscriptionEngine->setup();
self::assertNull($result->errors);
$result = $this->subscriptionEngine->boot();
self::assertNull($result->errors);

// commit an event
$this->commitExampleContentStreamEvent();
Expand Down Expand Up @@ -221,133 +225,6 @@ public function irreparableProjection()
);
}

/** @test */
public function projectionIsRolledBackAfterError()
{
$this->eventStore->setup();
$this->fakeProjection->expects(self::once())->method('setUp');
$this->fakeProjection->expects(self::once())->method('apply');
$result = $this->subscriptionEngine->setup();
self::assertNull($result->errors);
$result = $this->subscriptionEngine->boot();
self::assertNull($result->errors);

// commit an event
$this->commitExampleContentStreamEvent();

$exception = new \RuntimeException('This projection is kaputt.');

$this->secondFakeProjection->injectSaboteur(fn () => throw $exception);

$expectedFailure = ProjectionSubscriptionStatus::create(
subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'),
subscriptionStatus: SubscriptionStatus::ERROR,
subscriptionPosition: SequenceNumber::none(),
subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception),
setupStatus: ProjectionStatus::ok(),
);

self::assertEmpty(
$this->secondFakeProjection->getState()->findAppliedSequenceNumbers()
);

$result = $this->subscriptionEngine->catchUpActive();
self::assertSame($result->errors?->first()->message, 'This projection is kaputt.');

self::assertEquals(
$expectedFailure,
$this->subscriptionStatus('Vendor.Package:SecondFakeProjection')
);

// should be empty as we need an exact once delivery
self::assertEmpty(
$this->secondFakeProjection->getState()->findAppliedSequenceNumbers()
);

//
// fix projection and catchup
//

$this->secondFakeProjection->killSaboteur();

// reactivate and catchup
$result = $this->subscriptionEngine->reactivate(SubscriptionEngineCriteria::create([SubscriptionId::fromString('Vendor.Package:SecondFakeProjection')]));
self::assertNull($result->errors);

$this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(1));
self::assertEquals(
[SequenceNumber::fromInteger(1)],
$this->secondFakeProjection->getState()->findAppliedSequenceNumbers()
);
}

/** @test */
public function projectionIsRolledBackAfterErrorButKeepsSuccessFullEvents()
{
$this->eventStore->setup();
$this->fakeProjection->expects(self::once())->method('setUp');
$this->fakeProjection->expects(self::exactly(2))->method('apply');
$this->subscriptionEngine->setup();
$this->subscriptionEngine->boot();

// commit two events
$this->commitExampleContentStreamEvent();
$this->commitExampleContentStreamEvent();

$exception = new \RuntimeException('Event 2 is kaputt.');

// fail at the second event
$this->secondFakeProjection->injectSaboteur(
fn (EventEnvelope $eventEnvelope) =>
$eventEnvelope->sequenceNumber->value === 2
? throw $exception
: null
);

self::assertEmpty(
$this->secondFakeProjection->getState()->findAppliedSequenceNumbers()
);

$result = $this->subscriptionEngine->catchUpActive();
self::assertTrue($result->hasFailed());

$expectedFailure = ProjectionSubscriptionStatus::create(
subscriptionId: SubscriptionId::fromString('Vendor.Package:SecondFakeProjection'),
subscriptionStatus: SubscriptionStatus::ERROR,
subscriptionPosition: SequenceNumber::fromInteger(1),
subscriptionError: SubscriptionError::fromPreviousStatusAndException(SubscriptionStatus::ACTIVE, $exception),
setupStatus: ProjectionStatus::ok(),
);

self::assertEquals(
$expectedFailure,
$this->subscriptionStatus('Vendor.Package:SecondFakeProjection')
);

// the first successful event is applied and committet:
self::assertEquals(
[SequenceNumber::fromInteger(1)],
$this->secondFakeProjection->getState()->findAppliedSequenceNumbers()
);

//
// fix projection and catchup
//

$this->secondFakeProjection->killSaboteur();

// catchup after fix
$result = $this->subscriptionEngine->reactivate(SubscriptionEngineCriteria::create([SubscriptionId::fromString('Vendor.Package:SecondFakeProjection')]));
self::assertNull($result->errors);

// subscriptionError is reset, and the position is advanced if there were events
$this->expectOkayStatus('Vendor.Package:SecondFakeProjection', SubscriptionStatus::ACTIVE, SequenceNumber::fromInteger(2));
self::assertEquals(
[SequenceNumber::fromInteger(1), SequenceNumber::fromInteger(2)],
$this->secondFakeProjection->getState()->findAppliedSequenceNumbers()
);
}

/** @test */
public function projectionErrorWithMultipleProjectionsInContentRepositoryHandle()
{
Expand Down
Loading

0 comments on commit c47d181

Please sign in to comment.