Skip to content

Commit

Permalink
Implement PassesClaimConditions rule for custom conditional logic. (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
v16Studios authored Aug 9, 2023
1 parent 7b7276b commit 9402c8b
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 2 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
"ext-json": "*",
"ext-openssl": "*",
"enjin/platform-core": "*",
"phrity/websocket": "^1.0",
"rebing/graphql-laravel": "^9.0.0-rc1",
"spatie/laravel-package-tools": "^1.0",
"spatie/laravel-ray": "^1.0",
"phrity/websocket": "^1.0"
"spatie/laravel-ray": "^1.0"
},
"autoload": {
"psr-4": {
Expand Down
1 change: 1 addition & 0 deletions lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'end_date_after_start' => 'The :attribute must be a date after start.',
'end_date_greater_than' => 'The :attribute must be a date greater than :value.',
'is_paused' => 'The beam is paused.',
'passes_conditions' => 'Not all the conditions to claim have been met.',
'scan_limit' => 'You have reached the maximum limit to retry.',
'start_date_after_end' => 'The :attribute must be a date before end.',
'start_date_has_passed' => 'The start date of this beam has passed, it can no longer be modified.',
Expand Down
2 changes: 2 additions & 0 deletions src/GraphQL/Mutations/ClaimBeamMutation.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Enjin\Platform\Beam\Rules\NotExpired;
use Enjin\Platform\Beam\Rules\NotOwner;
use Enjin\Platform\Beam\Rules\NotPaused;
use Enjin\Platform\Beam\Rules\PassesClaimConditions;
use Enjin\Platform\Beam\Rules\SingleUseCodeExist;
use Enjin\Platform\Beam\Rules\VerifySignedMessage;
use Enjin\Platform\Beam\Services\BeamService;
Expand Down Expand Up @@ -104,6 +105,7 @@ protected function rules(array $args = []): array
$singleUse ? new SingleUseCodeExist() : '',
new CanClaim($singleUse),
new NotPaused($beamCode),
new PassesClaimConditions($singleUse),
],
'account' => ['filled', new ValidSubstrateAccount(), new NotOwner($singleUse)],
'signature' => ['sometimes', new VerifySignedMessage()],
Expand Down
93 changes: 93 additions & 0 deletions src/Rules/PassesClaimConditions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Enjin\Platform\Beam\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Laravel\SerializableClosure\Support\ReflectionClosure;

class PassesClaimConditions implements DataAwareRule, ValidationRule
{
protected static array $functions = [];

protected array $data = [];

/**
* Create new rule instance.
*/
public function __construct(
protected bool $singleUse
) {
}

public static function addConditionalFunctions(Closure|array|Collection $functions): void
{
if ($functions instanceof Closure) {
$functions = Arr::wrap($functions);
} elseif ($functions instanceof Collection) {
$functions = $functions->toArray();
}

static::$functions = array_merge(static::$functions, $functions);
}

public static function getConditionalFunctions(): array
{
return static::$functions;
}

public static function removeConditionalFunctions(Closure|array|Collection $functions): void
{
if ($functions instanceof Closure) {
$functions = Arr::wrap($functions);
} elseif ($functions instanceof Collection) {
$functions = $functions->toArray();
}

$staticFunctions = collect(static::$functions)->mapWithKeys(function ($function) {
$hash = sha1((new ReflectionClosure($function))->getCode());

return [$hash => $function];
});

$diffFunctions = collect($functions)->mapWithKeys(function ($function) {
$hash = sha1((new ReflectionClosure($function))->getCode());

return [$hash => $function];
});

static::$functions = $staticFunctions->diffKeys($diffFunctions)->values()->toArray();
}

public static function clearConditionalFunctions(): void
{
static::$functions = [];
}

/**
* Get the validation error message.
*/
public function message()
{
return __('enjin-platform-beam::validation.passes_conditions');
}

public function validate(string $attribute, mixed $value, Closure $fail): void
{
$conditions = collect(static::$functions);

if (!$conditions->every(fn ($function) => $function($attribute, $value, $this->singleUse, $this->data))) {
$fail(__('enjin-platform-beam::validation.passes_conditions'));
}
}

public function setData(array $data)
{
$this->data = $data;

return $this;
}
}
149 changes: 149 additions & 0 deletions tests/Feature/GraphQL/Mutations/ClaimBeamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Enjin\Platform\Beam\Events\BeamClaimPending;
use Enjin\Platform\Beam\Jobs\ClaimBeam;
use Enjin\Platform\Beam\Models\BeamClaim;
use Enjin\Platform\Beam\Rules\PassesClaimConditions;
use Enjin\Platform\Beam\Tests\Feature\GraphQL\TestCaseGraphQL;
use Enjin\Platform\Beam\Tests\Feature\Traits\CreateBeamData;
use Enjin\Platform\Beam\Tests\Feature\Traits\SeedBeamData;
Expand Down Expand Up @@ -103,6 +104,154 @@ public function test_it_can_claim_beam_with_ed25519(): void
$this->genericClaimTest(CryptoSignatureType::ED25519);
}

/**
* Test it can remove a condition from the rule.
*/
public function test_it_can_remove_a_condition_from_the_rule(): void
{
PassesClaimConditions::addConditionalFunctions([
function ($attribute, $code, $singleUse, $data) {
return CryptoSignatureType::ED25519->name == $data['cryptoSignatureType'];
},
function ($attribute, $code, $singleUse, $data) {
return 'code' == $attribute;
},
]);
$this->assertCount(2, PassesClaimConditions::getConditionalFunctions());

PassesClaimConditions::removeConditionalFunctions(
function ($attribute, $code, $singleUse, $data) {
return 'code' == $attribute;
}
);
$this->assertCount(1, PassesClaimConditions::getConditionalFunctions());

$this->genericClaimTest(CryptoSignatureType::ED25519);

PassesClaimConditions::clearConditionalFunctions();
$this->assertEmpty(PassesClaimConditions::getConditionalFunctions());
}

/**
* Test claiming beam with a single passing condition.
*/
public function test_it_can_claim_beam_with_single_condition_that_passes(): void
{
PassesClaimConditions::addConditionalFunctions(function ($attribute, $code, $singleUse, $data) {
return CryptoSignatureType::ED25519->name == $data['cryptoSignatureType'];
});
$this->assertNotEmpty(PassesClaimConditions::getConditionalFunctions());

$this->genericClaimTest(CryptoSignatureType::ED25519);

PassesClaimConditions::clearConditionalFunctions();
$this->assertEmpty(PassesClaimConditions::getConditionalFunctions());
}

/**
* Test claiming beam with multiple passing conditions.
*/
public function test_it_can_claim_beam_with_multiple_conditions_that_pass(): void
{
PassesClaimConditions::addConditionalFunctions([
function ($attribute, $code, $singleUse, $data) {
return CryptoSignatureType::ED25519->name == $data['cryptoSignatureType'];
},
function ($attribute, $code, $singleUse, $data) {
return 'code' == $attribute;
},
]);
$this->assertNotEmpty(PassesClaimConditions::getConditionalFunctions());

$this->genericClaimTest(CryptoSignatureType::ED25519);

PassesClaimConditions::clearConditionalFunctions();
$this->assertEmpty(PassesClaimConditions::getConditionalFunctions());
}

/**
* Test claiming beam with failing conditions.
*/
public function test_it_cannot_claim_beam_with_single_condition_that_fails(): void
{
PassesClaimConditions::addConditionalFunctions(function ($attribute, $code, $singleUse, $data) {
return CryptoSignatureType::SR25519->name == $data['cryptoSignatureType'];
});
$this->assertNotEmpty(PassesClaimConditions::getConditionalFunctions());

[$keypair, $publicKey, $privateKey] = $this->getKeyPair(CryptoSignatureType::ED25519);

$response = $this->graphql('GetBeam', [
'code' => $this->beam->code,
'account' => $publicKey,
]);
$this->assertNotEmpty($response['message']);

$message = $response['message']['message'];
$signature = $this->signMessage(CryptoSignatureType::ED25519, $keypair, $message, $privateKey);

Queue::fake();

$response = $this->graphql($this->method, [
'code' => $this->beam->code,
'account' => $publicKey,
'signature' => $signature,
'cryptoSignatureType' => CryptoSignatureType::ED25519->name,
], true);

$this->assertNotEmpty($response);

$this->assertArraySubset(['code' => ['Not all the conditions to claim have been met.']], $response['error']);

PassesClaimConditions::clearConditionalFunctions();
$this->assertEmpty(PassesClaimConditions::getConditionalFunctions());
}

/**
* Test claiming beam with multiple failing conditions.
*/
public function test_it_cannot_claim_beam_with_multiple_conditions_that_fail(): void
{
$functions = collect([
function ($attribute, $code, $singleUse, $data) {
return 'code' == $attribute;
},
function ($attribute, $code, $singleUse, $data) {
return CryptoSignatureType::SR25519->name == $data['cryptoSignatureType'];
},
]);

PassesClaimConditions::addConditionalFunctions($functions);
$this->assertNotEmpty(PassesClaimConditions::getConditionalFunctions());

[$keypair, $publicKey, $privateKey] = $this->getKeyPair(CryptoSignatureType::ED25519);

$response = $this->graphql('GetBeam', [
'code' => $this->beam->code,
'account' => $publicKey,
]);
$this->assertNotEmpty($response['message']);

$message = $response['message']['message'];
$signature = $this->signMessage(CryptoSignatureType::ED25519, $keypair, $message, $privateKey);

Queue::fake();

$response = $this->graphql($this->method, [
'code' => $this->beam->code,
'account' => $publicKey,
'signature' => $signature,
'cryptoSignatureType' => CryptoSignatureType::ED25519->name,
], true);

$this->assertNotEmpty($response);

$this->assertArraySubset(['code' => ['Not all the conditions to claim have been met.']], $response['error']);

PassesClaimConditions::clearConditionalFunctions();
$this->assertEmpty(PassesClaimConditions::getConditionalFunctions());
}

/**
* Test claiming beam with expired date.
*/
Expand Down

0 comments on commit 9402c8b

Please sign in to comment.