Add validation for contribution generator
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
1ba60a455f
commit
6960b0295a
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contribution\Actions;
|
||||
|
||||
use App\Contribution\ContributionFactory;
|
||||
use App\Rules\JsonBase64Rule;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\Tex\Compiler;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class GenerateAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param class-string<DvDocument> $document
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function handle(string $document, array $payload): Compiler
|
||||
{
|
||||
return Tex::compile($document::fromRequest($payload));
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): Compiler
|
||||
{
|
||||
$payload = $this->payload($request);
|
||||
$type = data_get($payload, 'type');
|
||||
ValidateAction::validateType($type);
|
||||
Validator::make($payload, app(ContributionFactory::class)->rules($type))->validate();
|
||||
|
||||
return $this->handle($type, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<stirng, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'payload' => [new JsonBase64Rule()],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function payload(ActionRequest $request): array
|
||||
{
|
||||
return json_decode(base64_decode($request->input('payload', '')), true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contribution\Actions;
|
||||
|
||||
use App\Contribution\ContributionFactory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ValidateAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function asController(): JsonResponse
|
||||
{
|
||||
return response()->json(['valid' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return app(ContributionFactory::class)->rules(request()->type);
|
||||
}
|
||||
|
||||
public function prepareForValidation(ActionRequest $request): void
|
||||
{
|
||||
static::validateType($request->input('type'));
|
||||
}
|
||||
|
||||
public static function validateType(?string $type = null): void
|
||||
{
|
||||
Validator::make(['type' => $type], app(ContributionFactory::class)->typeRule())->validate();
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contribution;
|
||||
|
||||
use App\Contribution\Documents\SolingenDocument;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Zoomyboy\Tex\BaseCompiler;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class ContributionController extends Controller
|
||||
{
|
||||
public function generate(Request $request): BaseCompiler
|
||||
{
|
||||
$payload = json_decode(base64_decode($request->input('payload', '')), true);
|
||||
|
||||
$validated = Validator::make($payload, [
|
||||
'dateFrom' => 'required|date|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|date|date_format:Y-m-d',
|
||||
'country' => 'required|exists:countries,id',
|
||||
'eventName' => 'required|max:100',
|
||||
'members' => 'array',
|
||||
'members.*' => 'integer|exists:members,id',
|
||||
'type' => 'required|string',
|
||||
'zipLocation' => 'required|string',
|
||||
])->validate();
|
||||
|
||||
/** @var class-string<SolingenDocument> */
|
||||
$type = $validated['type'];
|
||||
|
||||
return Tex::compile($type::fromRequest($validated));
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ use App\Contribution\Documents\ContributionDocument;
|
|||
use App\Contribution\Documents\DvDocument;
|
||||
use App\Contribution\Documents\RemscheidDocument;
|
||||
use App\Contribution\Documents\SolingenDocument;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ContributionFactory
|
||||
{
|
||||
|
@ -19,13 +21,36 @@ class ContributionFactory
|
|||
];
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, name: string}>
|
||||
* @return Collection<int, array{title: mixed, class: mixed}>
|
||||
*/
|
||||
public function compilerSelect(): array
|
||||
public function compilerSelect(): Collection
|
||||
{
|
||||
return collect($this->documents)->map(fn ($document) => [
|
||||
'title' => $document::getName(),
|
||||
'class' => $document,
|
||||
])->toArray();
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function typeRule(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', Rule::in($this->documents)],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<ContributionDocument> $type
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(string $type): array
|
||||
{
|
||||
return [
|
||||
...$type::globalRules(),
|
||||
...$type::rules(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,20 @@ use Zoomyboy\Tex\Document;
|
|||
abstract class ContributionDocument extends Document
|
||||
{
|
||||
abstract public static function getName(): string;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract public static function rules(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function globalRules(): array
|
||||
{
|
||||
return [
|
||||
'members' => 'present|array|min:1',
|
||||
'members.*' => 'integer|exists:members,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,4 +111,18 @@ class DvDocument extends ContributionDocument
|
|||
{
|
||||
return 'Für DV erstellen';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'country' => 'required|integer|exists:countries,id',
|
||||
'zipLocation' => 'required|string',
|
||||
'eventName' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,4 +85,17 @@ class RemscheidDocument extends ContributionDocument
|
|||
{
|
||||
return 'Für Remscheid erstellen';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'zipLocation' => 'required|string',
|
||||
'country' => 'required|integer|exists:countries,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,4 +95,17 @@ class SolingenDocument extends ContributionDocument
|
|||
{
|
||||
return 'Für Stadt Solingen erstellen';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'zipLocation' => 'required|string',
|
||||
'eventName' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class JsonBase64Rule implements Rule
|
||||
{
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === base64_decode($value, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (base64_encode(base64_decode($value, true)) !== $value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_array(json_decode(base64_decode($value), true))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return 'The validation error message.';
|
||||
}
|
||||
}
|
|
@ -88,9 +88,14 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
var payload = btoa(JSON.stringify(this.values));
|
||||
window.open(`/contribution/generate?payload=${payload}`);
|
||||
async submit() {
|
||||
try {
|
||||
await this.axios.post('/contribution-validate', this.values);
|
||||
var payload = btoa(JSON.stringify(this.values));
|
||||
window.open(`/contribution-generate?payload=${payload}`);
|
||||
} catch (e) {
|
||||
this.errorsFromException(e);
|
||||
}
|
||||
},
|
||||
onSubmitMemberResult(selected) {
|
||||
if (this.values.members.find((m) => m === selected.id) !== undefined) {
|
||||
|
|
|
@ -10,7 +10,8 @@ use App\Activity\Api\SubactivityShowAction;
|
|||
use App\Activity\Api\SubactivityStoreAction;
|
||||
use App\Activity\Api\SubactivityUpdateAction;
|
||||
use App\Contribution\Actions\FormAction as ContributionFormAction;
|
||||
use App\Contribution\ContributionController;
|
||||
use App\Contribution\Actions\GenerateAction as ContributionGenerateAction;
|
||||
use App\Contribution\Actions\ValidateAction as ContributionValidateAction;
|
||||
use App\Course\Controllers\CourseController;
|
||||
use App\Efz\ShowEfzDocumentAction;
|
||||
use App\Home\Actions\IndexAction as HomeIndexAction;
|
||||
|
@ -55,8 +56,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::resource('member.course', CourseController::class);
|
||||
Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz');
|
||||
Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync');
|
||||
Route::get('/contribution', ContributionFormAction::class)->name('contribution.form');
|
||||
Route::get('/contribution/generate', [ContributionController::class, 'generate'])->name('contribution.generate');
|
||||
Route::get('/activity', ActivityIndexAction::class)->name('activity.index');
|
||||
Route::get('/activity/{activity}/edit', ActivityEditAction::class)->name('activity.edit');
|
||||
Route::get('/activity/create', ActivityCreateAction::class)->name('activity.create');
|
||||
|
@ -66,4 +65,9 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::post('/subactivity', SubactivityStoreAction::class)->name('api.subactivity.store');
|
||||
Route::patch('/subactivity/{subactivity}', SubactivityUpdateAction::class)->name('api.subactivity.update');
|
||||
Route::get('/subactivity/{subactivity}', SubactivityShowAction::class)->name('api.subactivity.show');
|
||||
|
||||
// ------------------------------- Contributions -------------------------------
|
||||
Route::get('/contribution', ContributionFormAction::class)->name('contribution.form');
|
||||
Route::get('/contribution-generate', ContributionGenerateAction::class)->name('contribution.generate');
|
||||
Route::post('/contribution-validate', ContributionValidateAction::class)->name('contribution.validate');
|
||||
});
|
||||
|
|
|
@ -2,10 +2,14 @@
|
|||
|
||||
namespace Tests\Feature\Contribution;
|
||||
|
||||
use App\Contribution\Documents\ContributionDocument;
|
||||
use App\Contribution\Documents\DvDocument;
|
||||
use App\Contribution\Documents\SolingenDocument;
|
||||
use App\Country;
|
||||
use App\Member\Member;
|
||||
use Generator;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\RequestFactories\ContributionRequestFactory;
|
||||
use Tests\TestCase;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
|
@ -29,7 +33,7 @@ class StoreTest extends TestCase
|
|||
$member1 = Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Max', 'lastname' => 'Muster']);
|
||||
$member2 = Member::factory()->defaults()->create(['address' => 'Maxstr 44', 'zip' => '42719', 'firstname' => 'Jane', 'lastname' => 'Muster']);
|
||||
|
||||
$response = $this->call('GET', '/contribution/generate', [
|
||||
$response = $this->call('GET', '/contribution-generate', [
|
||||
'payload' => base64_encode(json_encode([
|
||||
'country' => $country->id,
|
||||
'dateFrom' => '1991-06-15',
|
||||
|
@ -46,24 +50,115 @@ class StoreTest extends TestCase
|
|||
Tex::assertCompiled($type, fn ($document) => $document->hasAllContent($bodyChecks));
|
||||
}
|
||||
|
||||
public function testItValidatesInput(): void
|
||||
/**
|
||||
* @testWith [""]
|
||||
* ["aaaa"]
|
||||
* ["YWFhCg=="]
|
||||
*/
|
||||
public function testInputShouldBeBase64EncodedJson(string $payload): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
$country = Country::factory()->create();
|
||||
$member1 = Member::factory()->defaults()->create();
|
||||
|
||||
$response = $this->call('GET', '/contribution/generate', [
|
||||
'payload' => base64_encode(json_encode([
|
||||
'country' => $country->id,
|
||||
'dateFrom' => '',
|
||||
'dateUntil' => '1991-06-16',
|
||||
'eventName' => 'Super tolles Lager',
|
||||
'members' => [$member1->id],
|
||||
'type' => SolingenDocument::class,
|
||||
'zipLocation' => '42777 SG',
|
||||
])),
|
||||
]);
|
||||
$this->call('GET', '/contribution-generate', ['payload' => $payload])->assertSessionHasErrors('payload');
|
||||
}
|
||||
|
||||
$response->assertSessionHasErrors('dateFrom');
|
||||
/**
|
||||
* @param array<string, string> $input
|
||||
* @param class-string<ContributionDocument> $documentClass
|
||||
* @dataProvider validationDataProvider
|
||||
*/
|
||||
public function testItValidatesInput(array $input, string $documentClass, string $errorField): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
Country::factory()->create();
|
||||
Member::factory()->defaults()->create();
|
||||
|
||||
$this->postJson('/contribution-validate', ContributionRequestFactory::new()->type($documentClass)->state($input)->create())
|
||||
->assertJsonValidationErrors($errorField);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $input
|
||||
* @param class-string<ContributionDocument> $documentClass
|
||||
* @dataProvider validationDataProvider
|
||||
*/
|
||||
public function testItValidatesInputBeforeGeneration(array $input, string $documentClass, string $errorField): void
|
||||
{
|
||||
$this->login()->loginNami();
|
||||
Country::factory()->create();
|
||||
Member::factory()->defaults()->create();
|
||||
|
||||
$this->call('GET', '/contribution-generate', [
|
||||
'payload' => ContributionRequestFactory::new()->type($documentClass)->state($input)->toBase64(),
|
||||
])->assertSessionHasErrors($errorField);
|
||||
}
|
||||
|
||||
protected function validationDataProvider(): Generator
|
||||
{
|
||||
yield [
|
||||
['type' => 'aaa'],
|
||||
SolingenDocument::class,
|
||||
'type',
|
||||
];
|
||||
yield [
|
||||
['type' => ''],
|
||||
SolingenDocument::class,
|
||||
'type',
|
||||
];
|
||||
yield [
|
||||
['dateFrom' => ''],
|
||||
SolingenDocument::class,
|
||||
'dateFrom',
|
||||
];
|
||||
yield [
|
||||
['dateFrom' => '2022-01'],
|
||||
SolingenDocument::class,
|
||||
'dateFrom',
|
||||
];
|
||||
yield [
|
||||
['dateUntil' => ''],
|
||||
SolingenDocument::class,
|
||||
'dateUntil',
|
||||
];
|
||||
yield [
|
||||
['dateUntil' => '2022-01'],
|
||||
SolingenDocument::class,
|
||||
'dateUntil',
|
||||
];
|
||||
yield [
|
||||
['country' => -1],
|
||||
DvDocument::class,
|
||||
'country',
|
||||
];
|
||||
yield [
|
||||
['country' => 'AAAA'],
|
||||
DvDocument::class,
|
||||
'country',
|
||||
];
|
||||
yield [
|
||||
['members' => 'A'],
|
||||
DvDocument::class,
|
||||
'members',
|
||||
];
|
||||
yield [
|
||||
['members' => [99999]],
|
||||
DvDocument::class,
|
||||
'members.0',
|
||||
];
|
||||
yield [
|
||||
['members' => ['lalala']],
|
||||
DvDocument::class,
|
||||
'members.0',
|
||||
];
|
||||
yield [
|
||||
['eventName' => ''],
|
||||
SolingenDocument::class,
|
||||
'eventName',
|
||||
];
|
||||
yield [
|
||||
['zipLocation' => ''],
|
||||
SolingenDocument::class,
|
||||
'zipLocation',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\RequestFactories;
|
||||
|
||||
use App\Contribution\ContributionFactory;
|
||||
use App\Contribution\Documents\ContributionDocument;
|
||||
use App\Country;
|
||||
use App\Member\Member;
|
||||
use Worksome\RequestFactories\RequestFactory;
|
||||
|
||||
class ContributionRequestFactory extends RequestFactory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
$compilers = collect(app(ContributionFactory::class)->compilerSelect())->pluck('class');
|
||||
|
||||
return [
|
||||
'country' => $this->faker->randomElement(Country::get())->id,
|
||||
'dateFrom' => $this->faker->date(),
|
||||
'dateUntil' => $this->faker->date(),
|
||||
'eventName' => $this->faker->words(3, true),
|
||||
'members' => [$this->faker->randomElement(Member::get())->id],
|
||||
'type' => $this->faker->randomElement($compilers),
|
||||
'zipLocation' => $this->faker->city,
|
||||
];
|
||||
}
|
||||
|
||||
public function toBase64(): string
|
||||
{
|
||||
return base64_encode(json_encode($this->create()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<ContributionDocument> $type
|
||||
*/
|
||||
public function type(string $type): self
|
||||
{
|
||||
return $this->state(['type' => $type]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue