Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/State/AbstractHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ protected function checkAccessControl(?Provenance $user, StateId $from, Reflecti

foreach ($controls as $accessControl) {
if ($accessControl instanceof DenyAnyOperation) {
if ($accessControl->fromType === null && $accessControl->fromId === null && $accessControl->fromRole === null && $accessControl->fromUser === null) {
return false;
}

if ($accessControl->fromUser && $user->userId === $accessControl->fromUser) {
return false;
}
Expand Down
11 changes: 6 additions & 5 deletions src/State/EntityHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
use Crell\Serde\Attributes\Field;
use Generator;
use Override;
use PropertyHookType;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
Expand Down Expand Up @@ -226,12 +225,12 @@ private function execute(Event $original, string $operation, array $input): Gene
if (! $this->checkAccessControl($this->user, $this->from, ...$operationReflection->getAttributes(AccessControl::class, ReflectionAttribute::IS_INSTANCEOF))) {
throw new SecurityException('Access denied');
}
$operationReflection = match ($operation) {
'get' => $operationReflection->getHook(PropertyHookType::Get),
'set' => $operationReflection->getHook(PropertyHookType::Set),
$result = match ($operation) {
'get' => $this->state->{$property},
'set' => $this->state->{$property} = $input[0],
default => throw new ReflectionException('Unknown operation'),
};
goto done;
goto finalize;
}

$operationReflection = $reflector->getMethod($operation);
Expand Down Expand Up @@ -281,6 +280,8 @@ private function execute(Event $original, string $operation, array $input): Gene
}
}

finalize:

if ($replyTo) {
foreach ($replyTo as $reply) {
yield WithPriority::high(
Expand Down
68 changes: 68 additions & 0 deletions tests/Unit/EntityHistoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@

// namespace Bottledcode\DurablePhp\Tests\Unit;

use Bottledcode\DurablePhp\Contexts\AuthContext\SecurityException;
use Bottledcode\DurablePhp\Events\AwaitResult;
use Bottledcode\DurablePhp\Events\RaiseEvent;
use Bottledcode\DurablePhp\Events\TaskCompleted;
use Bottledcode\DurablePhp\Events\WithEntity;
use Bottledcode\DurablePhp\Events\WithLock;
use Bottledcode\DurablePhp\State\EntityState;
Expand Down Expand Up @@ -193,3 +195,69 @@ public function signal(): void
$finalResult = processEvent($secondResult[1], $otherEntity->applyRaiseEvent(...));
expect($finalResult)->toBeEmpty();
});

it('can get a property value using get signal', function (): void {
// Create an entity state with a property
$history = getEntityHistory(
new class extends EntityState {
public string $testProperty = 'test value';
},
);
$history->from = StateId::fromInstance(OrchestrationInstance('test', 'test'));

// Create a signal to get the property value
$event = RaiseEvent::forOperation('$testProperty::get', []);
$event = AwaitResult::forEvent(StateId::fromInstance(OrchestrationInstance('test', 'test')), $event);

// Process the event
$result = processEvent($event, $history->applyRaiseEvent(...));

// Verify the result contains a TaskCompleted event with the property value
expect($result)->toHaveCount(1);
expect($result[0]->getInnerEvent()->getInnerEvent())->toBeInstanceOf(TaskCompleted::class);
expect($result[0]->getInnerEvent()->getInnerEvent()->result)->toBe(['value' => 'test value']);
});

it('can set a property value using set signal', function (): void {
// Create an entity state with a property
$history = getEntityHistory(
new class extends EntityState {
public string $testProperty = 'initial value';
},
);
$history->from = StateId::fromInstance(OrchestrationInstance('test', 'test'));

// Create a signal to set the property value
$event = RaiseEvent::forOperation('$testProperty::set', ['new value']);
$event = AwaitResult::forEvent(StateId::fromInstance(OrchestrationInstance('test', 'test')), $event);

// Process the event
$result = processEvent($event, $history->applyRaiseEvent(...));

// Verify the property was updated
expect($history->getState()->testProperty)->toBe('new value');

// Verify the result contains a TaskCompleted event
expect($result)->toHaveCount(1);
expect($result[0]->getInnerEvent()->getInnerEvent())->toBeInstanceOf(TaskCompleted::class);
});

it('handles access control for property signals', function (): void {
// Create a mock class with an AccessControl attribute on a property
$from = StateId::fromInstance(OrchestrationInstance('test', 'test'));
$mockClass = new class extends EntityState {
#[Bottledcode\DurablePhp\State\Attributes\DenyAnyOperation(fromType: 'test')]
public string $restrictedProperty = 'restricted value';

public string $publicProperty = 'public value';
};

$history = getEntityHistory($mockClass);
$history->from = $from;

// Try to access the restricted property
$restrictedEvent = RaiseEvent::forOperation('$restrictedProperty::get', []);

// This should throw a SecurityException, which is caught in the execute method
expect(fn() => processEvent($restrictedEvent, $history->applyRaiseEvent(...)))->toThrow(SecurityException::class);
});