From 4a452ef1febb9ffe6154935ba04d6f4f79694716 Mon Sep 17 00:00:00 2001 From: philipp lang Date: Wed, 7 Jun 2023 22:52:02 +0200 Subject: [PATCH] Add type validation --- app/Mailgateway/Actions/StoreAction.php | 45 +++++++++++++-- .../Resources/MailgatewayResource.php | 2 +- app/Mailgateway/Types/LocalType.php | 5 ++ app/Mailgateway/Types/MailmanType.php | 7 ++- app/Mailgateway/Types/Type.php | 24 ++++++++ phpstan.neon | 1 + resources/css/buttons.css | 3 + resources/js/components/ui/Button.vue | 2 +- resources/js/views/mailgateway/Index.vue | 27 ++++++--- tests/Feature/Mailgateway/IndexTest.php | 10 ++++ tests/Feature/Mailgateway/StoreTest.php | 57 ++++++++++++++++++- .../MailgatewayRequestFactory.php | 19 +++++++ 12 files changed, 185 insertions(+), 17 deletions(-) diff --git a/app/Mailgateway/Actions/StoreAction.php b/app/Mailgateway/Actions/StoreAction.php index cc66b1fb..5a2e6fbf 100644 --- a/app/Mailgateway/Actions/StoreAction.php +++ b/app/Mailgateway/Actions/StoreAction.php @@ -3,6 +3,7 @@ namespace App\Mailgateway\Actions; use App\Mailgateway\Models\Mailgateway; +use App\Mailgateway\Types\Type; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; use Lorisleiva\Actions\ActionRequest; @@ -12,26 +13,62 @@ class StoreAction { use AsAction; - public function handle(array $input) + /** + * @param array $input + */ + public function handle(array $input): void { - if (!(new $input['type']['cls']($input['type']['params']))->works()) { - throw ValidationException::withMessages(['erorr' => 'Verbindung fehlgeschlagen.']); + if (!app($input['type']['cls'])->setParams($input['type']['params'])->works()) { + throw ValidationException::withMessages(['connection' => 'Verbindung fehlgeschlagen.']); } Mailgateway::create($input); } + /** + * @return array + */ public function rules(): array { return [ 'name' => 'required|string|max:255', 'domain' => 'required|string|max:255', - 'type.cls' => ['required', 'string', 'max:255', Rule::in(app('mail-gateways'))], + ...$this->typeValidation(), 'type.params' => 'present|array', ...collect(request()->input('type.cls')::rules('storeValidator'))->mapWithKeys(fn ($rules, $key) => ["type.params.{$key}" => $rules]), ]; } + /** + * @return array + */ + public function getValidationAttributes(): array + { + return [ + 'type.cls' => 'Typ', + 'name' => 'Beschreibung', + 'domain' => 'Domain', + ...collect(request()->input('type.cls')::fieldNames())->mapWithKeys(fn ($attribute, $key) => ["type.params.{$key}" => $attribute]), + ]; + } + + /** + * @return array + */ + private function typeValidation(): array + { + return [ + 'type.cls' => ['required', 'string', 'max:255', Rule::in(app('mail-gateways'))], + ]; + } + + public function prepareForValidation(ActionRequest $request): void + { + if (!is_subclass_of(request()->input('type.cls'), Type::class)) { + throw ValidationException::withMessages(['type.cls' => 'Typ ist nicht valide.']); + } + } + public function asController(ActionRequest $request): void { $this->handle($request->validated()); diff --git a/app/Mailgateway/Resources/MailgatewayResource.php b/app/Mailgateway/Resources/MailgatewayResource.php index 18fe2545..9d22e10f 100644 --- a/app/Mailgateway/Resources/MailgatewayResource.php +++ b/app/Mailgateway/Resources/MailgatewayResource.php @@ -40,7 +40,7 @@ class MailgatewayResource extends JsonResource 'types' => app('mail-gateways')->map(fn ($gateway) => [ 'id' => $gateway, 'name' => $gateway::name(), - 'fields' => $gateway::fields(), + 'fields' => $gateway::presentFields('storeValidator'), 'defaults' => (object) $gateway::defaults(), ])->prepend([ 'id' => null, diff --git a/app/Mailgateway/Types/LocalType.php b/app/Mailgateway/Types/LocalType.php index d2497b43..6e879254 100644 --- a/app/Mailgateway/Types/LocalType.php +++ b/app/Mailgateway/Types/LocalType.php @@ -18,4 +18,9 @@ class LocalType extends Type { return []; } + + public function setParams(array $params): static + { + return $this; + } } diff --git a/app/Mailgateway/Types/MailmanType.php b/app/Mailgateway/Types/MailmanType.php index f5174ab2..bc5bb35f 100644 --- a/app/Mailgateway/Types/MailmanType.php +++ b/app/Mailgateway/Types/MailmanType.php @@ -10,11 +10,13 @@ class MailmanType extends Type public string $user; public string $password; - public function __construct($params) + public function setParams(array $params): static { $this->url = data_get($params, 'url'); $this->user = data_get($params, 'user'); $this->password = data_get($params, 'password'); + + return $this; } public static function name(): string @@ -27,6 +29,9 @@ class MailmanType extends Type return app(MailmanService::class)->setCredentials($this->url, $this->user, $this->password)->check(); } + /** + * {@inheritdoc} + */ public static function fields(): array { return [ diff --git a/app/Mailgateway/Types/Type.php b/app/Mailgateway/Types/Type.php index 7052a37f..a647167b 100644 --- a/app/Mailgateway/Types/Type.php +++ b/app/Mailgateway/Types/Type.php @@ -6,10 +6,18 @@ abstract class Type { abstract public static function name(): string; + /** + * @return array + */ abstract public static function fields(): array; abstract public function works(): bool; + /** + * @param array $params + */ + abstract public function setParams(array $params): static; + public static function defaults(): array { return collect(static::fields())->mapWithKeys(fn ($field) => [ @@ -17,6 +25,14 @@ abstract class Type ])->toArray(); } + public static function presentFields(string $validator): array + { + return array_map(fn ($field) => [ + ...$field, + 'is_required' => str_contains($field[$validator], 'required'), + ], static::fields()); + } + public static function rules(string $validator): array { return collect(static::fields())->mapWithKeys(fn ($field) => [ @@ -31,4 +47,12 @@ abstract class Type 'params' => get_object_vars($this), ]; } + + /** + * @return array + */ + public static function fieldNames(): array + { + return collect(static::fields())->mapWithKeys(fn ($field) => [$field['name'] => $field['label']])->toArray(); + } } diff --git a/phpstan.neon b/phpstan.neon index d7c61fce..a30b1e9d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -18,6 +18,7 @@ parameters: ContributionMemberData: 'array' ContributionRequestArray: 'array{dateFrom: string, dateUntil: string, zipLocation: string, country: int, eventName: string, members: array}' ContributionApiRequestArray: 'array{dateFrom: string, dateUntil: string, zipLocation: string, country: int, eventName: string, member_data: array}' + MailgatewayCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string}' ignoreErrors: - diff --git a/resources/css/buttons.css b/resources/css/buttons.css index 96012a52..f886a009 100644 --- a/resources/css/buttons.css +++ b/resources/css/buttons.css @@ -33,6 +33,9 @@ } &.btn-danger { @apply bg-red-400 text-red-100 hover:bg-red-300; + &:not(.disabled):hover { + @apply bg-red-500 text-red-100; + } } &.label { diff --git a/resources/js/components/ui/Button.vue b/resources/js/components/ui/Button.vue index 3ee5ef5f..9af8c165 100644 --- a/resources/js/components/ui/Button.vue +++ b/resources/js/components/ui/Button.vue @@ -3,7 +3,7 @@
- Weiter + diff --git a/resources/js/views/mailgateway/Index.vue b/resources/js/views/mailgateway/Index.vue index 2ac5c1e0..ef6ac797 100644 --- a/resources/js/views/mailgateway/Index.vue +++ b/resources/js/views/mailgateway/Index.vue @@ -4,8 +4,8 @@ Neue Verbindung -
-
+
+
-
- -
+ +
+ Speichern + Abbrechen +
+
diff --git a/tests/Feature/Mailgateway/IndexTest.php b/tests/Feature/Mailgateway/IndexTest.php index 2eb531bc..f486f7f1 100644 --- a/tests/Feature/Mailgateway/IndexTest.php +++ b/tests/Feature/Mailgateway/IndexTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Mailgateway; use App\Mailgateway\Models\Mailgateway; use App\Mailgateway\Types\LocalType; +use App\Mailgateway\Types\MailmanType; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -53,6 +54,15 @@ class IndexTest extends TestCase 'id' => LocalType::class, 'name' => 'Lokal', ], $response, 'data.meta.types.1'); + $this->assertInertiaHas([ + 'id' => MailmanType::class, + 'fields' => [ + [ + 'name' => 'url', + 'is_required' => true, + ], + ], + ], $response, 'data.meta.types.2'); $this->assertInertiaHas([ 'domain' => '', 'name' => '', diff --git a/tests/Feature/Mailgateway/StoreTest.php b/tests/Feature/Mailgateway/StoreTest.php index 3cda9c82..0e293841 100644 --- a/tests/Feature/Mailgateway/StoreTest.php +++ b/tests/Feature/Mailgateway/StoreTest.php @@ -3,8 +3,11 @@ namespace Tests\Feature\Mailgateway; use App\Mailgateway\Types\LocalType; +use App\Mailgateway\Types\MailmanType; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Phake; use Tests\RequestFactories\MailgatewayRequestFactory; +use Tests\RequestFactories\MailmanTypeRequest; use Tests\TestCase; class StoreTest extends TestCase @@ -20,7 +23,7 @@ class StoreTest extends TestCase public function testItCanStoreALocalGateway(): void { - $response = $this->post('/api/mailgateway', MailgatewayRequestFactory::new()->name('lala')->type(LocalType::class, [])->domain('example.com')->create()); + $response = $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->name('lala')->type(LocalType::class, [])->domain('example.com')->create()); $response->assertOk(); @@ -33,4 +36,56 @@ class StoreTest extends TestCase ]), ]); } + + public function testItCanStoreAMailmanGateway(): void + { + $typeParams = ['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret']; + $this->stubIo(MailmanType::class, function ($mock) use ($typeParams) { + Phake::when($mock)->setParams($typeParams)->thenReturn($mock); + Phake::when($mock)->works()->thenReturn(true); + }); + $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create()); + + $this->assertDatabaseHas('mailgateways', [ + 'type' => json_encode([ + 'cls' => MailmanType::class, + 'params' => $typeParams, + ]), + ]); + } + + public function testItThrowsErrorWhenMailmanConnectionFailed(): void + { + $typeParams = ['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret']; + $this->stubIo(MailmanType::class, function ($mock) use ($typeParams) { + Phake::when($mock)->setParams($typeParams)->thenReturn($mock); + Phake::when($mock)->works()->thenReturn(false); + }); + $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create()) + ->assertJsonValidationErrors('connection'); + } + + public function testItValidatesCustomFields(): void + { + $typeParams = ['url' => 'https://example.com', 'user' => '', 'password' => 'secret']; + $this->stubIo(MailmanType::class, function ($mock) use ($typeParams) { + Phake::when($mock)->setParams($typeParams)->thenReturn($mock); + Phake::when($mock)->works()->thenReturn(false); + }); + $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create()) + ->assertJsonValidationErrors(['type.params.user' => 'Benutzer ist erforderlich.']); + } + + public function testItValidatesType(): void + { + $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->missingType()->create()) + ->assertJsonValidationErrors('type.cls'); + } + + public function testItValidatesNameAndDomain(): void + { + $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->withoutName()->withoutDomain()->create()) + ->assertJsonValidationErrors('domain') + ->assertJsonValidationErrors('name'); + } } diff --git a/tests/RequestFactories/MailgatewayRequestFactory.php b/tests/RequestFactories/MailgatewayRequestFactory.php index 53cdf19b..26ae0de2 100644 --- a/tests/RequestFactories/MailgatewayRequestFactory.php +++ b/tests/RequestFactories/MailgatewayRequestFactory.php @@ -2,6 +2,7 @@ namespace Tests\RequestFactories; +use App\Mailgateway\Types\Type; use Worksome\RequestFactories\RequestFactory; class MailgatewayRequestFactory extends RequestFactory @@ -39,4 +40,22 @@ class MailgatewayRequestFactory extends RequestFactory 'params' => $params, ]]); } + + public function missingType(): self + { + return $this->state(['type' => [ + 'cls' => null, + 'params' => [], + ]]); + } + + public function withoutName(): self + { + return $this->state(['name' => '']); + } + + public function withoutDomain(): self + { + return $this->state(['domain' => '']); + } }