From 5b6c5539f6a60d1e9e39d45f3aeec515074875cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Mon, 16 Dec 2024 01:08:35 +0100 Subject: [PATCH 1/9] Training mails applications is an array indexed by the application id, not just a list The list incorrectly introduced in cfa6cab072a8b605a030f4dc2ad9292b35ad0cd7 (#179) Discovered by trying to get to PHPStan Level 10 (and by writing tests). --- app/src/Admin/Presenters/EmailsPresenter.php | 2 +- app/src/Form/TrainingMailsOutboxFormFactory.php | 2 +- app/src/Training/Mails/TrainingMails.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/Admin/Presenters/EmailsPresenter.php b/app/src/Admin/Presenters/EmailsPresenter.php index 1a3e111c8..625fb50b5 100644 --- a/app/src/Admin/Presenters/EmailsPresenter.php +++ b/app/src/Admin/Presenters/EmailsPresenter.php @@ -11,7 +11,7 @@ class EmailsPresenter extends BasePresenter { - /** @var list */ + /** @var array */ private array $applications = []; diff --git a/app/src/Form/TrainingMailsOutboxFormFactory.php b/app/src/Form/TrainingMailsOutboxFormFactory.php index dd97a25db..374227fa8 100644 --- a/app/src/Form/TrainingMailsOutboxFormFactory.php +++ b/app/src/Form/TrainingMailsOutboxFormFactory.php @@ -31,7 +31,7 @@ public function __construct( /** * @param callable(int): void $onSuccess - * @param list $applications + * @param array $applications */ public function create(callable $onSuccess, array $applications): UiForm { diff --git a/app/src/Training/Mails/TrainingMails.php b/app/src/Training/Mails/TrainingMails.php index 13f5cd11c..4d3840b84 100644 --- a/app/src/Training/Mails/TrainingMails.php +++ b/app/src/Training/Mails/TrainingMails.php @@ -79,7 +79,7 @@ public function sendSignUpMail( /** - * @return list + * @return array id => application * @throws TrainingDateDoesNotExistException * @throws HaliteAlert * @throws SodiumException @@ -150,7 +150,7 @@ public function getApplications(): array } } - return array_values($applications); + return $applications; } From 35cc6d724ab1e4a18dbcae2217d4e6b9c10de18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Sun, 15 Dec 2024 04:41:25 +0100 Subject: [PATCH 2/9] Asserts for PHPStan Level 10 for forms --- app/config/tests.neon | 2 +- app/psalm-baseline.xml | 74 ------ app/src/Articles/Blog/BlogPosts.php | 5 +- app/src/Form/ChangePasswordFormFactory.php | 2 + app/src/Form/PostFormFactory.php | 54 ++-- ...TrainingApplicationStatusesFormFactory.php | 4 + app/src/Form/TrainingDateFormFactory.php | 17 ++ app/src/Form/TrainingInvoiceFormFactory.php | 2 + .../Form/TrainingMailsOutboxFormFactory.php | 15 +- .../Application/LocaleLinkGeneratorMock.php | 27 +- app/src/Test/Database/Database.php | 11 +- app/src/Test/Database/ResultSet.php | 11 +- .../TrainingApplicationStatuses.php | 4 +- .../Form/ChangePasswordFormFactoryTest.phpt | 80 ++++++ app/tests/Form/PostFormFactoryTest.phpt | 231 ++++++++++++++++++ ...ApplicationPreliminaryFormFactoryTest.phpt | 94 +++++++ ...ingApplicationStatusesFormFactoryTest.phpt | 225 +++++++++++++++++ .../Form/TrainingDateFormFactoryTest.phpt | 210 ++++++++++++++++ .../Form/TrainingInvoiceFormFactoryTest.phpt | 56 +++++ .../TrainingMailsOutboxFormFactoryTest.phpt | 164 +++++++++++++ 20 files changed, 1172 insertions(+), 116 deletions(-) create mode 100644 app/tests/Form/ChangePasswordFormFactoryTest.phpt create mode 100644 app/tests/Form/PostFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingApplicationPreliminaryFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingDateFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingInvoiceFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt diff --git a/app/config/tests.neon b/app/config/tests.neon index 8f97b32fc..dd5fa301a 100644 --- a/app/config/tests.neon +++ b/app/config/tests.neon @@ -37,7 +37,7 @@ parameters: services: - MichalSpacekCz\Test\Application\ApplicationPresenter articles: MichalSpacekCz\Test\Articles\ArticlesMock - localeLinkGenerator: MichalSpacekCz\Test\Application\LocaleLinkGeneratorMock + localeLinkGenerator: MichalSpacekCz\Test\Application\LocaleLinkGeneratorMock(languages: %locales.languages%) database.default.explorer: MichalSpacekCz\Test\Database\Database database.upcKeys.explorer: @database.default.explorer database.pulse.explorer: @database.default.explorer diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 0b801c727..08cd6cc7a 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -5,28 +5,6 @@ getValue()]]> - - - newPassword]]> - password]]> - - - - - lead === '' ? null : $values->lead]]> - locale]]> - locale]]> - ogImage === '' ? null : $values->ogImage]]> - originally === '' ? null : $values->originally]]> - previewKey === '' ? null : $values->previewKey]]> - published]]> - recommended]]> - tags]]> - text]]> - translationGroup === '' ? null : $values->translationGroup]]> - editSummary) ? null : $values->editSummary]]> - - algo->new->algo]]> @@ -81,58 +59,6 @@ date]]> - - - cooperation]]> - cooperation]]> - end]]> - end]]> - feedbackHref]]> - feedbackHref]]> - label]]> - label]]> - note]]> - note]]> - public]]> - public]]> - remote]]> - remote]]> - remoteNotes]]> - remoteNotes]]> - remoteUrl]]> - remoteUrl]]> - start]]> - start]]> - status]]> - status]]> - training]]> - training]]> - training]]> - venue]]> - venue]]> - videoHref]]> - videoHref]]> - - - - - invoice]]> - paid]]> - - - - - additional]]> - cc ?: null]]> - feedbackRequest ?? false]]> - invoice]]> - invoiceId]]> - - - - - - diff --git a/app/src/Articles/Blog/BlogPosts.php b/app/src/Articles/Blog/BlogPosts.php index f88e9096c..3eb547617 100644 --- a/app/src/Articles/Blog/BlogPosts.php +++ b/app/src/Articles/Blog/BlogPosts.php @@ -3,11 +3,11 @@ namespace MichalSpacekCz\Articles\Blog; -use DateTime; use Exception; use MichalSpacekCz\Articles\Blog\Exceptions\BlogPostDoesNotExistException; use MichalSpacekCz\Articles\Blog\Exceptions\BlogPostWithoutIdException; use MichalSpacekCz\Database\TypedDatabase; +use MichalSpacekCz\DateTime\DateTimeFactory; use MichalSpacekCz\DateTime\Exceptions\InvalidTimezoneException; use MichalSpacekCz\Tags\Tags; use MichalSpacekCz\Utils\Exceptions\JsonItemNotStringException; @@ -30,6 +30,7 @@ public function __construct( private BlogPostFactory $factory, private Cache $exportsCache, private Tags $tags, + private DateTimeFactory $dateTimeFactory, private int $updatedInfoThreshold, ) { } @@ -229,7 +230,7 @@ public function update(BlogPost $post, ?string $editSummary, array $previousSlug ], $postId, ); - $editedAt = new DateTime(); + $editedAt = $this->dateTimeFactory->create(); if ($editSummary !== null) { $timeZone = $editedAt->getTimezone()->getName(); $this->database->query( diff --git a/app/src/Form/ChangePasswordFormFactory.php b/app/src/Form/ChangePasswordFormFactory.php index 57fb35590..4d2be9241 100644 --- a/app/src/Form/ChangePasswordFormFactory.php +++ b/app/src/Form/ChangePasswordFormFactory.php @@ -46,6 +46,8 @@ public function create(callable $onSuccess): UiForm $form->onSuccess[] = function (UiForm $form) use ($onSuccess): void { $values = $form->getFormValues(); + assert(is_string($values->password)); + assert(is_string($values->newPassword)); $this->authenticator->changePassword($this->user, $values->password, $values->newPassword); $onSuccess(); }; diff --git a/app/src/Form/PostFormFactory.php b/app/src/Form/PostFormFactory.php index cbb90533e..0747a0349 100644 --- a/app/src/Form/PostFormFactory.php +++ b/app/src/Form/PostFormFactory.php @@ -14,7 +14,6 @@ use MichalSpacekCz\DateTime\Exceptions\InvalidTimezoneException; use MichalSpacekCz\Form\Controls\TrainingControlsFactory; use MichalSpacekCz\Formatter\TexyFormatter; -use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Tags\Tags; use MichalSpacekCz\Twitter\Exceptions\TwitterCardNotFoundException; use MichalSpacekCz\Twitter\TwitterCards; @@ -153,16 +152,15 @@ function () use ($form, $post): BlogPost { $newPost = $this->buildPost($values, $post?->getId()); try { if ($post) { - $this->blogPosts->update($newPost, empty($values->editSummary) ? null : $values->editSummary, $post->getSlugTags()); + assert(is_string($values->editSummary)); + $this->blogPosts->update($newPost, $values->editSummary === '' ? null : $values->editSummary, $post->getSlugTags()); $onSuccessEdit($newPost); } else { $onSuccessAdd($this->blogPosts->add($newPost)); } } catch (UniqueConstraintViolationException) { $slug = $form->getComponent('slug'); - if (!$slug instanceof TextInput) { - throw new ShouldNotHappenException(sprintf("The 'slug' component should be '%s' but it's a %s", TextInput::class, get_debug_type($slug))); - } + assert($slug instanceof TextInput); $slug->addError($this->texyFormatter->translate('messages.blog.admin.duplicateslug')); } }; @@ -181,33 +179,33 @@ function () use ($form, $post): BlogPost { */ private function buildPost(stdClass $values, ?int $postId): BlogPost { - $title = $values->title; - if (!is_string($title)) { - throw new ShouldNotHappenException("Title should be a string, but it's a " . get_debug_type($title)); - } - $slug = $values->slug; - if (!is_string($slug)) { - throw new ShouldNotHappenException("Slug should be a string, but it's a " . get_debug_type($slug)); - } - $twitterCard = $values->twitterCard; - if (!is_string($twitterCard)) { - throw new ShouldNotHappenException("Twitter card should be a string, but it's a " . get_debug_type($twitterCard)); - } + assert(is_int($values->translationGroup) || $values->translationGroup === null); + assert(is_string($values->title)); + assert(is_string($values->slug)); + assert(is_int($values->locale)); + assert(is_string($values->lead)); + assert(is_string($values->text)); + assert(is_string($values->published)); + assert(is_string($values->previewKey)); + assert(is_string($values->originally)); + assert(is_string($values->ogImage)); + assert(is_string($values->tags)); + assert(is_string($values->recommended)); + assert(is_string($values->twitterCard)); + assert(is_array($values->cspSnippets) && array_is_list($values->cspSnippets)); + assert(is_array($values->allowedTags) && array_is_list($values->allowedTags)); + assert(is_bool($values->omitExports)); + /** @var list $cspSnippets */ $cspSnippets = $values->cspSnippets; - if (!is_array($cspSnippets) || !array_is_list($cspSnippets)) { - throw new ShouldNotHappenException("CSP snippets should be a list, but it's a " . get_debug_type($cspSnippets)); - } + /** @var list $allowedTagsGroups */ $allowedTagsGroups = $values->allowedTags; - if (!is_array($allowedTagsGroups) || !array_is_list($allowedTagsGroups)) { - throw new ShouldNotHappenException("Allowed tags groups should be a list, but it's a " . get_debug_type($allowedTagsGroups)); - } return $this->blogPostFactory->create( $postId, - $slug === '' ? Strings::webalize($title) : $slug, + $values->slug === '' ? Strings::webalize($values->title) : $values->slug, $values->locale, $this->locales->getLocaleById($values->locale), - $values->translationGroup === '' ? null : $values->translationGroup, - $title, + $values->translationGroup, + $values->title, $values->lead === '' ? null : $values->lead, $values->text, $values->published === '' ? null : new DateTime($values->published), @@ -217,10 +215,10 @@ private function buildPost(stdClass $values, ?int $postId): BlogPost $values->tags === '' ? [] : $this->tags->toArray($values->tags), $values->tags === '' ? [] : $this->tags->toSlugArray($values->tags), $values->recommended ? $this->recommendedLinks->getFromJson($values->recommended) : [], - $twitterCard === '' ? null : $this->twitterCards->getCard($twitterCard), + $values->twitterCard === '' ? null : $this->twitterCards->getCard($values->twitterCard), $cspSnippets, $allowedTagsGroups, - (bool)$values->omitExports, + $values->omitExports, ); } diff --git a/app/src/Form/TrainingApplicationStatusesFormFactory.php b/app/src/Form/TrainingApplicationStatusesFormFactory.php index a644a0871..752174582 100644 --- a/app/src/Form/TrainingApplicationStatusesFormFactory.php +++ b/app/src/Form/TrainingApplicationStatusesFormFactory.php @@ -7,6 +7,7 @@ use MichalSpacekCz\Training\Applications\TrainingApplication; use MichalSpacekCz\Training\Applications\TrainingApplications; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; +use Nette\Utils\ArrayHash; use Nette\Utils\Html; readonly class TrainingApplicationStatusesFormFactory @@ -48,7 +49,10 @@ public function create(callable $onSuccess, array $applications): UiForm $submitStatuses->onClick[] = function () use ($form, $onSuccess): void { $values = $form->getFormValues(); + assert($values->applications instanceof ArrayHash); + assert(is_string($values->date)); foreach ($values->applications as $id => $status) { + assert(is_string($status)); if ($status) { $this->trainingApplicationStatuses->updateStatus($id, $status, $values->date); } diff --git a/app/src/Form/TrainingDateFormFactory.php b/app/src/Form/TrainingDateFormFactory.php index 68a17636c..4a1e0f818 100644 --- a/app/src/Form/TrainingDateFormFactory.php +++ b/app/src/Form/TrainingDateFormFactory.php @@ -117,6 +117,7 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, ?Trainin ->setHtmlAttribute('title', 'Ponechte prázdné, aby se použila běžná cena'); $form->onValidate[] = function (UiForm $form) use ($price): void { $values = $form->getFormValues(); + assert(is_int($values->training)); $training = $this->trainings->getById($values->training); if ($values->price === '' && $training->getPrice() === null) { $price->addError('Běžná cena není nastavena, je třeba nastavit cenu zde'); @@ -141,6 +142,22 @@ public function create(callable $onSuccessAdd, callable $onSuccessEdit, ?Trainin $form->onSuccess[] = function (UiForm $form) use ($onSuccessAdd, $onSuccessEdit, $date): void { $values = $form->getFormValues(); + assert(is_int($values->training)); + assert(is_int($values->venue) || $values->venue === null); + assert(is_bool($values->remote)); + assert(is_string($values->start)); + assert(is_string($values->end)); + assert(is_string($values->label)); + assert(is_int($values->status)); + assert(is_bool($values->public)); + assert(is_int($values->cooperation)); + assert(is_string($values->note)); + assert(is_string($values->price)); + assert(is_string($values->studentDiscount)); + assert(is_string($values->remoteUrl)); + assert(is_string($values->remoteNotes)); + assert(is_string($values->videoHref)); + assert(is_string($values->feedbackHref)); if ($date) { $this->trainingDates->update( $date->getId(), diff --git a/app/src/Form/TrainingInvoiceFormFactory.php b/app/src/Form/TrainingInvoiceFormFactory.php index 8f7843c8e..b0ce4f99f 100644 --- a/app/src/Form/TrainingInvoiceFormFactory.php +++ b/app/src/Form/TrainingInvoiceFormFactory.php @@ -33,6 +33,8 @@ public function create(callable $onSuccess, callable $onError, array $unpaidInvo $form->addSubmit('submit', 'Zaplaceno'); $form->onSuccess[] = function (UiForm $form) use ($onSuccess, $onError): void { $values = $form->getFormValues(); + assert(is_string($values->invoice)); + assert(is_string($values->paid)); $count = $this->trainingApplications->setPaidDate($values->invoice, $values->paid); if ($count === null) { $onError(); diff --git a/app/src/Form/TrainingMailsOutboxFormFactory.php b/app/src/Form/TrainingMailsOutboxFormFactory.php index 374227fa8..119d54421 100644 --- a/app/src/Form/TrainingMailsOutboxFormFactory.php +++ b/app/src/Form/TrainingMailsOutboxFormFactory.php @@ -13,7 +13,8 @@ use Nette\Application\Application as NetteApplication; use Nette\Application\UI\Presenter; use Nette\Forms\Form; -use stdClass; +use Nette\Http\FileUpload; +use Nette\Utils\ArrayHash; readonly class TrainingMailsOutboxFormFactory { @@ -145,11 +146,11 @@ public function create(callable $onSuccess, array $applications): UiForm $form->addSubmit('submit', 'Odeslat'); $form->onSuccess[] = function (UiForm $form) use ($applications, $onSuccess): void { $values = $form->getFormValues(); + assert($values->applications instanceof ArrayHash); $sent = 0; foreach ($values->applications as $id => $data) { - if (!$data instanceof stdClass) { - throw new ShouldNotHappenException(sprintf("The presenter should be a '%s' but it's a %s", stdClass::class, get_debug_type($data))); - } + assert($data instanceof ArrayHash); + assert(is_string($data->additional)); if (empty($data->send) || !isset($applications[$id])) { continue; } @@ -171,12 +172,16 @@ public function create(callable $onSuccess, array $applications): UiForm } if ($nextStatus === TrainingApplicationStatus::MaterialsSent) { - $this->trainingMails->sendMaterials($applications[$id], $template, $data->feedbackRequest ?? false, $additional); + assert(is_bool($data->feedbackRequest)); + $this->trainingMails->sendMaterials($applications[$id], $template, $data->feedbackRequest, $additional); $this->trainingApplicationStatuses->updateStatus($id, TrainingApplicationStatus::MaterialsSent); $sent++; } if (in_array($nextStatus, [TrainingApplicationStatus::InvoiceSent, TrainingApplicationStatus::InvoiceSentAfter])) { + assert($data->invoice instanceof FileUpload); + assert(is_string($data->invoiceId)); + assert(is_string($data->cc)); if ($data->invoice->isOk()) { $this->trainingApplicationStorage->updateApplicationInvoiceData($id, $data->invoiceId); $applications[$id]->setInvoiceId((int)$data->invoiceId); diff --git a/app/src/Test/Application/LocaleLinkGeneratorMock.php b/app/src/Test/Application/LocaleLinkGeneratorMock.php index 521c16a51..751a8a343 100644 --- a/app/src/Test/Application/LocaleLinkGeneratorMock.php +++ b/app/src/Test/Application/LocaleLinkGeneratorMock.php @@ -3,12 +3,16 @@ namespace MichalSpacekCz\Test\Application; +use MichalSpacekCz\Application\Locale\LocaleLink; use MichalSpacekCz\Application\Locale\LocaleLinkGenerator; use Override; class LocaleLinkGeneratorMock extends LocaleLinkGenerator { + /** @var array */ + private array $links = []; + /** @var array */ private array $allLinks = []; @@ -16,16 +20,33 @@ class LocaleLinkGeneratorMock extends LocaleLinkGenerator private array $allLinksParams = []; - /** @noinspection PhpMissingParentConstructorInspection Intentionally */ - public function __construct() + /** + * @param array $languages + * @noinspection PhpMissingParentConstructorInspection Intentionally + */ + public function __construct( + private readonly array $languages, + ) { + } + + + /** + * @param array $links + */ + public function setLinks(array $links): void { + $this->links = $links; } #[Override] public function links(string $destination, array $params = []): array { - return []; + $links = []; + foreach ($this->links as $locale => $link) { + $links[$locale] = new LocaleLink($locale, $this->languages[$locale]['code'], $this->languages[$locale]['name'], $link); + } + return $links; } diff --git a/app/src/Test/Database/Database.php b/app/src/Test/Database/Database.php index 11d5d74cc..d46d7f0b5 100644 --- a/app/src/Test/Database/Database.php +++ b/app/src/Test/Database/Database.php @@ -56,6 +56,8 @@ class Database extends Explorer private int $fetchAllResultsPosition = 0; + private ?ResultSet $resultSet = null; + public function reset(): void { @@ -74,6 +76,7 @@ public function reset(): void $this->fetchAllDefaultResult = []; $this->fetchAllResults = []; $this->fetchAllResultsPosition = 0; + $this->resultSet = null; $this->wontThrow(); } @@ -135,7 +138,7 @@ public function query(string $sql, ...$params): ResultSet $this->queriesScalarParams[$sql][] = $this->formatValue($param); } } - return new ResultSet(); + return $this->resultSet ?? new ResultSet(); } @@ -263,6 +266,12 @@ public function addFetchAllResult(array $fetchAllResult): void } + public function setResultSet(ResultSet $resultSet): void + { + $this->resultSet = $resultSet; + } + + /** * @param list> $fetchAllResult * @return list diff --git a/app/src/Test/Database/ResultSet.php b/app/src/Test/Database/ResultSet.php index 8010503cc..8f7c16710 100644 --- a/app/src/Test/Database/ResultSet.php +++ b/app/src/Test/Database/ResultSet.php @@ -4,13 +4,22 @@ namespace MichalSpacekCz\Test\Database; use Nette\Database\ResultSet as NetteResultSet; +use Override; class ResultSet extends NetteResultSet { /** @noinspection PhpMissingParentConstructorInspection intentionally */ - public function __construct() + public function __construct( + private readonly ?int $rowCount = null, + ) { + } + + + #[Override] + public function getRowCount(): ?int { + return $this->rowCount; } } diff --git a/app/src/Training/ApplicationStatuses/TrainingApplicationStatuses.php b/app/src/Training/ApplicationStatuses/TrainingApplicationStatuses.php index 1be43758b..9aba5e6d8 100644 --- a/app/src/Training/ApplicationStatuses/TrainingApplicationStatuses.php +++ b/app/src/Training/ApplicationStatuses/TrainingApplicationStatuses.php @@ -7,6 +7,7 @@ use DateTimeInterface; use Exception; use MichalSpacekCz\Database\TypedDatabase; +use MichalSpacekCz\DateTime\DateTimeFactory; use MichalSpacekCz\Training\Exceptions\CannotUpdateTrainingApplicationStatusException; use MichalSpacekCz\Training\Exceptions\TrainingApplicationDoesNotExistException; use Nette\Database\Explorer; @@ -32,6 +33,7 @@ public function __construct( private readonly Explorer $database, private readonly TypedDatabase $typedDatabase, private readonly TrainingApplicationStatusHistory $statusHistory, + private readonly DateTimeFactory $dateTimeFactory, ) { } @@ -204,7 +206,7 @@ private function setStatus(int $applicationId, TrainingApplicationStatus $status assert($prevStatus->statusTime instanceof DateTime); assert(is_string($prevStatus->statusTimeTimeZone)); - $datetime = new DateTime($date ?? ''); + $datetime = $this->dateTimeFactory->create($date ?? ''); Debugger::log(sprintf( 'Changing status for application id: %d; old status: %s, old status time: %s; new status: %s, new status time: %s', diff --git a/app/tests/Form/ChangePasswordFormFactoryTest.phpt b/app/tests/Form/ChangePasswordFormFactoryTest.phpt new file mode 100644 index 000000000..b26d17a3a --- /dev/null +++ b/app/tests/Form/ChangePasswordFormFactoryTest.phpt @@ -0,0 +1,80 @@ +getService('passwordEncryption'); + assert($service instanceof SymmetricKeyEncryption); + $this->passwordEncryption = $service; + } + + + public function testCreateOnSuccessAdd(): void + { + PrivateProperty::setValue($this->user, 'identity', new SimpleIdentity(self::USER_ID, [], ['username' => self::USERNAME])); + PrivateProperty::setValue($this->user, 'authenticated', true); + $this->database->setFetchResult([ + 'userId' => self::USER_ID, + 'username' => self::USERNAME, + 'password' => $this->passwordEncryption->encrypt($this->passwords->hash(self::PASSWORD)), + ]); + $form = $this->formFactory->create( + function (): void { + $this->result = true; + }, + ); + $form->setDefaults([ + 'password' => self::PASSWORD, + 'newPassword' => self::NEW_PASSWORD, + ]); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::true($this->result); + [$hash, $userId] = $this->database->getParamsForQuery('UPDATE users SET password = ? WHERE id_user = ?'); + assert(is_string($hash)); + assert(is_int($userId)); + Assert::same(self::USER_ID, $userId); + Assert::true($this->passwords->verify(self::NEW_PASSWORD, $this->passwordEncryption->decrypt($hash))); + } + +} + +TestCaseRunner::run(ChangePasswordFormFactoryTest::class); diff --git a/app/tests/Form/PostFormFactoryTest.phpt b/app/tests/Form/PostFormFactoryTest.phpt new file mode 100644 index 000000000..33cad185c --- /dev/null +++ b/app/tests/Form/PostFormFactoryTest.phpt @@ -0,0 +1,231 @@ +applicationPresenter->createUiPresenter('Admin:Blog', 'Admin:Blog', 'default'); + $this->template = $templateFactory->createTemplate($presenter); + } + + + #[Override] + protected function setUp(): void + { + // Data needed to create the form + $twitterCardResult = [ + 'cardId' => 46, + 'card' => 'summary', + 'title' => 'Summary Card', + ]; + $this->database->addFetchAllResult([$twitterCardResult]); + $this->database->addFetchPairsResult([self::LOCALE_ID => 'en_US']); + // For onSuccess queries + $this->database->setFetchResult($twitterCardResult); + // Upcoming trainings + $this->database->addFetchAllResult([]); + // Blog post edits + $this->database->addFetchAllResult([]); + + $this->database->setDefaultInsertId('48'); + $this->localeLinkGenerator->setLinks(['en_US' => 'https://com.example/']); + } + + + #[Override] + protected function tearDown(): void + { + $this->blogPostAdd = null; + $this->blogPostEdit = null; + $this->templateSent = null; + $this->database->reset(); + } + + + public function testCreateOnSuccessAdd(): void + { + $form = $this->buildFormAdd(); + Arrays::invoke($form->onSuccess, $form); + Assert::same(48, $this->blogPostAdd?->getId()); + Assert::null($this->blogPostEdit); + Assert::null($this->templateSent); + Assert::same([], $this->database->getParamsArrayForQuery('INSERT INTO blog_post_edits')); + } + + + public function testCreateOnClickPreviewAdd(): void + { + $form = $this->buildFormAdd(); + $submit = $form->getComponent('preview'); + assert($submit instanceof SubmitButton); + Arrays::invoke($submit->onClick, $form); + if ($this->templateSent instanceof DefaultTemplate) { + Assert::contains('Title', $this->templateSent->renderToString()); + } else { + Assert::fail('A template should be sent on preview'); + } + } + + + public function testCreateOnSuccessEdit(): void + { + $form = $this->buildFormEdit(); + Arrays::invoke($form->onSuccess, $form); + Assert::null($this->blogPostAdd); + Assert::same(49, $this->blogPostEdit?->getId()); + Assert::null($this->templateSent); + $queryParams = [ + [ + 'key_blog_post' => 49, + 'edited_at' => '2024-12-15 00:33:08', + 'edited_at_timezone' => 'Europe/Prague', + 'summary' => self::EDIT_SUMMARY, + ], + ]; + Assert::same($queryParams, $this->database->getParamsArrayForQuery('INSERT INTO blog_post_edits')); + } + + + public function testCreateOnClickPreviewEdit(): void + { + $form = $this->buildFormEdit(); + $submit = $form->getComponent('preview'); + assert($submit instanceof SubmitButton); + Arrays::invoke($submit->onClick, $form); + if ($this->templateSent instanceof DefaultTemplate) { + Assert::contains('Title', $this->templateSent->renderToString()); + } else { + Assert::fail('A template should be sent on preview'); + } + } + + + private function setFormDefaults(UiForm $form): void + { + $form->setDefaults([ + 'locale' => self::LOCALE_ID, + 'published' => '2024-12-14 15:33:08', + 'title' => '**Title**', + 'twitterCard' => 'summary', + 'editSummary' => self::EDIT_SUMMARY, + ]); + } + + + private function buildFormAdd(): UiForm + { + $form = $this->formFactory->create( + function (BlogPost $post): void { + $this->blogPostAdd = $post; + }, + function (BlogPost $post): void { + $this->blogPostEdit = $post; + }, + $this->template, + function (?DefaultTemplate $template): void { + $this->templateSent = $template; + }, + null, + ); + $this->setFormDefaults($form); + $this->applicationPresenter->anchorForm($form); + return $form; + } + + + /** + * @return UiForm + */ + private function buildFormEdit(): UiForm + { + $post = new BlogPost( + 49, + '', + self::LOCALE_ID, + 'en_US', + null, + Html::fromText('title'), + 'title', + Html::fromText('lead'), + 'lead', + Html::fromText('text'), + 'text', + new DateTime(), + false, + null, + null, + null, + null, + [], + [], + [], + null, + 'https://example.com/something', + [], + [], + [], + false, + ); + $this->dateTimeFactory->setDateTime(new DateTimeImmutable('2024-12-15 00:33:08')); + + $form = $this->formFactory->create( + function (BlogPost $post): void { + $this->blogPostAdd = $post; + }, + function (BlogPost $post): void { + $this->blogPostEdit = $post; + }, + $this->template, + function (?DefaultTemplate $template): void { + $this->templateSent = $template; + }, + $post, + ); + $this->setFormDefaults($form); + $this->applicationPresenter->anchorForm($form); + return $form; + } + +} + +TestCaseRunner::run(PostFormFactoryTest::class); diff --git a/app/tests/Form/TrainingApplicationPreliminaryFormFactoryTest.phpt b/app/tests/Form/TrainingApplicationPreliminaryFormFactoryTest.phpt new file mode 100644 index 000000000..606ea4527 --- /dev/null +++ b/app/tests/Form/TrainingApplicationPreliminaryFormFactoryTest.phpt @@ -0,0 +1,94 @@ +setFetchPairsDefaultResult([ + 2 => 'TENTATIVE', + ]); + // For Statuses::getStatusId() in TrainingApplicationStorage::insertApplication() + $database->addFetchFieldResult(2); + // For TrainingApplicationSources::getSourceId in TrainingApplicationStorage::insertApplication() + $database->addFetchFieldResult(3); + // For Statuses::getStatusId() in Statuses::setStatus() + $database->addFetchFieldResult(4); + // For $prevStatus in Statuses::setStatus() + $database->setFetchDefaultResult([ + 'statusId' => 5, + 'statusTime' => new DateTime('-2 days'), + 'statusTimeTimeZone' => 'Europe/Prague', + ]); + + $this->form = $formFactory->create( + function (string $action): void { + $this->action = $action; + }, + function (string $message): void { + $this->message = $message; + }, + self::TRAINING_ID, + 'action', + ); + $applicationPresenter->anchorForm($this->form); + } + + + #[Override] + protected function tearDown(): void + { + $this->action = $this->message = null; + } + + + public function testCreateOnSuccess(): void + { + Arrays::invoke($this->form->onSuccess, $this->form); + Assert::same('action', $this->action); + Assert::null($this->message); + $params = $this->database->getParamsArrayForQuery('INSERT INTO training_applications'); + Assert::same(self::TRAINING_ID, $params[0]['key_training']); + } + + + public function testCreateOnSuccessSpam(): void + { + $this->form->setDefaults([ + 'name' => 'lowercase', + ]); + Arrays::invoke($this->form->onSuccess, $this->form); + Assert::null($this->action); + Assert::same('messages.trainings.spammyapplication', $this->message); + } + +} + +TestCaseRunner::run(TrainingApplicationPreliminaryFormFactoryTest::class); diff --git a/app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt b/app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt new file mode 100644 index 000000000..05d6cb95d --- /dev/null +++ b/app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt @@ -0,0 +1,225 @@ +setDateTime($dateTime); + // For TrainingApplicationStatuses::getStatusId() + $this->database->addFetchFieldResult(self::STATUS_ID); + // For TrainingApplicationStatuses::getChildrenStatuses() + $this->database->addFetchPairsResult([ + 15 => TrainingApplicationStatus::Attended->value, + ]); + + $this->form = $formFactory->create( + function (Html|null $message): void { + if ($message === null) { + $this->result = true; + } else { + $this->result = $message->toHtml(); + } + }, + [$this->buildApplication()], + ); + $applicationPresenter->anchorForm($this->form); + $this->form->setDefaults([ + 'applications' => [ + self::APPLICATION_ID => TrainingApplicationStatus::Attended->value, + ], + ]); + } + + + public function testCreateOnClickSubmit(): void + { + $statusDateTime = new DateTime('2024-10-20 05:06:07'); + // For TrainingApplicationStatuses::setStatus() + $this->database->addFetchResult([ + 'statusId' => 15, // REMINDED + 'statusTime' => $statusDateTime, + 'statusTimeTimeZone' => $statusDateTime->getTimezone()->getName(), + ]); + // For TrainingApplicationStatuses::getDiscardedStatuses() + $this->database->addFetchFieldResult(self::STATUS_ID + 1); + // For TrainingApplicationStatuses::getAllowFilesStatuses() + $this->database->addFetchFieldResult(self::STATUS_ID + 2); + $this->database->addFetchFieldResult(self::STATUS_ID + 3); + $this->database->addFetchFieldResult(self::STATUS_ID + 4); + $submit = $this->form->getComponent('submit'); + assert($submit instanceof SubmitButton); + Arrays::invoke($submit->onClick); + Assert::true($this->result); + Assert::same([ + [ + 'key_status' => self::STATUS_ID, + 'status_time' => '2024-12-17 12:10:48', + 'status_time_timezone' => 'Europe/Prague', + ], + ], $this->database->getParamsArrayForQuery('UPDATE training_applications SET ? WHERE id_application = ?')); + } + + + public function testCreateOnClickFamiliar(): void + { + $this->addApplicationFetchResult(); + $statusDateTime = new DateTime('2024-10-20 07:08:09'); + // For TrainingApplicationStatuses::setStatus() + $this->database->addFetchResult([ + 'statusId' => 15, // REMINDED + 'statusTime' => $statusDateTime, + 'statusTimeTimeZone' => $statusDateTime->getTimezone()->getName(), + ]); + $familiar = $this->form->getComponent('familiar'); + assert($familiar instanceof SubmitButton); + Arrays::invoke($familiar->onClick); + Assert::same('Tykání nastaveno pro 1 účastníků ve stavu ATTENDED', $this->result); + Assert::same([self::APPLICATION_ID], $this->database->getParamsForQuery('UPDATE training_applications SET familiar = TRUE WHERE id_application = ?')); + } + + + private function addApplicationFetchResult(): void + { + $this->database->addFetchResult([ + 'id' => self::APPLICATION_ID, + 'name' => null, + 'email' => null, + 'familiar' => 0, + 'company' => null, + 'street' => null, + 'city' => null, + 'zip' => null, + 'country' => null, + 'companyId' => null, + 'companyTaxId' => null, + 'note' => null, + 'status' => 'ATTENDED', + 'statusTime' => new DateTime(), + 'dateId' => null, + 'trainingId' => null, + 'trainingAction' => 'action', + 'trainingName' => 'Le //Name//', + 'trainingStart' => null, + 'trainingEnd' => null, + 'publicDate' => 1, + 'remote' => 1, + 'remoteUrl' => 'https://remote.example/', + 'remoteNotes' => null, + 'videoHref' => null, + 'feedbackHref' => null, + 'venueAction' => null, + 'venueName' => null, + 'venueNameExtended' => null, + 'venueAddress' => null, + 'venueCity' => null, + 'price' => null, + 'vatRate' => null, + 'priceVat' => null, + 'discount' => null, + 'invoiceId' => null, + 'paid' => null, + 'accessToken' => 'token', + 'sourceAlias' => 'michal-spacek', + 'sourceName' => 'Michal Špaček', + ]); + } + + + private function buildApplication(): TrainingApplication + { + return new TrainingApplication( + $this->applicationStatuses, + $this->trainingMailMessageFactory, + $this->trainingFiles, + self::APPLICATION_ID, + null, + null, + false, + null, + null, + null, + null, + null, + null, + null, + null, + TrainingApplicationStatus::Reminded, + new DateTime(), + true, + false, + false, + null, + null, + 'action', + Html::fromText('Name'), + null, + null, + false, + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + '', + '', + null, + null, + null, + 'accessToken', + 'michal-spacek', + 'Michal Špaček', + 'MŠ', + ); + } + +} + +TestCaseRunner::run(TrainingApplicationStatusesFormFactoryTest::class); diff --git a/app/tests/Form/TrainingDateFormFactoryTest.phpt b/app/tests/Form/TrainingDateFormFactoryTest.phpt new file mode 100644 index 000000000..67189ed1d --- /dev/null +++ b/app/tests/Form/TrainingDateFormFactoryTest.phpt @@ -0,0 +1,210 @@ + self::TRAINING_ID, + 'action' => '', + 'name' => 'Training name', + 'description' => '', + 'content' => '', + 'upsell' => '', + 'prerequisites' => '', + 'audience' => '', + 'capacity' => 20, + 'price' => null, + 'studentDiscount' => 40, + 'materials' => '', + 'custom' => 0, + 'successorId' => null, + 'discontinuedId' => null, + ]; + // For Trainings::getById() + $this->database->setFetchResult($training); + $this->database->addFetchAllResult([$training]); + $this->database->addFetchAllResult([ + [ + 'id' => 60, + 'name' => 'Venue name', + 'nameExtended' => '', + 'href' => '', + 'address' => '', + 'city' => '', + 'descriptionTexy' => '', + 'action' => '', + 'entrance' => '', + 'entranceNavigation' => '', + 'streetview' => '', + 'parkingTexy' => '', + 'publicTransportTexy' => '', + ], + ]); + $this->database->addFetchAllResult([ + [ + 'id' => 3, + 'status' => 'CONFIRMED', + 'description' => 'Displayed on the site with full date, regular signup', + ], + ]); + $this->database->addFetchPairsResult([ + self::COOPERATION_ID => 'coop', + ]); + } + + + #[Override] + protected function tearDown(): void + { + $this->database->reset(); + $this->resultAdd = null; + $this->resultEditId = null; + } + + + public function testCreateOnSuccessAdd(): void + { + $form = $this->formFactory->create( + function (): void { + $this->resultAdd = true; + }, + function (): void { + }, + null, + ); + $this->applicationPresenter->anchorForm($form); + $form->setDefaults([ + 'training' => self::TRAINING_ID, + 'status' => TrainingDateStatus::Confirmed->id(), + 'cooperation' => self::COOPERATION_ID, + ]); + Arrays::invoke($form->onSuccess, $form); + Assert::true($this->resultAdd); + Assert::null($this->resultEditId); + } + + + public function testCreateOnSuccessEdit(): void + { + $form = $this->formFactory->create( + function (): void { + }, + function (int $dateId): void { + $this->resultEditId = $dateId; + }, + $this->buildTrainingDate(), + ); + $this->applicationPresenter->anchorForm($form); + $form->setDefaults([ + 'training' => self::TRAINING_ID, + 'status' => TrainingDateStatus::Confirmed->id(), + 'cooperation' => self::COOPERATION_ID, + ]); + Arrays::invoke($form->onSuccess, $form); + Assert::null($this->resultAdd); + Assert::same(self::TRAINING_DATE_ID, $this->resultEditId); + } + + + public function testCreateOnValidate(): void + { + $form = $this->formFactory->create( + function (): void { + }, + function (int $dateId): void { + $this->resultEditId = $dateId; + }, + $this->buildTrainingDate(), + ); + $this->applicationPresenter->anchorForm($form); + $form->setDefaults([ + 'training' => self::TRAINING_ID, + 'status' => TrainingDateStatus::Confirmed->id(), + 'cooperation' => self::COOPERATION_ID, + ]); + Arrays::invoke($form->onValidate, $form); + Assert::null($this->resultAdd); + Assert::null($this->resultEditId); + Assert::same(['Běžná cena není nastavena, je třeba nastavit cenu zde'], $form->getErrors()); + } + + + private function buildTrainingDate(): TrainingDate + { + return new TrainingDate( + self::TRAINING_DATE_ID, + '', + self::TRAINING_ID, + true, + false, + new DateTime('2024-10-20 10:00:00'), + new DateTime('2024-10-21 18:00:00'), + null, + null, + true, + TrainingDateStatus::Confirmed, + '', + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + false, + null, + false, + null, + null, + null, + null, + ); + } + +} + +TestCaseRunner::run(TrainingDateFormFactoryTest::class); diff --git a/app/tests/Form/TrainingInvoiceFormFactoryTest.phpt b/app/tests/Form/TrainingInvoiceFormFactoryTest.phpt new file mode 100644 index 000000000..d5cdec327 --- /dev/null +++ b/app/tests/Form/TrainingInvoiceFormFactoryTest.phpt @@ -0,0 +1,56 @@ +formFactory->create( + function (int $count): void { + $this->count = $count; + }, + function (): void { + $this->count = null; + }, + [11, 22], + ); + $this->applicationPresenter->anchorForm($form); + + $this->database->setResultSet(new ResultSet(1)); + Arrays::invoke($form->onSuccess, $form); + Assert::same(1, $this->count); + + $this->database->setResultSet(new ResultSet()); + Arrays::invoke($form->onSuccess, $form); + Assert::null($this->count); + } + +} + +TestCaseRunner::run(TrainingInvoiceFormFactoryTest::class); diff --git a/app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt b/app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt new file mode 100644 index 000000000..438fcd063 --- /dev/null +++ b/app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt @@ -0,0 +1,164 @@ +trainingStart = new DateTime('2024-04-03 02:01:00'); + } + + + public function testCreateOnSuccess(): void + { + // For TrainingFiles::getFiles() => TrainingApplicationStatuses::getAllowFilesStatuses() + $this->database->setFetchFieldDefaultResult(26); + // For TrainingFiles::getFiles() + $this->database->addFetchAllResult([ + [ + 'fileId' => 52, + 'fileName' => 'filename.pdf', + 'start' => $this->trainingStart, + 'added' => (clone $this->trainingStart)->modify('+1 week'), + ], + ]); + + $presenter = $this->applicationPresenter->createUiPresenter('Admin:Emails', 'Admin:Emails', 'default'); + PrivateProperty::setValue($this->application, 'presenter', $presenter); + $application = $this->buildApplication(); + + $application->setNextStatus(TrainingApplicationStatus::Reminded); + $form = $this->formFactory->create( + function (int $sent): void { + $this->sent = $sent; + }, + [$application->getId() => $application], + ); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::same(1, $this->sent); + Assert::same('Připomenutí školení Training Name 3.–5. dubna 2024', $this->mailer->getMail()->getSubject()); + + $application->setNextStatus(TrainingApplicationStatus::MaterialsSent); + $form = $this->formFactory->create( + function (int $sent): void { + $this->sent = $sent; + }, + [$application->getId() => $application], + ); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::same(1, $this->sent); + Assert::same('Materiály ze školení Training Name', $this->mailer->getMail()->getSubject()); + Assert::contains(self::FEEDBACK_URL, $this->mailer->getMail()->getBody()); + + $form->setDefaults([ + 'applications' => [ + $application->getId() => [ + 'feedbackRequest' => false, + ], + ], + ]); + Arrays::invoke($form->onSuccess, $form); + Assert::same(1, $this->sent); + Assert::same('Materiály ze školení Training Name', $this->mailer->getMail()->getSubject()); + Assert::notContains(self::FEEDBACK_URL, $this->mailer->getMail()->getBody()); + } + + + private function buildApplication(): TrainingApplication + { + return new TrainingApplication( + $this->applicationStatuses, + $this->trainingMailMessageFactory, + $this->trainingFiles, + 3212, + 'John Doe', + 'email@example.com', + false, + null, + null, + null, + null, + null, + null, + null, + null, + TrainingApplicationStatus::Attended, + new DateTime(), + true, + false, + false, + null, + null, + 'action', + Html::fromText('Training Name'), + $this->trainingStart, + new DateTime('2024-04-05 06:07:08'), + false, + true, + 'https://zoom.example/', + null, + null, + self::FEEDBACK_URL, + null, + null, + null, + null, + null, + 150, + 10, + 165, + '', + '', + null, + null, + null, + 'accessToken', + 'michal-spacek', + 'Michal Špaček', + 'MŠ', + ); + } + +} + +TestCaseRunner::run(TrainingMailsOutboxFormFactoryTest::class); From 1a3087159c32f87cc7073891c1ebbee9dcd7a2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 17 Dec 2024 10:30:33 +0100 Subject: [PATCH 3/9] Need to pass the enum value, not just a string Follow-up to #279 --- app/src/Form/TrainingApplicationMultipleFormFactory.php | 3 ++- app/src/Form/TrainingApplicationStatusesFormFactory.php | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/Form/TrainingApplicationMultipleFormFactory.php b/app/src/Form/TrainingApplicationMultipleFormFactory.php index 57bf4bb18..8b53b0943 100644 --- a/app/src/Form/TrainingApplicationMultipleFormFactory.php +++ b/app/src/Form/TrainingApplicationMultipleFormFactory.php @@ -6,6 +6,7 @@ use MichalSpacekCz\Form\Controls\TrainingControlsFactory; use MichalSpacekCz\Http\HttpInput; use MichalSpacekCz\Training\Applications\TrainingApplicationStorage; +use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; use MichalSpacekCz\Training\Dates\TrainingDate; @@ -71,7 +72,7 @@ public function create(callable $onSuccess, TrainingDate $trainingDate): UiForm $application->note, $trainingDate->getPrice(), $trainingDate->getStudentDiscount(), - $values->status, + TrainingApplicationStatus::from($values->status), $values->source, $values->date, ); diff --git a/app/src/Form/TrainingApplicationStatusesFormFactory.php b/app/src/Form/TrainingApplicationStatusesFormFactory.php index 752174582..ec6c6002d 100644 --- a/app/src/Form/TrainingApplicationStatusesFormFactory.php +++ b/app/src/Form/TrainingApplicationStatusesFormFactory.php @@ -6,6 +6,7 @@ use MichalSpacekCz\Form\Controls\TrainingControlsFactory; use MichalSpacekCz\Training\Applications\TrainingApplication; use MichalSpacekCz\Training\Applications\TrainingApplications; +use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; use Nette\Utils\ArrayHash; use Nette\Utils\Html; @@ -54,7 +55,7 @@ public function create(callable $onSuccess, array $applications): UiForm foreach ($values->applications as $id => $status) { assert(is_string($status)); if ($status) { - $this->trainingApplicationStatuses->updateStatus($id, $status, $values->date); + $this->trainingApplicationStatuses->updateStatus($id, TrainingApplicationStatus::from($status), $values->date); } } $onSuccess(null); @@ -72,7 +73,7 @@ public function create(callable $onSuccess, array $applications): UiForm $statuses = []; foreach ($attendedStatuses as $status) { - $statuses[] = Html::el('code')->setText($status); + $statuses[] = Html::el('code')->setText($status->value); } $message = Html::el() ->setText('Tykání nastaveno pro ' . $total . ' účastníků ve stavu ') From e2487d5e6cfe9065481b6ba0f9d0414fee35648f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Tue, 17 Dec 2024 14:49:46 +0100 Subject: [PATCH 4/9] Add a fetch() result queue to the database mock --- app/src/Test/Database/Database.php | 26 +++++++++++++++---- app/tests/Articles/ArticlesTest.phpt | 2 +- .../Form/ChangePasswordFormFactoryTest.phpt | 2 +- app/tests/Form/PostFormFactoryTest.phpt | 2 +- app/tests/Form/SignInFormFactoryTest.phpt | 2 +- .../Form/TrainingDateFormFactoryTest.phpt | 2 +- app/tests/Interviews/InterviewsTest.phpt | 4 +-- app/tests/Pulse/CompaniesTest.phpt | 2 +- .../PasswordHashingAlgorithmsTest.phpt | 2 +- app/tests/Pulse/SitesTest.phpt | 2 +- .../TrainingApplicationFormSuccessTest.phpt | 2 +- .../TrainingApplicationStorageTest.phpt | 2 +- .../Training/Dates/TrainingDatesTest.phpt | 4 +-- .../Files/TrainingFilesDownloadTest.phpt | 4 +-- .../Training/Venues/TrainingVenuesTest.phpt | 2 +- app/tests/Twitter/TwitterCardsTest.phpt | 2 +- app/tests/User/ManagerTest.phpt | 10 +++---- 17 files changed, 44 insertions(+), 28 deletions(-) diff --git a/app/src/Test/Database/Database.php b/app/src/Test/Database/Database.php index d46d7f0b5..846032e37 100644 --- a/app/src/Test/Database/Database.php +++ b/app/src/Test/Database/Database.php @@ -31,7 +31,12 @@ class Database extends Explorer private array $queriesArrayParams = []; /** @var array */ - private array $fetchResult = []; + private array $fetchDefaultResult = []; + + /** @var list> */ + private array $fetchResults = []; + + private int $fetchResultsPosition = 0; private mixed $fetchFieldDefaultResult = null; @@ -66,7 +71,9 @@ public function reset(): void $this->insertIdsPosition = 0; $this->queriesScalarParams = []; $this->queriesArrayParams = []; - $this->fetchResult = []; + $this->fetchDefaultResult = []; + $this->fetchResults = []; + $this->fetchResultsPosition = 0; $this->fetchPairsDefaultResult = []; $this->fetchPairsResults = []; $this->fetchPairsResultsPosition = 0; @@ -172,12 +179,21 @@ public function getParamsArrayForQuery(string $query): array } + /** + * @param array $fetchDefaultResult + */ + public function setFetchDefaultResult(array $fetchDefaultResult): void + { + $this->fetchDefaultResult = $fetchDefaultResult; + } + + /** * @param array $fetchResult */ - public function setFetchResult(array $fetchResult): void + public function addFetchResult(array $fetchResult): void { - $this->fetchResult = $fetchResult; + $this->fetchResults[] = $fetchResult; } @@ -188,7 +204,7 @@ public function setFetchResult(array $fetchResult): void #[Override] public function fetch(string $sql, ...$params): ?Row { - $row = $this->createRow($this->fetchResult); + $row = $this->createRow($this->fetchResults[$this->fetchResultsPosition++] ?? $this->fetchDefaultResult); return $row->count() > 0 ? $row : null; } diff --git a/app/tests/Articles/ArticlesTest.phpt b/app/tests/Articles/ArticlesTest.phpt index 64c826eca..bddf219c5 100644 --- a/app/tests/Articles/ArticlesTest.phpt +++ b/app/tests/Articles/ArticlesTest.phpt @@ -195,7 +195,7 @@ class ArticlesTest extends TestCase public function testGetLabelByTag(): void { Assert::null($this->articles->getLabelByTag('foo')); - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'tags' => '["HTTP Secure", "HTTP/2"]', 'slugTags' => '["https", "http-2"]', ]); diff --git a/app/tests/Form/ChangePasswordFormFactoryTest.phpt b/app/tests/Form/ChangePasswordFormFactoryTest.phpt index b26d17a3a..2cabe494f 100644 --- a/app/tests/Form/ChangePasswordFormFactoryTest.phpt +++ b/app/tests/Form/ChangePasswordFormFactoryTest.phpt @@ -51,7 +51,7 @@ class ChangePasswordFormFactoryTest extends TestCase { PrivateProperty::setValue($this->user, 'identity', new SimpleIdentity(self::USER_ID, [], ['username' => self::USERNAME])); PrivateProperty::setValue($this->user, 'authenticated', true); - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'userId' => self::USER_ID, 'username' => self::USERNAME, 'password' => $this->passwordEncryption->encrypt($this->passwords->hash(self::PASSWORD)), diff --git a/app/tests/Form/PostFormFactoryTest.phpt b/app/tests/Form/PostFormFactoryTest.phpt index 33cad185c..b289408d8 100644 --- a/app/tests/Form/PostFormFactoryTest.phpt +++ b/app/tests/Form/PostFormFactoryTest.phpt @@ -61,7 +61,7 @@ class PostFormFactoryTest extends TestCase $this->database->addFetchAllResult([$twitterCardResult]); $this->database->addFetchPairsResult([self::LOCALE_ID => 'en_US']); // For onSuccess queries - $this->database->setFetchResult($twitterCardResult); + $this->database->setFetchDefaultResult($twitterCardResult); // Upcoming trainings $this->database->addFetchAllResult([]); // Blog post edits diff --git a/app/tests/Form/SignInFormFactoryTest.phpt b/app/tests/Form/SignInFormFactoryTest.phpt index 9d4ac1b17..846102fe9 100644 --- a/app/tests/Form/SignInFormFactoryTest.phpt +++ b/app/tests/Form/SignInFormFactoryTest.phpt @@ -77,7 +77,7 @@ class SignInFormFactoryTest extends TestCase */ public function testCreateOnSuccess(string $username, string $password, ?string $message): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'userId' => 123, 'username' => 'root', 'password' => $this->passwordEncryption->encrypt($this->passwords->hash('hunter2')), diff --git a/app/tests/Form/TrainingDateFormFactoryTest.phpt b/app/tests/Form/TrainingDateFormFactoryTest.phpt index 67189ed1d..20ce678d0 100644 --- a/app/tests/Form/TrainingDateFormFactoryTest.phpt +++ b/app/tests/Form/TrainingDateFormFactoryTest.phpt @@ -59,7 +59,7 @@ class TrainingDateFormFactoryTest extends TestCase 'discontinuedId' => null, ]; // For Trainings::getById() - $this->database->setFetchResult($training); + $this->database->setFetchDefaultResult($training); $this->database->addFetchAllResult([$training]); $this->database->addFetchAllResult([ [ diff --git a/app/tests/Interviews/InterviewsTest.phpt b/app/tests/Interviews/InterviewsTest.phpt index 9bdc72a51..3263f2ac9 100644 --- a/app/tests/Interviews/InterviewsTest.phpt +++ b/app/tests/Interviews/InterviewsTest.phpt @@ -96,7 +96,7 @@ class InterviewsTest extends TestCase public function testGet(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => 1, 'action' => 'action-1', 'title' => 'Action 1', @@ -124,7 +124,7 @@ class InterviewsTest extends TestCase public function testGetById(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => 2, 'action' => 'action-2', 'title' => 'Action 2', diff --git a/app/tests/Pulse/CompaniesTest.phpt b/app/tests/Pulse/CompaniesTest.phpt index fcff4fb0e..35326645f 100644 --- a/app/tests/Pulse/CompaniesTest.phpt +++ b/app/tests/Pulse/CompaniesTest.phpt @@ -56,7 +56,7 @@ class CompaniesTest extends TestCase public function testGetByName(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => 123, 'name' => 'One', 'tradeName' => 'dba', diff --git a/app/tests/Pulse/Passwords/PasswordHashingAlgorithmsTest.phpt b/app/tests/Pulse/Passwords/PasswordHashingAlgorithmsTest.phpt index 0961a5c3c..c73df44a7 100644 --- a/app/tests/Pulse/Passwords/PasswordHashingAlgorithmsTest.phpt +++ b/app/tests/Pulse/Passwords/PasswordHashingAlgorithmsTest.phpt @@ -57,7 +57,7 @@ class PasswordHashingAlgorithmsTest extends TestCase public function testGetAlgorithmByName(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => 303, 'algo' => 'Arr-gone', 'alias' => 'arr-gone', diff --git a/app/tests/Pulse/SitesTest.phpt b/app/tests/Pulse/SitesTest.phpt index ed515a18c..def8a228a 100644 --- a/app/tests/Pulse/SitesTest.phpt +++ b/app/tests/Pulse/SitesTest.phpt @@ -48,7 +48,7 @@ class SitesTest extends TestCase public function testGetByUrl(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => 123, 'url' => 'https://example.com/', 'alias' => 'one', diff --git a/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt b/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt index 3a368a403..602bdaee5 100644 --- a/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt +++ b/app/tests/Training/ApplicationForm/TrainingApplicationFormSuccessTest.phpt @@ -106,7 +106,7 @@ class TrainingApplicationFormSuccessTest extends TestCase $this->database->addFetchFieldResult(1); // For Statuses::getStatusId() in TrainingApplicationStorage::insertApplication() $this->database->addFetchFieldResult(303); // For TrainingApplicationSources::getSourceId in TrainingApplicationStorage::insertApplication() $this->database->addFetchFieldResult(4); // For Statuses::getStatusId() in Statuses::setStatus() - $this->database->setFetchResult([ // For $prevStatus in Statuses::setStatus() + $this->database->setFetchDefaultResult([ // For $prevStatus in Statuses::setStatus() 'statusId' => 1, 'statusTime' => new DateTime('-2 days'), 'statusTimeTimeZone' => 'Europe/Prague', diff --git a/app/tests/Training/Applications/TrainingApplicationStorageTest.phpt b/app/tests/Training/Applications/TrainingApplicationStorageTest.phpt index 9a9ec7435..5a1e6bca8 100644 --- a/app/tests/Training/Applications/TrainingApplicationStorageTest.phpt +++ b/app/tests/Training/Applications/TrainingApplicationStorageTest.phpt @@ -50,7 +50,7 @@ class TrainingApplicationStorageTest extends TestCase $this->database->addFetchFieldResult(self::STATUS_CREATED); // For ApplicationStatuses::getStatusId() in TrainingApplicationStorage::insertApplication() $this->database->addFetchFieldResult(self::SOURCE_ID); // For TrainingApplicationSources::getSourceId in TrainingApplicationStorage::insertApplication() $this->database->setDefaultInsertId((string)self::INSERT_ID); - $this->database->setFetchResult([ // For ApplicationStatuses::setStatus() in ApplicationStatuses::updateStatusCallbackReturnId() + $this->database->setFetchDefaultResult([ // For ApplicationStatuses::setStatus() in ApplicationStatuses::updateStatusCallbackReturnId() 'statusId' => self::STATUS_CREATED, 'statusTime' => new DateTime(), 'statusTimeTimeZone' => 'Europe/Prague', diff --git a/app/tests/Training/Dates/TrainingDatesTest.phpt b/app/tests/Training/Dates/TrainingDatesTest.phpt index cc685568c..e70af76c4 100644 --- a/app/tests/Training/Dates/TrainingDatesTest.phpt +++ b/app/tests/Training/Dates/TrainingDatesTest.phpt @@ -104,7 +104,7 @@ class TrainingDatesTest extends TestCase public function testGet(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'dateId' => 1, 'trainingId' => 1, 'action' => 'action-1', @@ -223,7 +223,7 @@ class TrainingDatesTest extends TestCase */ public function testLastFreeSeats(DateTime $start, TrainingDateStatus $status, bool $lastFreeSeats): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'dateId' => 1, 'trainingId' => 1, 'action' => 'action-1', diff --git a/app/tests/Training/Files/TrainingFilesDownloadTest.phpt b/app/tests/Training/Files/TrainingFilesDownloadTest.phpt index c7a73da40..5a8fe5fd7 100644 --- a/app/tests/Training/Files/TrainingFilesDownloadTest.phpt +++ b/app/tests/Training/Files/TrainingFilesDownloadTest.phpt @@ -115,7 +115,7 @@ class TrainingFilesDownloadTest extends TestCase $sessionSection->set('token', self::TOKEN); $filename = 'file.zip'; $filesDir = __DIR__ . '/'; - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'added' => new DateTime(), 'fileId' => 1337, 'fileName' => $filename, @@ -131,7 +131,7 @@ class TrainingFilesDownloadTest extends TestCase private function setApplicationFetchResult(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => self::APPLICATION_ID, 'name' => null, 'email' => null, diff --git a/app/tests/Training/Venues/TrainingVenuesTest.phpt b/app/tests/Training/Venues/TrainingVenuesTest.phpt index d996dc202..e99ee973b 100644 --- a/app/tests/Training/Venues/TrainingVenuesTest.phpt +++ b/app/tests/Training/Venues/TrainingVenuesTest.phpt @@ -30,7 +30,7 @@ class TrainingVenuesTest extends TestCase }, TrainingVenueNotFoundException::class, "Training venue 'foo' doesn't exist"); - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => 1, 'name' => 'Name', 'nameExtended' => null, diff --git a/app/tests/Twitter/TwitterCardsTest.phpt b/app/tests/Twitter/TwitterCardsTest.phpt index 3ee96f7f7..26f1bc58c 100644 --- a/app/tests/Twitter/TwitterCardsTest.phpt +++ b/app/tests/Twitter/TwitterCardsTest.phpt @@ -58,7 +58,7 @@ class TwitterCardsTest extends TestCase public function testGetCard(): void { - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'cardId' => 3, 'card' => 'summary', 'title' => 'Summary', diff --git a/app/tests/User/ManagerTest.phpt b/app/tests/User/ManagerTest.phpt index 80cc87825..68b5637b2 100644 --- a/app/tests/User/ManagerTest.phpt +++ b/app/tests/User/ManagerTest.phpt @@ -119,7 +119,7 @@ class ManagerTest extends TestCase $newPassword = 'hunter3'; PrivateProperty::setValue($this->user, 'authenticated', true); PrivateProperty::setValue($this->user, 'identity', new SimpleIdentity(1337, [], ['username' => '303'])); - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'userId' => 1337, 'username' => '303', 'password' => $this->passwordEncryption->encrypt($this->passwords->hash($oldPassword)), @@ -163,7 +163,7 @@ class ManagerTest extends TestCase $hash = hash('sha512', $token); $userId = 1338; $username = '🍪🍪🍪'; - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => $tokenId, 'token' => $hash, 'userId' => $userId, @@ -194,7 +194,7 @@ class ManagerTest extends TestCase $hash = hash('sha512', $token); $userId = 1338; $username = '🍕🍕🍕'; - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'id' => $tokenId, 'token' => $hash, 'userId' => $userId, @@ -222,7 +222,7 @@ class ManagerTest extends TestCase Assert::exception(function (): void { $this->authenticator->authenticate('foo', 'bar'); }, AuthenticationException::class, 'The username is incorrect.'); - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'userId' => $userId, 'username' => $username, 'password' => $this->passwordEncryption->encrypt($this->passwords->hash($password)), @@ -242,7 +242,7 @@ class ManagerTest extends TestCase $username = 'foo'; $password = 'bar'; $hash = password_hash($password, PASSWORD_DEFAULT); - $this->database->setFetchResult([ + $this->database->setFetchDefaultResult([ 'userId' => $userId, 'username' => $username, 'password' => $this->passwordEncryption->encrypt($hash), From 27e46c2a8943237e540fb8ea0343c19d037e7a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Thu, 19 Dec 2024 02:36:58 +0100 Subject: [PATCH 5/9] Correct form field name I don't think it ever worked but now it's fixed thanks to the PHPStan Level 10 effort (and tests). --- app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php b/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php index 7bd345c2a..9197b14b0 100644 --- a/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php +++ b/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php @@ -206,7 +206,7 @@ private function validatePasswordsStorages(UiForm $form, ArrayHash $values): voi if (!empty($values->site->new->url) && $this->sites->getByUrl($values->site->new->url)) { $form->addError('Can\'t add new site, duplicated URL'); } - if (!empty($values->algo->new->algo) && $this->hashingAlgorithms->getAlgorithmByName($values->algo->new->algo)) { + if (!empty($values->algo->new->algoName) && $this->hashingAlgorithms->getAlgorithmByName($values->algo->new->algoName)) { $form->addError('Can\'t add new algorithm, duplicated name'); } } From 4c8f9cfb7890d7cb7c7566b4533f1aa6efd8a1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Wed, 18 Dec 2024 02:26:04 +0100 Subject: [PATCH 6/9] A service to create objects and data needed in tests --- app/config/tests.neon | 1 + app/psalm-baseline.xml | 7 - .../Test/Training/TrainingTestDataFactory.php | 142 ++++++++++++++++++ ...ingApplicationStatusesFormFactoryTest.phpt | 116 +------------- .../TrainingMailsOutboxFormFactoryTest.phpt | 77 ++-------- ...TrainingApplicationFormDataLoggerTest.phpt | 70 +-------- ...TrainingApplicationSessionSectionTest.phpt | 78 +--------- .../Files/TrainingFilesDownloadTest.phpt | 53 +------ .../TrainingFilesSessionSectionTest.phpt | 69 +-------- .../Mails/TrainingMailMessageFactoryTest.phpt | 76 +--------- 10 files changed, 180 insertions(+), 509 deletions(-) create mode 100644 app/src/Test/Training/TrainingTestDataFactory.php diff --git a/app/config/tests.neon b/app/config/tests.neon index dd5fa301a..92c12e979 100644 --- a/app/config/tests.neon +++ b/app/config/tests.neon @@ -54,6 +54,7 @@ services: security.userStorage: MichalSpacekCz\Test\Security\NullUserStorage - MichalSpacekCz\Test\TestCaseRunner trainingFilesStorage: MichalSpacekCz\Test\Training\TrainingFilesNullStorage + - MichalSpacekCz\Test\Training\TrainingTestDataFactory cache.storage: Nette\Caching\Storages\DevNullStorage netteHttpResponse: # Needed for User\Manager because https://github.com/nette/http/issues/200 create: Nette\Http\Response diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 08cd6cc7a..77fcf82ea 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -52,13 +52,6 @@ status]]> - - - - - date]]> - - diff --git a/app/src/Test/Training/TrainingTestDataFactory.php b/app/src/Test/Training/TrainingTestDataFactory.php new file mode 100644 index 000000000..b4aa0ce67 --- /dev/null +++ b/app/src/Test/Training/TrainingTestDataFactory.php @@ -0,0 +1,142 @@ +applicationStatuses, + $this->trainingMailMessageFactory, + $this->trainingFiles, + $id, + $name, + $email, + $familiar, + null, + null, + null, + null, + null, + null, + null, + null, + $status, + new DateTime(), + true, + false, + false, + $dateId, + null, + 'action', + Html::fromText($trainingName), + $trainingStart, + $trainingEnd, + false, + $remote, + $remoteUrl, + null, + null, + $feedbackHref, + null, + null, + null, + null, + null, + null, + null, + null, + '', + '', + null, + null, + null, + 'accessToken', + 'michal-spacek', + 'Michal Špaček', + 'MŠ', + ); + } + + + /** + * @return array + */ + public function getDatabaseResultData(int $id): array + { + return [ + 'id' => $id, + 'name' => null, + 'email' => null, + 'familiar' => 0, + 'company' => null, + 'street' => null, + 'city' => null, + 'zip' => null, + 'country' => null, + 'companyId' => null, + 'companyTaxId' => null, + 'note' => null, + 'status' => 'ATTENDED', + 'statusTime' => new DateTime(), + 'dateId' => null, + 'trainingId' => null, + 'trainingAction' => 'action', + 'trainingName' => 'Le //Name//', + 'trainingStart' => null, + 'trainingEnd' => null, + 'publicDate' => 1, + 'remote' => 1, + 'remoteUrl' => 'https://remote.example/', + 'remoteNotes' => null, + 'videoHref' => null, + 'feedbackHref' => null, + 'venueAction' => null, + 'venueName' => null, + 'venueNameExtended' => null, + 'venueAddress' => null, + 'venueCity' => null, + 'price' => null, + 'vatRate' => null, + 'priceVat' => null, + 'discount' => null, + 'invoiceId' => null, + 'paid' => null, + 'accessToken' => 'token', + 'sourceAlias' => 'michal-spacek', + 'sourceName' => 'Michal Špaček', + ]; + } + +} diff --git a/app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt b/app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt index 05d6cb95d..22d9b0bb4 100644 --- a/app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt +++ b/app/tests/Form/TrainingApplicationStatusesFormFactoryTest.phpt @@ -10,11 +10,8 @@ use MichalSpacekCz\Test\Application\ApplicationPresenter; use MichalSpacekCz\Test\Database\Database; use MichalSpacekCz\Test\DateTime\DateTimeMachineFactory; use MichalSpacekCz\Test\TestCaseRunner; -use MichalSpacekCz\Training\Applications\TrainingApplication; +use MichalSpacekCz\Test\Training\TrainingTestDataFactory; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; -use MichalSpacekCz\Training\Files\TrainingFiles; -use MichalSpacekCz\Training\Mails\TrainingMailMessageFactory; use Nette\Forms\Controls\SubmitButton; use Nette\Utils\Arrays; use Nette\Utils\Html; @@ -36,9 +33,7 @@ class TrainingApplicationStatusesFormFactoryTest extends TestCase public function __construct( private readonly Database $database, - private readonly TrainingApplicationStatuses $applicationStatuses, - private readonly TrainingMailMessageFactory $trainingMailMessageFactory, - private readonly TrainingFiles $trainingFiles, + private readonly TrainingTestDataFactory $dataFactory, TrainingApplicationStatusesFormFactory $formFactory, ApplicationPresenter $applicationPresenter, DateTimeMachineFactory $dateTimeFactory, @@ -60,7 +55,7 @@ class TrainingApplicationStatusesFormFactoryTest extends TestCase $this->result = $message->toHtml(); } }, - [$this->buildApplication()], + [$dataFactory->getTrainingApplication(self::APPLICATION_ID, status: TrainingApplicationStatus::Reminded)], ); $applicationPresenter->anchorForm($this->form); $this->form->setDefaults([ @@ -102,7 +97,7 @@ class TrainingApplicationStatusesFormFactoryTest extends TestCase public function testCreateOnClickFamiliar(): void { - $this->addApplicationFetchResult(); + $this->database->addFetchResult($this->dataFactory->getDatabaseResultData(self::APPLICATION_ID)); $statusDateTime = new DateTime('2024-10-20 07:08:09'); // For TrainingApplicationStatuses::setStatus() $this->database->addFetchResult([ @@ -117,109 +112,6 @@ class TrainingApplicationStatusesFormFactoryTest extends TestCase Assert::same([self::APPLICATION_ID], $this->database->getParamsForQuery('UPDATE training_applications SET familiar = TRUE WHERE id_application = ?')); } - - private function addApplicationFetchResult(): void - { - $this->database->addFetchResult([ - 'id' => self::APPLICATION_ID, - 'name' => null, - 'email' => null, - 'familiar' => 0, - 'company' => null, - 'street' => null, - 'city' => null, - 'zip' => null, - 'country' => null, - 'companyId' => null, - 'companyTaxId' => null, - 'note' => null, - 'status' => 'ATTENDED', - 'statusTime' => new DateTime(), - 'dateId' => null, - 'trainingId' => null, - 'trainingAction' => 'action', - 'trainingName' => 'Le //Name//', - 'trainingStart' => null, - 'trainingEnd' => null, - 'publicDate' => 1, - 'remote' => 1, - 'remoteUrl' => 'https://remote.example/', - 'remoteNotes' => null, - 'videoHref' => null, - 'feedbackHref' => null, - 'venueAction' => null, - 'venueName' => null, - 'venueNameExtended' => null, - 'venueAddress' => null, - 'venueCity' => null, - 'price' => null, - 'vatRate' => null, - 'priceVat' => null, - 'discount' => null, - 'invoiceId' => null, - 'paid' => null, - 'accessToken' => 'token', - 'sourceAlias' => 'michal-spacek', - 'sourceName' => 'Michal Špaček', - ]); - } - - - private function buildApplication(): TrainingApplication - { - return new TrainingApplication( - $this->applicationStatuses, - $this->trainingMailMessageFactory, - $this->trainingFiles, - self::APPLICATION_ID, - null, - null, - false, - null, - null, - null, - null, - null, - null, - null, - null, - TrainingApplicationStatus::Reminded, - new DateTime(), - true, - false, - false, - null, - null, - 'action', - Html::fromText('Name'), - null, - null, - false, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - '', - '', - null, - null, - null, - 'accessToken', - 'michal-spacek', - 'Michal Špaček', - 'MŠ', - ); - } - } TestCaseRunner::run(TrainingApplicationStatusesFormFactoryTest::class); diff --git a/app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt b/app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt index 438fcd063..8e3d80fc9 100644 --- a/app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt +++ b/app/tests/Form/TrainingMailsOutboxFormFactoryTest.phpt @@ -10,14 +10,10 @@ use MichalSpacekCz\Test\Database\Database; use MichalSpacekCz\Test\NullMailer; use MichalSpacekCz\Test\PrivateProperty; use MichalSpacekCz\Test\TestCaseRunner; -use MichalSpacekCz\Training\Applications\TrainingApplication; +use MichalSpacekCz\Test\Training\TrainingTestDataFactory; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; -use MichalSpacekCz\Training\Files\TrainingFiles; -use MichalSpacekCz\Training\Mails\TrainingMailMessageFactory; use Nette\Application\Application; use Nette\Utils\Arrays; -use Nette\Utils\Html; use Tester\Assert; use Tester\TestCase; @@ -38,10 +34,8 @@ class TrainingMailsOutboxFormFactoryTest extends TestCase private readonly TrainingMailsOutboxFormFactory $formFactory, private readonly ApplicationPresenter $applicationPresenter, private readonly Application $application, - private readonly TrainingApplicationStatuses $applicationStatuses, - private readonly TrainingMailMessageFactory $trainingMailMessageFactory, - private readonly TrainingFiles $trainingFiles, private readonly NullMailer $mailer, + private readonly TrainingTestDataFactory $dataFactory, ) { $this->trainingStart = new DateTime('2024-04-03 02:01:00'); } @@ -63,7 +57,16 @@ class TrainingMailsOutboxFormFactoryTest extends TestCase $presenter = $this->applicationPresenter->createUiPresenter('Admin:Emails', 'Admin:Emails', 'default'); PrivateProperty::setValue($this->application, 'presenter', $presenter); - $application = $this->buildApplication(); + $application = $this->dataFactory->getTrainingApplication( + 3212, + name: 'John Doe', + email: 'email@example.com', + trainingStart: $this->trainingStart, + trainingEnd: new DateTime('2024-04-05 06:07:08'), + remote: true, + remoteUrl: 'https://zoom.example/', + feedbackHref: self::FEEDBACK_URL, + ); $application->setNextStatus(TrainingApplicationStatus::Reminded); $form = $this->formFactory->create( @@ -103,62 +106,6 @@ class TrainingMailsOutboxFormFactoryTest extends TestCase Assert::notContains(self::FEEDBACK_URL, $this->mailer->getMail()->getBody()); } - - private function buildApplication(): TrainingApplication - { - return new TrainingApplication( - $this->applicationStatuses, - $this->trainingMailMessageFactory, - $this->trainingFiles, - 3212, - 'John Doe', - 'email@example.com', - false, - null, - null, - null, - null, - null, - null, - null, - null, - TrainingApplicationStatus::Attended, - new DateTime(), - true, - false, - false, - null, - null, - 'action', - Html::fromText('Training Name'), - $this->trainingStart, - new DateTime('2024-04-05 06:07:08'), - false, - true, - 'https://zoom.example/', - null, - null, - self::FEEDBACK_URL, - null, - null, - null, - null, - null, - 150, - 10, - 165, - '', - '', - null, - null, - null, - 'accessToken', - 'michal-spacek', - 'Michal Špaček', - 'MŠ', - ); - } - } TestCaseRunner::run(TrainingMailsOutboxFormFactoryTest::class); diff --git a/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt b/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt index bc6708e8e..04038ea3b 100644 --- a/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt +++ b/app/tests/Training/ApplicationForm/TrainingApplicationFormDataLoggerTest.phpt @@ -3,17 +3,11 @@ declare(strict_types = 1); namespace MichalSpacekCz\Training\ApplicationForm; -use DateTime; use MichalSpacekCz\Test\Http\NullSession; use MichalSpacekCz\Test\NullLogger; use MichalSpacekCz\Test\TestCaseRunner; -use MichalSpacekCz\Training\Applications\TrainingApplication; +use MichalSpacekCz\Test\Training\TrainingTestDataFactory; use MichalSpacekCz\Training\Applications\TrainingApplicationSessionSection; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; -use MichalSpacekCz\Training\Files\TrainingFiles; -use MichalSpacekCz\Training\Mails\TrainingMailMessageFactory; -use Nette\Utils\Html; use Override; use Tester\Assert; use Tester\TestCase; @@ -32,9 +26,7 @@ class TrainingApplicationFormDataLoggerTest extends TestCase private readonly TrainingApplicationFormDataLogger $formDataLogger, private readonly NullLogger $logger, private readonly NullSession $session, - private readonly TrainingApplicationStatuses $applicationStatuses, - private readonly TrainingMailMessageFactory $trainingMailMessageFactory, - private readonly TrainingFiles $trainingFiles, + private readonly TrainingTestDataFactory $dataFactory, ) { } @@ -86,69 +78,13 @@ class TrainingApplicationFormDataLoggerTest extends TestCase $trainingName = 'foo'; $session = $this->getTrainingSessionSection(); - $session->setApplicationForTraining($trainingName, $this->getApplication()); + $session->setApplicationForTraining($trainingName, $this->dataFactory->getTrainingApplication(self::APPLICATION_ID, dateId: self::DATE_ID)); $this->formDataLogger->log($values, $trainingName, self::DATE_ID, $session); $expected = sprintf("Application session data for foo: id => '%s', dateId => '%s', form values: key1 => 'value1', key2 => 'value2', key3 => int", self::APPLICATION_ID, self::DATE_ID); Assert::same([$expected], $this->logger->getLogged()); } - private function getApplication(): TrainingApplication - { - return new TrainingApplication( - $this->applicationStatuses, - $this->trainingMailMessageFactory, - $this->trainingFiles, - self::APPLICATION_ID, - null, - null, - false, - null, - null, - null, - null, - null, - null, - null, - null, - TrainingApplicationStatus::Attended, - new DateTime(), - true, - false, - false, - self::DATE_ID, - null, - 'action', - Html::fromText('Name'), - null, - null, - false, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - '', - '', - null, - null, - null, - 'accessToken', - 'michal-spacek', - 'Michal Špaček', - 'MŠ', - ); - } - - private function getTrainingSessionSection(): TrainingApplicationSessionSection { return $this->session->getSection('section', TrainingApplicationSessionSection::class); diff --git a/app/tests/Training/Applications/TrainingApplicationSessionSectionTest.phpt b/app/tests/Training/Applications/TrainingApplicationSessionSectionTest.phpt index 99a6fe79f..6378c9d59 100644 --- a/app/tests/Training/Applications/TrainingApplicationSessionSectionTest.phpt +++ b/app/tests/Training/Applications/TrainingApplicationSessionSectionTest.phpt @@ -7,15 +7,11 @@ namespace MichalSpacekCz\Training\Applications; use DateTime; use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\TestCaseRunner; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; +use MichalSpacekCz\Test\Training\TrainingTestDataFactory; use MichalSpacekCz\Training\Dates\TrainingDate; use MichalSpacekCz\Training\Dates\TrainingDateStatus; -use MichalSpacekCz\Training\Files\TrainingFiles; -use MichalSpacekCz\Training\Mails\TrainingMailMessageFactory; use Nette\Http\Session; use Nette\Http\SessionSection; -use Nette\Utils\Html; use Override; use stdClass; use Tester\Assert; @@ -29,25 +25,13 @@ class TrainingApplicationSessionSectionTest extends TestCase private const int APPLICATION_ID = 303; private const int DATE_ID = 909; - private const string NAME = 'Foo'; - private const string EMAIL = 'foo@example.example'; - private const string COMPANY = 'Teh Company'; - private const string STREET = 'Street'; - private const string CITY = 'City'; - private const string ZIP = '303808'; - private const string COUNTRY = 'Country'; - private const string COMPANY_ID = '31337'; - private const string COMPANY_TAX_ID = 'CZ31337'; - private const string NOTE = 'Note'; private TrainingApplicationSessionSection $trainingApplicationSessionSection; private SessionSection $sessionSection; public function __construct( - private readonly TrainingApplicationStatuses $applicationStatuses, - private readonly TrainingMailMessageFactory $trainingMailMessageFactory, - private readonly TrainingFiles $trainingFiles, + private readonly TrainingTestDataFactory $dataFactory, Session $sessionHandler, ) { $this->trainingApplicationSessionSection = $sessionHandler->getSection('training', TrainingApplicationSessionSection::class); @@ -64,7 +48,7 @@ class TrainingApplicationSessionSectionTest extends TestCase public function testSetApplicationForTraining(): void { - $application = $this->buildApplication(); + $application = $this->dataFactory->getTrainingApplication(self::APPLICATION_ID, null, null); $this->sessionSection->set('application', ['foo' => 'bar']); $this->trainingApplicationSessionSection->setApplicationForTraining('training-action', $application); Assert::same(['foo' => 'bar', 'training-action' => ['id' => $application->getId(), 'dateId' => $application->getDateId()]], $this->sessionSection->get('application')); @@ -195,62 +179,6 @@ class TrainingApplicationSessionSectionTest extends TestCase } - private function buildApplication(): TrainingApplication - { - return new TrainingApplication( - $this->applicationStatuses, - $this->trainingMailMessageFactory, - $this->trainingFiles, - self::APPLICATION_ID, - self::NAME, - self::EMAIL, - false, - self::COMPANY, - self::STREET, - self::CITY, - self::ZIP, - self::COUNTRY, - self::COMPANY_ID, - self::COMPANY_TAX_ID, - self::NOTE, - TrainingApplicationStatus::Attended, - new DateTime(), - true, - false, - false, - self::DATE_ID, - null, - 'action', - Html::fromText('Name'), - null, - null, - false, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - '', - '', - null, - null, - null, - 'accessToken', - 'michal-spacek', - 'Michal Špaček', - 'MŠ', - ); - } - - private function buildTrainingDate(): TrainingDate { return new TrainingDate( diff --git a/app/tests/Training/Files/TrainingFilesDownloadTest.phpt b/app/tests/Training/Files/TrainingFilesDownloadTest.phpt index 5a8fe5fd7..e4c85cfbd 100644 --- a/app/tests/Training/Files/TrainingFilesDownloadTest.phpt +++ b/app/tests/Training/Files/TrainingFilesDownloadTest.phpt @@ -12,6 +12,7 @@ use MichalSpacekCz\Test\Http\NullSession; use MichalSpacekCz\Test\PrivateProperty; use MichalSpacekCz\Test\TestCaseRunner; use MichalSpacekCz\Test\Training\TrainingFilesNullStorage; +use MichalSpacekCz\Test\Training\TrainingTestDataFactory; use Nette\Application\Application; use Nette\Application\BadRequestException; use Nette\Application\Responses\RedirectResponse; @@ -37,6 +38,7 @@ class TrainingFilesDownloadTest extends TestCase private readonly Database $database, private readonly NullSession $session, private readonly TrainingFilesNullStorage $storage, + private readonly TrainingTestDataFactory $dataFactory, Application $application, ) { $this->presenter = new UiPresenterMock(); @@ -55,7 +57,7 @@ class TrainingFilesDownloadTest extends TestCase public function testStartRedirect(): void { $this->database->setFetchFieldDefaultResult(123); // For Statuses::getStatusId() - $this->setApplicationFetchResult(); // For TrainingApplications::getApplicationByToken() + $this->database->setFetchDefaultResult($this->dataFactory->getDatabaseResultData(self::APPLICATION_ID)); // For TrainingApplications::getApplicationByToken() Assert::true($this->applicationPresenter->expectSendResponse(function (): void { $this->trainingFilesDownload->start('action', self::TOKEN); })); @@ -84,7 +86,7 @@ class TrainingFilesDownloadTest extends TestCase $sessionSection = $this->session->getSection('training'); $sessionSection->set('applicationId', self::APPLICATION_ID); $sessionSection->set('token', self::TOKEN); - $this->setApplicationFetchResult(); // For TrainingApplications::getApplicationById() + $this->database->setFetchDefaultResult($this->dataFactory->getDatabaseResultData(self::APPLICATION_ID)); // For TrainingApplications::getApplicationById() Assert::same(self::APPLICATION_ID, $this->trainingFilesDownload->start('action', null)->getId()); } @@ -128,53 +130,6 @@ class TrainingFilesDownloadTest extends TestCase Assert::same('application/zip', $response->getContentType()); } - - private function setApplicationFetchResult(): void - { - $this->database->setFetchDefaultResult([ - 'id' => self::APPLICATION_ID, - 'name' => null, - 'email' => null, - 'familiar' => 0, - 'company' => null, - 'street' => null, - 'city' => null, - 'zip' => null, - 'country' => null, - 'companyId' => null, - 'companyTaxId' => null, - 'note' => null, - 'status' => 'ATTENDED', - 'statusTime' => new DateTime(), - 'dateId' => null, - 'trainingId' => null, - 'trainingAction' => 'action', - 'trainingName' => 'Le //Name//', - 'trainingStart' => null, - 'trainingEnd' => null, - 'publicDate' => 1, - 'remote' => 1, - 'remoteUrl' => 'https://remote.example/', - 'remoteNotes' => null, - 'videoHref' => null, - 'feedbackHref' => null, - 'venueAction' => null, - 'venueName' => null, - 'venueNameExtended' => null, - 'venueAddress' => null, - 'venueCity' => null, - 'price' => null, - 'vatRate' => null, - 'priceVat' => null, - 'discount' => null, - 'invoiceId' => null, - 'paid' => null, - 'accessToken' => 'token', - 'sourceAlias' => 'michal-spacek', - 'sourceName' => 'Michal Špaček', - ]); - } - } TestCaseRunner::run(TrainingFilesDownloadTest::class); diff --git a/app/tests/Training/Files/TrainingFilesSessionSectionTest.phpt b/app/tests/Training/Files/TrainingFilesSessionSectionTest.phpt index 1c65895ce..924d9cae4 100644 --- a/app/tests/Training/Files/TrainingFilesSessionSectionTest.phpt +++ b/app/tests/Training/Files/TrainingFilesSessionSectionTest.phpt @@ -3,16 +3,11 @@ declare(strict_types = 1); namespace MichalSpacekCz\Training\Files; -use DateTime; use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\TestCaseRunner; -use MichalSpacekCz\Training\Applications\TrainingApplication; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; -use MichalSpacekCz\Training\Mails\TrainingMailMessageFactory; +use MichalSpacekCz\Test\Training\TrainingTestDataFactory; use Nette\Http\Session; use Nette\Http\SessionSection; -use Nette\Utils\Html; use Override; use Tester\Assert; use Tester\TestCase; @@ -33,9 +28,7 @@ class TrainingFilesSessionSectionTest extends TestCase public function __construct( - private readonly TrainingApplicationStatuses $applicationStatuses, - private readonly TrainingMailMessageFactory $trainingMailMessageFactory, - private readonly TrainingFiles $trainingFiles, + private readonly TrainingTestDataFactory $dataFactory, Session $sessionHandler, ) { $this->trainingFilesSessionSection = $sessionHandler->getSection('training', TrainingFilesSessionSection::class); @@ -56,7 +49,7 @@ class TrainingFilesSessionSectionTest extends TestCase Assert::same(self::TOKEN, $this->sessionSection->get(self::TOKEN_KEY)); Assert::null($this->sessionSection->get(self::APPLICATION_ID_KEY)); - $this->trainingFilesSessionSection->setValues(self::TOKEN, $this->buildApplication()); + $this->trainingFilesSessionSection->setValues(self::TOKEN, $this->dataFactory->getTrainingApplication(self::APPLICATION_ID)); Assert::same(self::TOKEN, $this->sessionSection->get(self::TOKEN_KEY)); Assert::same(self::APPLICATION_ID, $this->sessionSection->get(self::APPLICATION_ID_KEY)); } @@ -102,62 +95,6 @@ class TrainingFilesSessionSectionTest extends TestCase Assert::same(self::TOKEN, $this->trainingFilesSessionSection->getToken()); } - - private function buildApplication(): TrainingApplication - { - return new TrainingApplication( - $this->applicationStatuses, - $this->trainingMailMessageFactory, - $this->trainingFiles, - self::APPLICATION_ID, - null, - null, - false, - null, - null, - null, - null, - null, - null, - null, - null, - TrainingApplicationStatus::Attended, - new DateTime(), - true, - false, - false, - null, - null, - 'action', - Html::fromText('Name'), - null, - null, - false, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - '', - '', - null, - null, - null, - 'accessToken', - 'michal-spacek', - 'Michal Špaček', - 'MŠ', - ); - } - } TestCaseRunner::run(TrainingFilesSessionSectionTest::class); diff --git a/app/tests/Training/Mails/TrainingMailMessageFactoryTest.phpt b/app/tests/Training/Mails/TrainingMailMessageFactoryTest.phpt index 79ad338ba..712a527d5 100644 --- a/app/tests/Training/Mails/TrainingMailMessageFactoryTest.phpt +++ b/app/tests/Training/Mails/TrainingMailMessageFactoryTest.phpt @@ -7,11 +7,8 @@ use DateTime; use MichalSpacekCz\ShouldNotHappenException; use MichalSpacekCz\Test\Database\Database; use MichalSpacekCz\Test\TestCaseRunner; -use MichalSpacekCz\Training\Applications\TrainingApplication; +use MichalSpacekCz\Test\Training\TrainingTestDataFactory; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; -use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; -use MichalSpacekCz\Training\Files\TrainingFiles; -use Nette\Utils\Html; use Tester\Assert; use Tester\TestCase; @@ -24,15 +21,14 @@ class TrainingMailMessageFactoryTest extends TestCase public function __construct( private readonly Database $database, private readonly TrainingMailMessageFactory $trainingMailMessageFactory, - private readonly TrainingApplicationStatuses $applicationStatuses, - private readonly TrainingFiles $trainingFiles, + private readonly TrainingTestDataFactory $dataFactory, ) { } public function testGetMailMessage(): void { - $application = $this->getApplication(); + $application = $this->dataFactory->getTrainingApplication(1, null, null); Assert::exception(function () use ($application): void { $this->trainingMailMessageFactory->getMailMessage($application); }, ShouldNotHappenException::class, "Unsupported next status: ''"); @@ -43,14 +39,14 @@ class TrainingMailMessageFactoryTest extends TestCase $application->setNextStatus(TrainingApplicationStatus::MaterialsSent); Assert::same('materials', $this->trainingMailMessageFactory->getMailMessage($application)->getBasename()); - $application = $this->getApplication(true); + $application = $this->dataFactory->getTrainingApplication(1, familiar: true); $application->setNextStatus(TrainingApplicationStatus::MaterialsSent); Assert::same('materialsFamiliar', $this->trainingMailMessageFactory->getMailMessage($application)->getBasename()); $application->setNextStatus(TrainingApplicationStatus::InvoiceSent); Assert::same('invoice', $this->trainingMailMessageFactory->getMailMessage($application)->getBasename()); - $application = $this->getApplication(status: TrainingApplicationStatus::ProFormaInvoiceSent); + $application = $this->dataFactory->getTrainingApplication(1, status: TrainingApplicationStatus::ProFormaInvoiceSent); $application->setNextStatus(TrainingApplicationStatus::InvoiceSent); Assert::same('invoiceAfterProforma', $this->trainingMailMessageFactory->getMailMessage($application)->getBasename()); @@ -62,11 +58,11 @@ class TrainingMailMessageFactoryTest extends TestCase $this->trainingMailMessageFactory->getMailMessage($application); }, ShouldNotHappenException::class, "Training application id '1' with next status 'REMINDED' should have both training start and end set"); - $application = $this->getApplication(trainingStart: new DateTime(), trainingEnd: new DateTime()); + $application = $this->dataFactory->getTrainingApplication(1, trainingStart: new DateTime(), trainingEnd: new DateTime()); $application->setNextStatus(TrainingApplicationStatus::Reminded); Assert::same('reminder', $this->trainingMailMessageFactory->getMailMessage($application)->getBasename()); - $application = $this->getApplication(isRemote: true, trainingStart: new DateTime(), trainingEnd: new DateTime()); + $application = $this->dataFactory->getTrainingApplication(1, trainingStart: new DateTime(), trainingEnd: new DateTime(), remote: true); $application->setNextStatus(TrainingApplicationStatus::Reminded); Assert::same('reminderRemote', $this->trainingMailMessageFactory->getMailMessage($application)->getBasename()); } @@ -83,67 +79,11 @@ class TrainingMailMessageFactoryTest extends TestCase 'statusTimeTimeZone' => 'Europe/Prague', ], ]); - $application = $this->getApplication(); + $application = $this->dataFactory->getTrainingApplication(1, null, null); $application->setNextStatus(TrainingApplicationStatus::InvoiceSentAfter); Assert::same('invoiceAfterProforma', $this->trainingMailMessageFactory->getMailMessage($application)->getBasename()); } - - private function getApplication(bool $familiar = false, TrainingApplicationStatus $status = TrainingApplicationStatus::Attended, bool $isRemote = false, ?DateTime $trainingStart = null, ?DateTime $trainingEnd = null): TrainingApplication - { - return new TrainingApplication( - $this->applicationStatuses, - $this->trainingMailMessageFactory, - $this->trainingFiles, - 1, - null, - null, - $familiar, - null, - null, - null, - null, - null, - null, - null, - null, - $status, - new DateTime(), - true, - false, - false, - null, - null, - 'action', - Html::fromText('Name'), - $trainingStart, - $trainingEnd, - false, - $isRemote, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - '', - '', - null, - null, - null, - 'accessToken', - 'michal-spacek', - 'Michal Špaček', - 'MŠ', - ); - } - } TestCaseRunner::run(TrainingMailMessageFactoryTest::class); From dee4843dfbebc226ad22f7eba8dbfd1a105d9741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Wed, 18 Dec 2024 04:08:01 +0100 Subject: [PATCH 7/9] More asserts for PHPStan Level 10 forms --- app/psalm-baseline.xml | 47 ---- .../PasswordsStorageAlgorithmFormFactory.php | 11 + .../TrainingApplicationAdminFormFactory.php | 42 +++- ...TrainingApplicationMultipleFormFactory.php | 16 ++ app/src/Test/Database/Database.php | 10 +- .../Test/Training/TrainingTestDataFactory.php | 6 +- ...swordsStorageAlgorithmFormFactoryTest.phpt | 226 ++++++++++++++++++ ...ainingApplicationAdminFormFactoryTest.phpt | 106 ++++++++ ...ingApplicationMultipleFormFactoryTest.phpt | 123 ++++++++++ 9 files changed, 526 insertions(+), 61 deletions(-) create mode 100644 app/tests/Form/Pulse/PasswordsStorageAlgorithmFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingApplicationAdminFormFactoryTest.phpt create mode 100644 app/tests/Form/TrainingApplicationMultipleFormFactoryTest.phpt diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 77fcf82ea..dd6cc57e4 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -5,53 +5,6 @@ getValue()]]> - - - algo->new->algo]]> - company->id]]> - company->new->name]]> - site->new->url]]> - - - - - - citySet ? $values->city : null]]> - companyIdSet ? $values->companyId : null]]> - companySet ? $values->company : null]]> - companyTaxIdSet ? $values->companyTaxId : null]]> - countrySet ? $values->country : null]]> - discount]]> - emailSet ? $values->email : null]]> - familiar]]> - invoiceId]]> - nameSet ? $values->name : null]]> - noteSet ? $values->note : null]]> - paid]]> - source]]> - streetSet ? $values->street : null]]> - vatRate]]> - zipSet ? $values->zip : null]]> - vatRate) !== '' ? $values->vatRate / 100 : null]]> - - - - - city]]> - company]]> - companyId]]> - companyTaxId]]> - email]]> - name]]> - note]]> - street]]> - zip]]> - country]]> - date]]> - source]]> - status]]> - - diff --git a/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php b/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php index 9197b14b0..c92ecee25 100644 --- a/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php +++ b/app/src/Form/Pulse/PasswordsStorageAlgorithmFormFactory.php @@ -189,7 +189,18 @@ public function create(callable $onSuccess, int $newDisclosures): UiForm */ private function validatePasswordsStorages(UiForm $form, ArrayHash $values): void { + assert($values->company instanceof ArrayHash); + assert($values->company->new instanceof ArrayHash); + assert(is_string($values->company->new->name)); + assert($values->site instanceof ArrayHash); + assert(is_int($values->site->id) || $values->site->id === Sites::ALL || $values->site->id === null); + assert($values->site->new instanceof ArrayHash); + assert(is_string($values->site->new->url)); + assert($values->algo instanceof ArrayHash); + assert($values->algo->new instanceof ArrayHash); + assert(is_string($values->algo->new->algoName)); if (empty($values->company->new->name)) { + assert(is_int($values->company->id)); $storages = $this->passwords->getStoragesByCompanyId($values->company->id); $specificSites = array_filter($storages->getSites(), function ($site) { return $site instanceof StorageSpecificSite; diff --git a/app/src/Form/TrainingApplicationAdminFormFactory.php b/app/src/Form/TrainingApplicationAdminFormFactory.php index 15976a117..de617552d 100644 --- a/app/src/Form/TrainingApplicationAdminFormFactory.php +++ b/app/src/Form/TrainingApplicationAdminFormFactory.php @@ -91,7 +91,35 @@ public function create(callable $onSuccess, callable $onStatusHistoryDeleteSucce $form->onSuccess[] = function (UiForm $form) use ($application, $onSuccess): void { $values = $form->getFormValues(); - $dateId = $values->date ?? null; + assert(is_bool($values->nameSet)); + assert(is_string($values->name)); + assert(is_bool($values->emailSet)); + assert(is_string($values->email)); + assert(is_bool($values->companySet)); + assert(is_string($values->company)); + assert(is_bool($values->streetSet)); + assert(is_string($values->street)); + assert(is_bool($values->citySet)); + assert(is_string($values->city)); + assert(is_bool($values->zipSet)); + assert(is_string($values->zip)); + assert(is_bool($values->countrySet)); + assert(is_string($values->country)); + assert(is_bool($values->companyIdSet)); + assert(is_string($values->companyId)); + assert(is_bool($values->companyTaxIdSet)); + assert(is_string($values->companyTaxId)); + assert(is_bool($values->noteSet)); + assert(is_string($values->note)); + assert(is_string($values->source)); + assert(is_float($values->price) || $values->price === null); + assert(is_string($values->vatRate)); + assert(is_float($values->priceVat) || $values->priceVat === null); + assert(is_string($values->discount)); + assert(is_string($values->invoiceId) || $values->invoiceId === null); + assert(is_string($values->paid)); + assert(is_bool($values->familiar)); + assert(is_int($values->date) || $values->date === null); $this->trainingApplicationStorage->updateApplicationData( $application->getId(), $values->nameSet ? $values->name : null, @@ -105,16 +133,16 @@ public function create(callable $onSuccess, callable $onStatusHistoryDeleteSucce $values->companyTaxIdSet ? $values->companyTaxId : null, $values->noteSet ? $values->note : null, $values->source, - (is_float($values->price) ? $values->price : null), - (trim($values->vatRate) !== '' ? $values->vatRate / 100 : null), - (is_float($values->priceVat) ? $values->priceVat : null), - (trim($values->discount) !== '' ? (int)$values->discount : null), + $values->price, + trim($values->vatRate) !== '' ? (float)$values->vatRate / 100 : null, + $values->priceVat, + trim($values->discount) !== '' ? (int)$values->discount : null, $values->invoiceId, $values->paid, $values->familiar, - $dateId, + $values->date, ); - $onSuccess($dateId); + $onSuccess($values->date); }; $this->setApplication($form, $application); return $form; diff --git a/app/src/Form/TrainingApplicationMultipleFormFactory.php b/app/src/Form/TrainingApplicationMultipleFormFactory.php index 8b53b0943..8da688d9a 100644 --- a/app/src/Form/TrainingApplicationMultipleFormFactory.php +++ b/app/src/Form/TrainingApplicationMultipleFormFactory.php @@ -9,6 +9,7 @@ use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatus; use MichalSpacekCz\Training\ApplicationStatuses\TrainingApplicationStatuses; use MichalSpacekCz\Training\Dates\TrainingDate; +use Nette\Utils\ArrayHash; readonly class TrainingApplicationMultipleFormFactory { @@ -56,7 +57,22 @@ public function create(callable $onSuccess, TrainingDate $trainingDate): UiForm $form->onSuccess[] = function (UiForm $form) use ($trainingDate, $onSuccess): void { $values = $form->getFormValues(); + assert($values->applications instanceof ArrayHash); + assert(is_string($values->country)); + assert(is_string($values->status)); + assert(is_string($values->source)); + assert(is_string($values->date)); foreach ($values->applications as $application) { + assert($application instanceof ArrayHash); + assert(is_string($application->name)); + assert(is_string($application->email)); + assert(is_string($application->company)); + assert(is_string($application->street)); + assert(is_string($application->city)); + assert(is_string($application->zip)); + assert(is_string($application->companyId)); + assert(is_string($application->companyTaxId)); + assert(is_string($application->note)); $this->trainingApplicationStorage->insertApplication( $trainingDate->getTrainingId(), $trainingDate->getId(), diff --git a/app/src/Test/Database/Database.php b/app/src/Test/Database/Database.php index 846032e37..224e32449 100644 --- a/app/src/Test/Database/Database.php +++ b/app/src/Test/Database/Database.php @@ -24,10 +24,10 @@ class Database extends Explorer private int $insertIdsPosition = 0; - /** @var array> */ + /** @var array> */ private array $queriesScalarParams = []; - /** @var array>> */ + /** @var array>> */ private array $queriesArrayParams = []; /** @var array */ @@ -155,14 +155,14 @@ public function query(string $sql, ...$params): ResultSet * For example datetime is stored without timezone info. * The DateTime format here is the same as in \Nette\Database\Drivers\MySqlDriver::formatDateTime() but without the quotes. */ - private function formatValue(string|int|bool|DateTimeInterface|null $value): string|int|bool|null + private function formatValue(string|int|float|bool|DateTimeInterface|null $value): string|int|float|bool|null { return $value instanceof DateTimeInterface ? $value->format(DateTimeFormat::MYSQL) : $value; } /** - * @return array + * @return array */ public function getParamsForQuery(string $query): array { @@ -171,7 +171,7 @@ public function getParamsForQuery(string $query): array /** - * @return array> + * @return array> */ public function getParamsArrayForQuery(string $query): array { diff --git a/app/src/Test/Training/TrainingTestDataFactory.php b/app/src/Test/Training/TrainingTestDataFactory.php index b4aa0ce67..31126e200 100644 --- a/app/src/Test/Training/TrainingTestDataFactory.php +++ b/app/src/Test/Training/TrainingTestDataFactory.php @@ -29,12 +29,14 @@ public function getTrainingApplication( bool $familiar = false, TrainingApplicationStatus $status = TrainingApplicationStatus::Attended, ?int $dateId = null, + string $trainingAction = 'action', string $trainingName = 'Training Name', ?DateTime $trainingStart = null, ?DateTime $trainingEnd = null, bool $remote = false, ?string $remoteUrl = null, ?string $feedbackHref = null, + string $sourceAlias = 'michal-spacek', ): TrainingApplication { return new TrainingApplication( $this->applicationStatuses, @@ -59,7 +61,7 @@ public function getTrainingApplication( false, $dateId, null, - 'action', + $trainingAction, Html::fromText($trainingName), $trainingStart, $trainingEnd, @@ -83,7 +85,7 @@ public function getTrainingApplication( null, null, 'accessToken', - 'michal-spacek', + $sourceAlias, 'Michal Špaček', 'MŠ', ); diff --git a/app/tests/Form/Pulse/PasswordsStorageAlgorithmFormFactoryTest.phpt b/app/tests/Form/Pulse/PasswordsStorageAlgorithmFormFactoryTest.phpt new file mode 100644 index 000000000..278e00763 --- /dev/null +++ b/app/tests/Form/Pulse/PasswordsStorageAlgorithmFormFactoryTest.phpt @@ -0,0 +1,226 @@ +database->addFetchAllResult([ + [ + 'id' => 5, + 'name' => '', + 'tradeName' => '', + 'alias' => '', + 'sortName' => '', + ], + ]); + // Sites + $this->database->addFetchAllResult([ + [ + 'id' => 6, + 'url' => '', + 'alias' => '', + ], + ]); + // Algorithms + $this->database->addFetchAllResult([ + [ + 'id' => 7, + 'algo' => 'bcrypt', + 'alias' => 'bcrypt', + 'salted' => 1, + 'stretched' => 1, + ], + ]); + // Disclosures + $this->database->addFetchAllResult([ + [ + 'id' => 8, + 'alias' => '', + 'type' => '', + ], + ]); + $this->form = $formFactory->create( + function (): void { + }, + 1, + ); + $applicationPresenter->anchorForm($this->form); + } + + + #[Override] + protected function tearDown(): void + { + $this->database->reset(); + $this->form->cleanErrors(); + } + + + public function testCreateOnValidateSpecificSitesExist(): void + { + // For Passwords::getStoragesByCompanyId() + $this->database->addFetchAllResult([ + [ + 'companyId' => 1, + 'companyName' => '', + 'tradeName' => null, + 'sortName' => '', + 'companyAlias' => '', + 'siteId' => 2, + 'siteUrl' => '', + 'siteAlias' => '', + 'sharedWith' => null, + 'algoId' => 10, + 'algoAlias' => 'argon2', + 'algoName' => 'Argon2', + 'algoSalted' => 1, + 'algoStretched' => 1, + 'from' => new DateTime(), + 'fromConfirmed' => 1, + 'disclosureId' => 121, + 'disclosureUrl' => '', + 'disclosureArchive' => '', + 'disclosureNote' => null, + 'disclosurePublished' => new DateTime(), + 'disclosureAdded' => null, + 'disclosureTypeAlias' => 'faq', + 'disclosureType' => 'FAQ', + 'attributes' => '', + 'note' => null, + ], + ]); + $this->form->setDefaults([ + 'company' => [ + 'id' => 5, + ], + 'site' => [ + 'id' => Sites::ALL, + ], + ]); + Arrays::invoke($this->form->onValidate, $this->form); + Assert::same(["Invalid combination, can't add disclosure for all sites when sites already exist"], $this->form->getErrors()); + } + + + public function testCreateOnValidateSiteAssigned(): void + { + // For Passwords::getStoragesByCompanyId() + $this->database->addFetchAllResult([]); + $this->form->setDefaults([ + 'company' => [ + 'id' => 5, + ], + 'site' => [ + 'id' => 6, + ], + ]); + Arrays::invoke($this->form->onValidate, $this->form); + Assert::same(['Invalid combination, the site is already assigned to different company'], $this->form->getErrors()); + } + + + public function testCreateOnValidateDuplicateCompany(): void + { + // For Companies::getByName() + $this->database->addFetchResult([ + 'id' => 9, + 'name' => '', + 'tradeName' => '', + 'alias' => '', + 'sortName' => '', + ]); + $this->form->setDefaults([ + 'company' => [ + 'new' => [ + 'name' => 'Nouveau', + ], + ], + ]); + Arrays::invoke($this->form->onValidate, $this->form); + Assert::same(["Can't add new company, duplicated name"], $this->form->getErrors()); + } + + + public function testCreateOnValidateDuplicateUrl(): void + { + // For Companies::getByName() + $this->database->addFetchResult([]); + // For Sites::getByUrl() + $this->database->addFetchResult([ + 'id' => 10, + 'url' => '', + 'alias' => '', + ]); + $this->form->setDefaults([ + 'company' => [ + 'new' => [ + 'name' => 'Nouveau', + ], + ], + 'site' => [ + 'new' => [ + 'url' => 'https://www.example/', + ], + ], + ]); + Arrays::invoke($this->form->onValidate, $this->form); + Assert::same(["Can't add new site, duplicated URL"], $this->form->getErrors()); + } + + + public function testCreateOnValidateDuplicateAlgo(): void + { + // For Companies::getByName() + $this->database->addFetchResult([]); + // For PasswordHashingAlgorithms::getAlgorithmByName() + $this->database->addFetchResult([ + 'id' => 11, + 'algo' => '', + 'alias' => '', + 'salted' => 1, + 'stretched' => 1, + ]); + $this->form->setDefaults([ + 'company' => [ + 'new' => [ + 'name' => 'Nouveau Deux', + ], + ], + 'algo' => [ + 'new' => [ + 'algoName' => 'Algo Land', + ], + ], + ]); + Arrays::invoke($this->form->onValidate, $this->form); + Assert::same(["Can't add new algorithm, duplicated name"], $this->form->getErrors()); + } + +} + +TestCaseRunner::run(PasswordsStorageAlgorithmFormFactoryTest::class); diff --git a/app/tests/Form/TrainingApplicationAdminFormFactoryTest.phpt b/app/tests/Form/TrainingApplicationAdminFormFactoryTest.phpt new file mode 100644 index 000000000..156109b5b --- /dev/null +++ b/app/tests/Form/TrainingApplicationAdminFormFactoryTest.phpt @@ -0,0 +1,106 @@ +database->addFetchPairsResult([ + 'foo-bar' => 'Foo Bar', + ]); + // For TrainingApplicationStatuses::setStatus() => TrainingApplicationSources::getSourceId() + $this->database->addFetchFieldResult(12); + // For UpcomingTrainingDates::getPublicUpcoming() => UpcomingTrainingDates::getUpcoming() + $this->database->addFetchAllResult([ + [ + 'dateId' => self::TRAINING_DATE_ID, + 'trainingId' => 1, + 'action' => self::TRAINING_ACTION, + 'name' => 'Name', + 'price' => 2600, + 'studentDiscount' => null, + 'hasCustomPrice' => 0, + 'hasCustomStudentDiscount' => 0, + 'start' => new DateTime('+1 day'), + 'end' => new DateTime('+2 days'), + 'labelJson' => '{"cs_CZ": "lej-bl", "en_US": "la-bel"}', + 'public' => 1, + 'status' => TrainingDateStatus::Confirmed->value, + 'remote' => 0, + 'remoteUrl' => null, + 'remoteNotes' => null, + 'venueId' => 1, + 'venueAction' => 'venue-1', + 'venueHref' => 'https://venue.example', + 'venueName' => 'Venue name', + 'venueNameExtended' => 'Venue name extended', + 'venueAddress' => 'Address', + 'venueCity' => 'City', + 'venueDescription' => 'Venue **description**', + 'cooperationId' => 1, + 'cooperationDescription' => 'Co-op', + 'videoHref' => 'https://video.example', + 'feedbackHref' => 'https://feedback.example', + 'note' => 'Not-E', + ], + ]); + + $form = $this->formFactory->create( + function (?int $dateId): void { + $this->result = $dateId; + }, + function (): void { + }, + $this->dataFactory->getTrainingApplication(12, dateId: null, trainingAction: self::TRAINING_ACTION, sourceAlias: 'foo-bar'), + ); + $this->applicationPresenter->anchorForm($form); + $form->setDefaults([ + 'date' => self::TRAINING_DATE_ID, + 'familiar' => true, + 'country' => 'cz', + 'price' => 13.37, + 'priceVat' => 14.47, + ]); + Arrays::invoke($form->onSuccess, $form); + Assert::same(self::TRAINING_DATE_ID, $this->result); + $params = $this->database->getParamsArrayForQuery('UPDATE training_applications SET ? WHERE id_application = ?'); + Assert::true($params[0]['familiar']); + Assert::same(self::TRAINING_DATE_ID, $params[0]['key_date']); + } + +} + +TestCaseRunner::run(TrainingApplicationAdminFormFactoryTest::class); diff --git a/app/tests/Form/TrainingApplicationMultipleFormFactoryTest.phpt b/app/tests/Form/TrainingApplicationMultipleFormFactoryTest.phpt new file mode 100644 index 000000000..969cc5db3 --- /dev/null +++ b/app/tests/Form/TrainingApplicationMultipleFormFactoryTest.phpt @@ -0,0 +1,123 @@ +database->addFetchPairsResult([ + self::NEW_STATUS_ID => TrainingApplicationStatus::Attended->value, + ]); + // For TrainingApplicationSources::getAll() + $this->database->addFetchPairsResult([ + 'foo-bar' => 'Foo Bar', + ]); + // For TrainingApplicationStatuses::getStatusId() + $this->database->addFetchFieldResult(self::NEW_STATUS_ID); + // For TrainingApplicationSources::getSourceId() + $this->database->addFetchFieldResult(11); + // For TrainingApplicationStatuses::setStatus() => TrainingApplicationSources::getSourceId() + $this->database->addFetchFieldResult(12); + // For TrainingApplicationStatuses::setStatus() + $dateTime = new DateTime(); + $this->database->addFetchResult([ + 'statusId' => 17, + 'statusTime' => $dateTime, + 'statusTimeTimeZone' => $dateTime->getTimezone()->getName(), + ]); + + $form = $this->formFactory->create( + function (int $dateId): void { + $this->result = $dateId; + }, + $this->buildTrainingDate(), + ); + $form->setDefaults([ + 'status' => TrainingApplicationStatus::Attended->value, + 'source' => 'foo-bar', + 'country' => 'cz', + ]); + $this->applicationPresenter->anchorForm($form); + Arrays::invoke($form->onSuccess, $form); + Assert::same(self::TRAINING_DATE_ID, $this->result); + $params = $this->database->getParamsArrayForQuery('INSERT INTO training_applications'); + Assert::same(self::TRAINING_DATE_ID, $params[0]['key_date']); + Assert::same(self::NEW_STATUS_ID, $params[0]['key_status']); + } + + + private function buildTrainingDate(): TrainingDate + { + return new TrainingDate( + self::TRAINING_DATE_ID, + '', + 10, + true, + false, + new DateTime('2024-10-20 10:00:00'), + new DateTime('2024-10-21 18:00:00'), + null, + null, + true, + TrainingDateStatus::Confirmed, + '', + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + false, + null, + false, + null, + null, + null, + null, + ); + } + +} + +TestCaseRunner::run(TrainingApplicationMultipleFormFactoryTest::class); From 5afea806373802f2fa2640fc44450edc1c490408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Thu, 19 Dec 2024 04:11:43 +0100 Subject: [PATCH 8/9] Add Nette\Http\FileUpload::getImageSize() stub with return type narrowed PR'd https://github.com/phpstan/phpstan-nette/pull/166 and this can be removed once the PR is merged and deployed. --- app/phpstan.neon | 2 ++ app/stubs/Nette/Http/FileUpload.phpstub | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 app/stubs/Nette/Http/FileUpload.phpstub diff --git a/app/phpstan.neon b/app/phpstan.neon index ac9774a51..b084dcc3d 100644 --- a/app/phpstan.neon +++ b/app/phpstan.neon @@ -10,6 +10,8 @@ parameters: - phpt level: 9 checkMissingOverrideMethodAttribute: true + stubFiles: + - stubs/Nette/Http/FileUpload.phpstub includes: - phar://phpstan.phar/conf/bleedingEdge.neon diff --git a/app/stubs/Nette/Http/FileUpload.phpstub b/app/stubs/Nette/Http/FileUpload.phpstub new file mode 100644 index 000000000..370f5fc73 --- /dev/null +++ b/app/stubs/Nette/Http/FileUpload.phpstub @@ -0,0 +1,18 @@ + Date: Thu, 19 Dec 2024 04:16:24 +0100 Subject: [PATCH 9/9] Go back to PHPStan level max A few weeks and +10% test coverage later. --- app/phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/phpstan.neon b/app/phpstan.neon index b084dcc3d..6776789a5 100644 --- a/app/phpstan.neon +++ b/app/phpstan.neon @@ -8,7 +8,7 @@ parameters: fileExtensions: - php - phpt - level: 9 + level: max checkMissingOverrideMethodAttribute: true stubFiles: - stubs/Nette/Http/FileUpload.phpstub