From 296d0e52b1dd3a0cbeceab39ca0adf56fe83da53 Mon Sep 17 00:00:00 2001 From: IanM Date: Tue, 12 Dec 2023 18:06:06 +0000 Subject: [PATCH 1/5] fix: rank image fixes --- extend.php | 18 +------ js/src/forum/components/RankingImage.tsx | 21 ++++++++ js/src/forum/components/RankingsPage.js | 16 +----- resources/less/forum/extension.less | 4 -- src/Api/AddForumAttributes.php | 50 +++++++++++++++++++ .../Controllers/DeleteTopImageController.php | 19 +++---- .../Controllers/UploadTopImageController.php | 42 ++++++++-------- 7 files changed, 105 insertions(+), 65 deletions(-) create mode 100644 js/src/forum/components/RankingImage.tsx create mode 100644 src/Api/AddForumAttributes.php diff --git a/extend.php b/extend.php index bcc6b04a..10bf8c4e 100644 --- a/extend.php +++ b/extend.php @@ -107,11 +107,7 @@ (new Extend\ApiSerializer(Serializer\ForumSerializer::class)) ->hasMany('ranks', Serializers\RankSerializer::class) - ->attributes(function (Serializer\ForumSerializer $serializer, $forum, $attributes) { - $attributes['canViewRankingPage'] = $serializer->getActor()->can('fof.gamification.viewRankingPage'); - - return $attributes; - }), + ->attributes(Api\AddForumAttributes::class), (new Extend\ApiController(Controller\ShowForumController::class)) ->prepareDataForSerialization(function (Controller\ShowForumController $controller, &$data) { @@ -122,17 +118,7 @@ ->default('fof-gamification.blockedUsers', '') ->default('fof-gamification.rankAmt', 2) ->default('fof-gamification.firstPostOnly', false) - ->default('fof-gamification.allowSelfVotes', true) - ->serializeToForum('fof-gamification.topimage1Url', 'fof-gamification.topimage1_path', function ($value) { - return $value ? "/assets/$value" : null; - }) - ->serializeToForum('fof-gamification.topimage2Url', 'fof-gamification.topimage2_path', function ($value) { - return $value ? "/assets/$value" : null; - }) - ->serializeToForum('fof-gamification.topimage3Url', 'fof-gamification.topimage3_path', function ($value) { - return $value ? "/assets/$value" : null; - }) - ->serializeToForum('fof-gamification-op-votes-only', 'fof-gamification.firstPostOnly', 'boolVal'), + ->default('fof-gamification.allowSelfVotes', true), (new Extend\ApiSerializer(Serializer\UserSerializer::class)) ->attributes(AddUserAttributes::class), diff --git a/js/src/forum/components/RankingImage.tsx b/js/src/forum/components/RankingImage.tsx new file mode 100644 index 00000000..a968d265 --- /dev/null +++ b/js/src/forum/components/RankingImage.tsx @@ -0,0 +1,21 @@ +import app from 'flarum/forum/app'; +import Component, { ComponentAttrs } from 'flarum/common/Component'; +import type Mithril from 'mithril'; +import icon from 'flarum/common/helpers/icon'; + +interface RankingImageAttrs extends ComponentAttrs { + place: number; +} + +export default class RankingImage extends Component { + view() { + const imgUrl = app.forum.attribute(`fof-gamification.topimage${this.attrs.place}Url`); + const place = this.attrs.place; + + return imgUrl ? ( + + ) : ( + {icon('fas fa-trophy')} + ); + } +} diff --git a/js/src/forum/components/RankingsPage.js b/js/src/forum/components/RankingsPage.js index 8f7d709e..bcab9505 100755 --- a/js/src/forum/components/RankingsPage.js +++ b/js/src/forum/components/RankingsPage.js @@ -6,9 +6,8 @@ import Button from 'flarum/common/components/Button'; import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; import listItems from 'flarum/common/helpers/listItems'; import username from 'flarum/common/helpers/username'; -import icon from 'flarum/common/helpers/icon'; -import setting from '../helpers/setting'; import Link from 'flarum/common/components/Link'; +import RankingImage from './RankingImage'; /** * This page re-uses Flarum's IndexPage CSS classes @@ -60,18 +59,7 @@ export default class RankingsPage extends Page { ++i; return [ - {i < 4 ? ( - setting('customRankingImages', true) ? ( - - ) : ( - {icon('fas fa-trophy')} - ) - ) : ( - {this.addOrdinalSuffix(i)} - )} + {i < 4 ? : {this.addOrdinalSuffix(i)}}

diff --git a/resources/less/forum/extension.less b/resources/less/forum/extension.less index 3363f273..3f70bef9 100755 --- a/resources/less/forum/extension.less +++ b/resources/less/forum/extension.less @@ -72,10 +72,6 @@ } } -.rankings-mobile { - width: 215px; -} - .CommentPost-votes { .Post-points { background-color: transparent; diff --git a/src/Api/AddForumAttributes.php b/src/Api/AddForumAttributes.php new file mode 100644 index 00000000..16509282 --- /dev/null +++ b/src/Api/AddForumAttributes.php @@ -0,0 +1,50 @@ +settings = $settings; + $this->uploadDir = $factory->disk('flarum-assets'); + } + + public function __invoke(ForumSerializer $serializer, $model, array $attributes): array + { + $attributes['canViewRankingPage'] = $serializer->getActor()->can('fof.gamification.viewRankingPage'); + $attributes['fof-gamification-op-votes-only'] = (bool) $this->settings->get('fof-gamification.firstPostOnly'); + + $attributes['fof-gamification.topimage1Url'] = $this->urlForKey('fof-gamification.topimage1_path'); + $attributes['fof-gamification.topimage2Url'] = $this->urlForKey('fof-gamification.topimage2_path'); + $attributes['fof-gamification.topimage3Url'] = $this->urlForKey('fof-gamification.topimage3_path'); + + return $attributes; + } + + protected function urlForKey(string $key): ?string + { + $value = $this->settings->get($key); + + if ($value === null) { + return null; + } + + return $this->uploadDir->url($value); + } +} diff --git a/src/Api/Controllers/DeleteTopImageController.php b/src/Api/Controllers/DeleteTopImageController.php index a90356a3..41e35cb0 100755 --- a/src/Api/Controllers/DeleteTopImageController.php +++ b/src/Api/Controllers/DeleteTopImageController.php @@ -12,13 +12,12 @@ namespace FoF\Gamification\Api\Controllers; use Flarum\Api\Controller\AbstractDeleteController; -use Flarum\Foundation\Paths; use Flarum\Http\RequestUtil; use Flarum\Settings\SettingsRepositoryInterface; +use Illuminate\Contracts\Filesystem\Cloud; +use Illuminate\Contracts\Filesystem\Factory; use Illuminate\Support\Arr; use Laminas\Diactoros\Response\EmptyResponse; -use League\Flysystem\Adapter\Local; -use League\Flysystem\Filesystem; use Psr\Http\Message\ServerRequestInterface; class DeleteTopImageController extends AbstractDeleteController @@ -29,14 +28,14 @@ class DeleteTopImageController extends AbstractDeleteController protected $settings; /** - * @var Paths + * @var Cloud */ - protected $paths; + protected $uploadDir; - public function __construct(SettingsRepositoryInterface $settings, Paths $paths) + public function __construct(SettingsRepositoryInterface $settings, Factory $factory) { $this->settings = $settings; - $this->paths = $paths; + $this->uploadDir = $factory->disk('flarum-assets'); } protected function delete(ServerRequestInterface $request) @@ -49,10 +48,8 @@ protected function delete(ServerRequestInterface $request) $this->settings->set($key, null); - $uploadDir = new Filesystem(new Local($this->paths->public.'/assets')); - - if ($uploadDir->has($path)) { - $uploadDir->delete($path); + if ($this->uploadDir->exists($path)) { + $this->uploadDir->delete($path); } return new EmptyResponse(204); diff --git a/src/Api/Controllers/UploadTopImageController.php b/src/Api/Controllers/UploadTopImageController.php index 756be8d7..f210a247 100755 --- a/src/Api/Controllers/UploadTopImageController.php +++ b/src/Api/Controllers/UploadTopImageController.php @@ -15,12 +15,11 @@ use Flarum\Foundation\Paths; use Flarum\Http\RequestUtil; use Flarum\Settings\SettingsRepositoryInterface; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; +use Illuminate\Contracts\Filesystem\Cloud; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Intervention\Image\ImageManager; -use League\Flysystem\Adapter\Local; -use League\Flysystem\Filesystem; -use League\Flysystem\MountManager; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -41,11 +40,17 @@ class UploadTopImageController extends ShowForumController */ protected $imageManager; - public function __construct(SettingsRepositoryInterface $settings, Paths $paths, ImageManager $imageManager) + /** + * @var Cloud + */ + protected $uploadDir; + + public function __construct(SettingsRepositoryInterface $settings, Paths $paths, ImageManager $imageManager, FilesystemFactory $factory) { $this->settings = $settings; $this->paths = $paths; $this->imageManager = $imageManager; + $this->uploadDir = $factory->disk('flarum-assets'); } public function data(ServerRequestInterface $request, Document $document) @@ -60,31 +65,28 @@ public function data(ServerRequestInterface $request, Document $document) $file->moveTo($tmpFile); if ('1' == $id) { - $size = 125; + $size = 200; } elseif ('2' == $id) { - $size = 75; + $size = 150; } else { - $size = 50; + $size = 100; } - $encodedImage = $this->imageManager->make($tmpFile)->resize($size, $size)->encode('png'); - file_put_contents($tmpFile, $encodedImage); + $image = $this->imageManager->make($tmpFile); - $extension = 'png'; - - $mount = new MountManager([ - 'source' => new Filesystem(new Local(pathinfo($tmpFile, PATHINFO_DIRNAME))), - 'target' => new Filesystem(new Local($this->paths->public.'/assets')), - ]); - - if (($path = $this->settings->get($key = "fof-gamification.topimage{$id}_path")) && $mount->has($file = "target://$path")) { - $mount->delete($file); + if (extension_loaded('exif')) { + $image->orientate(); } - $uploadName = 'topimage-'.Str::lower(Str::random(8)).'.'.$extension; + $encodedImage = $image->fit($size, $size)->encode('png'); + + - $mount->move('source://'.pathinfo($tmpFile, PATHINFO_BASENAME), "target://$uploadName"); + $key = "fof-gamification.topimage{$id}_path"; + $uploadName = 'topimage-'.Str::lower(Str::random(8)).'.png'; + $this->uploadDir->put($uploadName, $encodedImage); + $this->settings->set($key, $uploadName); return parent::data($request, $document); From 9b7d98e4206b58f6c8593c41e17d620432d86520 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 12 Dec 2023 18:06:34 +0000 Subject: [PATCH 2/5] Apply fixes from StyleCI --- src/Api/AddForumAttributes.php | 9 +++++++++ src/Api/Controllers/UploadTopImageController.php | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Api/AddForumAttributes.php b/src/Api/AddForumAttributes.php index 16509282..99a9fd96 100644 --- a/src/Api/AddForumAttributes.php +++ b/src/Api/AddForumAttributes.php @@ -1,5 +1,14 @@ fit($size, $size)->encode('png'); - - $key = "fof-gamification.topimage{$id}_path"; $uploadName = 'topimage-'.Str::lower(Str::random(8)).'.png'; $this->uploadDir->put($uploadName, $encodedImage); - + $this->settings->set($key, $uploadName); return parent::data($request, $document); From efbcc39f05d756a82aec354ac6895de54fa89bfa Mon Sep 17 00:00:00 2001 From: IanM Date: Tue, 12 Dec 2023 19:09:59 +0000 Subject: [PATCH 3/5] simple upload topimage test --- .github/workflows/backend.yml | 2 +- .gitignore | 10 +-- composer.json | 26 +++++- .../Controllers/UploadTopImageController.php | 38 ++++++-- tests/EnhancedTestCase.php | 81 ++++++++++++++++++ tests/fixtures/.gitkeep | 0 tests/fixtures/topimage.png | Bin 0 -> 40335 bytes tests/integration/api/TopImageTest.php | 76 ++++++++++++++++ tests/integration/setup.php | 16 ++++ tests/phpunit.integration.xml | 25 ++++++ tests/phpunit.unit.xml | 27 ++++++ tests/unit/.gitkeep | 0 12 files changed, 280 insertions(+), 21 deletions(-) create mode 100644 tests/EnhancedTestCase.php create mode 100644 tests/fixtures/.gitkeep create mode 100644 tests/fixtures/topimage.png create mode 100644 tests/integration/api/TopImageTest.php create mode 100644 tests/integration/setup.php create mode 100644 tests/phpunit.integration.xml create mode 100644 tests/phpunit.unit.xml create mode 100644 tests/unit/.gitkeep diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 04b510cf..55443cf3 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -6,7 +6,7 @@ jobs: run: uses: flarum/framework/.github/workflows/REUSABLE_backend.yml@1.x with: - enable_backend_testing: false + enable_backend_testing: true enable_phpstan: true backend_directory: . diff --git a/.gitignore b/.gitignore index 55f7d837..fbaf0d88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,4 @@ -/vendor -composer.phar -.DS_Store -Thumbs.db +vendor node_modules -bower_components -.idea -.floo* -js/dist composer.lock +.phpunit.result.cache diff --git a/composer.json b/composer.json index f06f7130..6c28d518 100644 --- a/composer.json +++ b/composer.json @@ -58,19 +58,37 @@ }, "flarum-cli": { "modules": { - "githubActions": true + "githubActions": true, + "backendTesting": true } } }, "scripts": { "analyse:phpstan": "phpstan analyse", - "clear-cache:phpstan": "phpstan clear-result-cache" + "clear-cache:phpstan": "phpstan clear-result-cache", + "test": [ + "@test:unit", + "@test:integration" + ], + "test:unit": "phpunit -c tests/phpunit.unit.xml", + "test:integration": "phpunit -c tests/phpunit.integration.xml", + "test:setup": "@php tests/integration/setup.php" }, "scripts-descriptions": { - "analyse:phpstan": "Run static analysis" + "analyse:phpstan": "Run static analysis", + "test": "Runs all tests.", + "test:unit": "Runs all unit tests.", + "test:integration": "Runs all integration tests.", + "test:setup": "Sets up a database for use with integration tests. Execute this only once." }, "require-dev": { "flarum/phpstan": "*", - "flarum/pusher": "*" + "flarum/pusher": "*", + "flarum/testing": "^1.0.0" + }, + "autoload-dev": { + "psr-4": { + "FoF\\Gamification\\Tests\\": "tests/" + } } } diff --git a/src/Api/Controllers/UploadTopImageController.php b/src/Api/Controllers/UploadTopImageController.php index 25860faa..364e95f3 100755 --- a/src/Api/Controllers/UploadTopImageController.php +++ b/src/Api/Controllers/UploadTopImageController.php @@ -13,6 +13,7 @@ use Flarum\Api\Controller\ShowForumController; use Flarum\Foundation\Paths; +use Flarum\Foundation\ValidationException; use Flarum\Http\RequestUtil; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Filesystem\Cloud; @@ -20,6 +21,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use Intervention\Image\ImageManager; +use Laminas\Diactoros\UploadedFile; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -57,19 +59,37 @@ public function data(ServerRequestInterface $request, Document $document) { RequestUtil::getActor($request)->assertAdmin(); - $id = Arr::get($request->getQueryParams(), 'id'); + $id = (int) Arr::get($request->getQueryParams(), 'id', 0); + /** @var UploadedFile | null */ $file = Arr::first($request->getUploadedFiles()); - $tmpFile = tempnam($this->paths->storage.'/tmp', 'topimage.'.$id); + if (!$file) { + throw new ValidationException(['file' => 'No file was uploaded']); + } + + if (!$file instanceof UploadedFile) { + if (is_array($file)) { + $file = Arr::first($file); + + } else { + throw new ValidationException(['file' => 'Not an UploadFile instance']); + } + } + + $tmpFile = @tempnam($this->paths->storage.'/tmp', 'topimage.'.$id); $file->moveTo($tmpFile); - if ('1' == $id) { - $size = 200; - } elseif ('2' == $id) { - $size = 150; - } else { - $size = 100; + switch ($id) { + case 1: + $size = 150;; + break; + case 2: + $size = 125; + break; + default: + $size = 100; + break; } $image = $this->imageManager->make($tmpFile); @@ -87,6 +107,8 @@ public function data(ServerRequestInterface $request, Document $document) $this->settings->set($key, $uploadName); + unlink($tmpFile); + return parent::data($request, $document); } } diff --git a/tests/EnhancedTestCase.php b/tests/EnhancedTestCase.php new file mode 100644 index 00000000..4c3dc569 --- /dev/null +++ b/tests/EnhancedTestCase.php @@ -0,0 +1,81 @@ +requestWithMultipart($method, $path, $options); + } + + // Otherwise, use the parent implementation + return parent::request($method, $path, $options); + } + + protected function requestWithMultipart(string $method, string $path, array $options): ServerRequestInterface + { + $uploadedFiles = []; + foreach ($options['multipart'] as $fileData) { + if (!is_string($fileData['contents'])) { + throw new \InvalidArgumentException("The 'contents' must be a string file path."); + } + + $stream = new Stream(fopen($fileData['contents'], 'r+')); + $uploadedFile = new UploadedFile( + $stream, + $stream->getSize(), + UPLOAD_ERR_OK, + $fileData['filename'], + $fileData['type'] ?? 'application/octet-stream' + ); + + $uploadedFiles['files'][] = $uploadedFile; + } + + $request = new ServerRequest([], $uploadedFiles, $path, $method); + + // Do we want a JSON request body? + if (isset($options['json'])) { + $request = $this->requestWithJsonBody( + $request, + $options['json'] + ); + } + + // Authenticate as a given user + if (isset($options['authenticatedAs'])) { + $request = $this->requestAsUser( + $request, + $options['authenticatedAs'] + ); + } + + return $request; + } + + protected function uploadFile(string $path) + { + if (!file_exists($path)) { + throw new \InvalidArgumentException("File not found at path: $path"); + } + + return [ + 'contents' => $path, + 'filename' => basename($path), + ]; + } + + protected function fixtures(string $file): string + { + return __DIR__.'/fixtures/'.$file; + } +} diff --git a/tests/fixtures/.gitkeep b/tests/fixtures/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/topimage.png b/tests/fixtures/topimage.png new file mode 100644 index 0000000000000000000000000000000000000000..4903fef0282c8b765c88226f24c91e80fff4d468 GIT binary patch literal 40335 zcmeFZWmuGJ-#$u*NJ|T-q=1A-$Iu8Uph%Z=4k-;Ygp?8@sYr{&kRqYd-Q6vn(lRp) z!~Y(5p7-6K_Wrt$ee4hGSWA!ly6@}y<$0dJYgV+jrYh+z##>leSfpxCm0n_D;TT~4 zAtC_&rqMQS9{3lw=Sx)utg^wo+gMm~SZYf0uY9rh8VqB=1Advc{#Vam;BvgX(_xUp ztQbX){#X?Kk)z|WrCoir0FKm8!Mmz)VR&);x5bGl*yuv=+U0`p$*ti1B#ITMzMV!j zAfHR1W6AB5T^gOou1?z_zXlpQaYK6^H!NxE24pN;yg4_7X`TPI%NT<>>Q2-~`ZSz1 z9F$f2pO3gMT>Ud1D|I;cwjSU*(`|1QxZv zpSxARfj;Oc?v-MNi5O;ey!-1E&u-Qrvk#yV^3xN+)3JjVMy+qq;$w1y-rxuRzIa#T z49^=(gJi#j$p^lsx`hKdj6|^Eb+aZ&gSF6a1_bX81M7w_+q(8@iA1&&cR!=SOmoE$ zYql_5GqMGd#S$`Un-%1-Xjh} zZI8=AaU*Qw-uO-&M=q`LE9^eD`28hs)w!xw>&;n_Vi599{!8Q7QXzHPiEj`P3js`B z8cv!N4DAOUYiSZ4b0P2ZjF(Dn6;ymktQb(&Eq@0UKNes-(`WhZ%T{wci&xh%R&4wN z=Hwi|h!g62sACibUW2KfPD}K(a;($O@l# z-{QF$$GP+IcP$s_8QN^EY;(W8YmE>DZ$tI4+ds6*rkwpR?YPXS&q1KAi#EwB1UT^1!!Y zItsypb5Ji235wy|-iVKhlxa%Y63t1=OQa5Yc{5>d9Oo~|9OEkRHHQ?gW z$t+|+hnGS!IjE1?iS&hzVKsd|LQWcv;1bD;m>#KJR(ESZ!3X?HFQcw3#kCy&3bUK7 z8R6;drBvAG-tHn19^`OV-)E4h{1!y#yQW1xC}Y+eXHG4Sc0cc%&htQXd_2&R0u%5G zrMM^w%2A3(v#O8MT2Yi_v)!yK;iAWc2sV%Qi2>GswM2cE;TqmmcThEDhkXgR@+v=m zRX^ZZ44I)KIrPK>Qn|n`HSNw?ydc?{)+z4g4@0+QbPJl+;-|Gnuzyt>Q<-dj=dYFq zCgSQ@%A?Cf@$1B(3LA>Qw%!L>$T>c^lBGUw+wAO#D74*1(BEv$4{A#Lrq_PMw-%CKvC~dy{gUZ0 z4Ajq>MYKi^VmtH0|)kA&23z7f-3;*wJ$PLDaJ<~;rT0C(&tUFs4Etb$;!7Yl2VGdpD z&K#*1OIp@aXa~}UC`44aL=#Pc#@0yop`C|N@^_XCdJbyfsHK2?gDk%tb6N&!#G~UQG(C?OGricfWKQ3S^oVi$* zC8^kBCPlHnia4lw`;;RV3E{~h5)p%NSfy ziv1*~HuE$qrhw^MWSZP75t{V~CLgocxxJz&9d6u;3sJy9*A-E5VfrodkkL>fMi6L& z_+dlTyx~#aYg+f^(e*osvBqyLjXeuojWm)upAQiafA^a1f5r+b>6SzBe~~uaYoo&;0jqahWzN)%{Y|I)QXXuteaKwR0*UYKp}T*S9`iYRm!Fb1E{7QdjmMy zu}ZS>XTY`w^ZiqS_n~HusGlP}504$sdi5(lHhZ#hCOT6Dwlexj~M| z8{PbXfiSA}wp+YN!%X?a{GG@RP~3Vv>TTcMZI4(e&UtMKiwJT9Ys(>s}kkpnTm-(6$fqq4|A1s)iML=~rVi;+$~_*3YuYh9Nc4;Rc@ z=E6f!6QSJkPVc`(!>gvLkLgJ0hrO?Rb}( z9MbWrlYH|Iinyu=bsuUFhglR6yxIHVb{FoYwDYC(-`uUu#rzi{FLKIlyo4t@dF`gN zz3={M=OSAs1pIxU`!iK9HC1AYKsPH8s(C|%r(={)4;Df6NhC zbBLcH%2$g$)7pSswy{Nv;k~7SBiv zki#UQHuIE>!8e9tpY85D#gLWs5ggE!FXLJ;P}h$mp*;F4?AJ#{MsPm>v1CIJmmZwr z(?j!QFVfX2d=~Ui#AaV$df*AdK-^GmiiMQz4AF?mlecr>Li8dO-NltYcZ430p1Nm75U{v14t(e zY3)9ZUfo(I0}Ji=TN*^=mjRKdTF=q?_u#Lz?9C?z?7*|H(j6zt;WVofNExd##+` z@qxG);4R&X;+^x10pKPjhqpTrnkNw`d34<^#7W(z_a`ne@=;`o%}lKBkMo93J(FZi zF`~rjz}<-VP}Ja*CtFGc>QX+FZZ2!GSTQ@)WC2y_kQK#)?awmK#0Wt>n@a!jotYfm z9_bdmB=!Z!>u8y4rM>mrdqt1V?F28l75xln znIIGolOEeTIF~Qt*rEPL(t?-MF~m(*{tXh30`m4ryPi-G2A;G^?YmLjUeS5-mK0NC zlRk5Omgr~Ap%A>ozZSpt6cVodM;^$O51_I@v@{a#v*X10VB1fb@T^B&t%4I3Cz^5f zj$yZ*i|0Dp6WDuLdrO!}7`UZ?cEV$9kj=UkS+#<7T4nm6B-$*Z_;RQ2docJc-yV|Y zIxp*~T)?5Vf_v*wnDeQ0J0)0m%V2-eHemBM$~^|tR=7gBAH`e7YFn`r zP@%A=K>zzNpM93QIys@3zTu*nT0kY(+)csYDc`b|HPLnGMMK`Ir`YCZ5gxG0TBH(r zjjq1x+K$hwd}KY^H6uq(gztsT0clrnep~4MtVN(g&>B$o3BCD6tUy=S$0pYF#lB+2&rSN*(4#;81ef`P;MjZT2a}*AFBl*!Ru;GNG2^ zp^q)l@PbFcj;SvTlL=3SZ=ZRclG|47jpa9-7O+3wTfN=b0Js#EtQM^x;Vx3&xddw_I$S|#wF zP9_ct2OG|2CJ5~``I9Bj7Zw8sT&kMIks$Sh3ka?DFd==De3UyMPS<;KC+)YB##9Zv zvD?olr_EG&k6Le`-m&rgAqViaJX#5)K$k@M+l4@&Rgy+^IKK7idd^nlLKw|KhimT} zsR`nd=ZX_0T34^go>BBh;)4JIE3s3Utm!#3TXsY zbgLa~@a!@c#KOUqco-tN4Wsx>z83%C)^R~logC<;cvT@H1RX&rBu&NvWeGz4UUV_H z80Tuz^keSYP7V3|{2hJfx#`&vqiSFmF2)aSCA_10Zwt(_G zjXr1unY~zv_U?26gte=q;%x>joS~L z;7hZXuvDF(hiyq420gIRxTOdKO_lt3^Us!QlhU-`qbxyZ|0)>m<4hwTKB{h7EK@okl-G(Bj4+IS+JF?8BffP8j zU!(C9{l6{zlXN=nx}CD1$YDD>X)ZUfPh;+Hx8GoOZh%pp1fhYn_Nr)Yb#`ckU^Xk* zGCS*ceQQ|_?kCVCOG#j2-b`^cCHT*G6a;qH_y@yW6#1*Sv~YadLsfA>=w1A<91o=m zwhgsvT@2j&0J4hs0Fv<(r_Y-L4!&B)@xHW_TyoW11lrOTxB0%Bft;EC?%Qh$Vj^T6Xx@__t z0ei6o3O0p&G*<%8P@0dSDD_)IoSy;DT4)v;<9fbRnB zr7Lfp6bvOqybpO$W^ZhXcAhtxVN0%`{rls^31w-Dtxy9)4AsSj5YCW@aTH^8iwxZSWRe3!)Ywqi8tT( z&H_N8>*HG}5ez5}_n<})IneNs;y-fLtiXcHM+*8KuiBmkSFdREQY4T%KZ|?Rw%)29 zXh@v(=)r9`KKmh8LCi)JwNlQKu8Mv`f!GoD)69vd#*E&~Db0g&_V9AY5#K940k&Biu4jZ6jPw zQ+gGe4KvRi1jBQ<3w(b?-eW;YE_6l{KsCd#u0`0QPYUQQUXkz~^hcEY2!gG#M`lPp zXlF-9T(a0ypls@*r}St-zsu`8EW`J3*CZHp_UG${hJ{WS(COkSxB<@Gla)cTHU#?s z@+o-@{yndL{JYQ8%BOliPV^mjP`{*4^+3drHq75t2OHW}MMhQGC#sej|OilhA4G3U@bMTzKC=E-hj?+EZ0WT9m;;>NLV^*-qO?B75 z;QlNneDoWC=Lt!De@|Cxs3?DsFjiivfIO1X%VBM>a)Ln%m{PpH_oVPgnK4CmCnap> z?p&0sHh?XQ9*#a08cYWMoCYH0!7%)9PuV)e^@}~7t0@ zHkJl55&U{3-?dy}lYKmt#m}z?2foKvXoKT>P$gM<&%9A~gbw!)lbM|@)wZ`NcVC3n zDyN#-h-i}x>~>PqRAAdoI*+Bq6Wj;fA9;?J#n?4oeC4Zp+jKit ze?L2}hjnd;BH^n#Ri1A`X{n6OAn26Mh_~^T26J1M0vSANT2xx#-Zdmc?wbkI= z-BocwYu5>UjV20*2}A#0B)CHQ;*rl0tM8JXKWC_k~%#*o+sN{FUzXTn>iLtTqG7@{W!dQNRa{^QRoz{=$h~XkJaSN-rdS+<$A7HU6d$GqwtYXP&C$wDmoqYhe#=oi29o(A`#^fG42-( zx%(N?p4Hp4w(0S@OwIM>h59=)hewRbg5&Q}s-GyT)aT)1WXMFnmNDf4qHB^l%vZ6_ zJlCN>fMgISqRx3a)9U`S6|p+4|MgAoYT56)CUq{5R$zVY)_LV3wMH{UAcXnCgZoI< z;uiJcdtzK@U??1AiRN~9wEX}=jrRC1@nSY383Q!nyYC=mz&MXod*wZQA>MMQ8nF;p zlWq`Iojs45zv3Pq$&K%ONdWJ2ArsW2A-`PR3htTzN#6a#y2rvdH)?>msHr_*8J)FG zL+R9pyxb`2PZ3`Y!cg8La?XC%`xs2cND44(>MKF!)W=C7AHnamSUl3cn`iPTNd)Pb zvoe{zVs41_JtSFd#!!qf6hkj%0MK9e%>Dpme59c93i3$hXL51F$6Q9Qp9Q^Juz%n` z_ZmO&w%*aiARZP#sc&rZ$62+ z;MdgZqs{8p`Zv=IRI3WI{?8VcSysO&@jv-z0NH*ESHMEGGsD?r1)-sYz2eLy$K;4H zuE=jhfGQwK?&za?ItI+$IBR*l9_ZGNE-7VBr@(pWCX4kubk+W&0f(VJ`N`!*L6Qbj zl)H6dqXRr6Lt&#sRt!b6uV5#4!87{Q9qLy!8V65W?;3)R@Ux6H51rf$@X_tBqRII_ zoAhV>+7AZmJWvOQ#-m=nR~#G4)4FW-P5iwk_q8^6u}I3Xx-W!|-c-<|f?!QW8qPqo ztkqArq4kyQIPxe2#gK?|>1MSqG1?*e`bY-p_Z0avTB1C$gqn#fp#I4;=iBq$LQd6F z3W2+T+KoY7Yg*g5$OOhuR18^yNvz|+kcgkp-0o8Ezm4y0TZvx?TX%i92$pobDsA?d z(%mTySP-JPjZJhE5>9~!!j<2vjDA^PH2O#AyFKsV+K%SM<*P$sxBtAlHyM{aNmK^( z5mf#WGVMy~0@Y81Wo0OWe!BBt7Ix|eXoa?uycZQw+eW=qJ@^3kDcu(NAFaJOM|&b{md$ns-MngI!5 zkjf01MC$YIrn_^J?;CIs`EK#QZ23R-%HsO~st$~W->2JRQtCd?jpA*;4=`P4J2246 zf)lPb7eEi^o-tZUCRrR?xq57_Jxj=V=Po&~dl2#77nd9r!+ltw$KpNo$(>&a+6_92 zPDZH#*8kD!d3I)%*K@bXROG_WyDxsf;o+ZHjWSal1^Og147<+z)OyV-fx8#fR}>p+ z#lwx??kkP{^ZTT@p=Qibm_#mSUhMRqzT6fAy}pL&ijJh_;Ee8VefF#`y;z7H78)@4J{%&zMcHQHFa@>Vp%9K zt8xzNmIIONgI1|>%Dfz_&Lt$DX22y=j(8r}XV1d4IBx1|GIbI;@U5X0Tp)+dRIl7j zHoCb&KNV7`EqpNsi>M?j8P66Ols@&;wTdQBzz4Jn&rk$4!%h4F7Z280MbSMEViOZV zq&i=5s@uQ0bI?yr#YGFG-DP9GqcVXF==%ey?8;KM=~01AXekpM!i zqO}|-Nd2q--Au@yLCa`5pK4vx-{{t(q9e&^*3~x8+eaz(EkhK+AR&PtcIY>$+m5vis(wzh4 zO)0T-EI+w=Y`|!$)U8)ecA(ibPy#1jq}6I65&g01%eTO^{{sl@-;NjkxCa<=j3U~a z2w@Gl89Guqe?LCD5~<~@945TAOY*1td#5Cwvns%2xiAbiG2%G%5(8y`)Z1p2E{}&+ zxO$Wi(2Njkga3Y=Fr{v53=ym0BQ0S&L#PC9sVNSiY*+9g55EI|0COE^5cgU*R#8v# z?NztewAO%`T3pWm&|vt>x7+Z|VU)?qb#TXj6Y|C8>|u;=4n?}#$hYr;$B*Z3b?uB9 z98%w^B&GxNkgy3XvLUaBT@=mXA4_i7$Ng&@%X}O~NpkDrc=9f%kN9m0g=^l4OFmK7A zkZW2|ltE@Z`)P1xNuXh?KB+D;QQc9=#19e*ib0OEFRD1R0#od0QjNn+r>oW`;h?c4 zq}13hI^XqOq%}FtJuDnMWvt7`?ZXG7U*G=|K+71cIMbUL77OYxxWsaC-Bt=>^9*Ei zkKtN;f!Bl&KbgGRbA5hwymXl*q@R0-3D|_`yB`fQml&5c}czJ2i?(R{|cs!Bdu4oV*M)pOG>}uS zj*jy==Exmn@dkXM!KVHBTURN~=a;oc+;UAgXRYcia$ewxjWZmcfEIy-*;VKY=Xw2C zu4YSTxl|%!%KcxYu+DKciVZKd3S_T8r7Mwm_A(c}_tEC=-q2FTb)fPPeSnhcoo=#U z8qPI2-wv32Q{|uZ7|)~Bf5qR!huF`=D|XUg1()bhL*YvT$Q)6QcEJCBy>f*J{(RxS zqhEdp^($#|WH_XJ4;mLVp{B`GD|qW)No{Bl*iiSmN2{o)UP*6~^7SP%;>fWRRcPHO zQar#6lX6&>>R{ETgJ@`FlQKP5_7{k6>0^@#yS!IkWBL9+V0FphcBU}T^>V?|m)}TG z-Y1*zN3N!9<;Q7&HSR3~J0Hmk$Zvv>rx+M~Q#&&Vj~)ewF+D7OMH(c_KYK?pFiq0} z8=~;%4eI9stcjDLiV&LuD->y^x`)y5H@z4!kNR6X zs%LZdm*w}RWtaF0uIizuqy$XGQk6G=0t=hMfCc-M@RAbtHyQ0RFUV1@d+_#0>e|)n z?@EuS^}~VHLXe9NYV)@QO=E`zzRJXy#M59Cgv2MZ%;l3@dZdlGA~L)#`#685*s3c# z9oa2Guq{|ny4&G1!=jSA?P5yJLt7qPX!tWbLd;s;du|1&vF{?tb)z_lm`Z)E)_&v5{k=_TlzW)SB6Z?QPR$s9IJSO!>NvZM}JCk*+_?8HwfgnT5EF433Jvd61|$xtd=s| zpY9da>vNK|wSS*e6EC_#&3;Hy7K3t6sldg=loy2@beNScGa$Rkojm2}7ync@pF?6u zu`bbpPT1^TLFaf#t&P!ilipj%+tnk^stzSi8|U%XI#wj^%GpO06q ze@g2jc>c+Kn)Z=F@A&mC;s3oYV)zz zLoc;t%bL3%ZF^)RPZjF_=Esd^3J7=krtjy9@c^)a8Ns%4Q+Zp7vO!xz%x8AWSKnv+ zkDt7BblP}GY@T}6i|kQy=y(Tj2@Ewad7T*oBH7eoA;UzF3s8c3b(#7GW`_^n9|@$p z!J~g+3p^-(ULdE;JhcuwvnQecM)wok=_m~}Ek-n6`t&uXIVMVC$uZAS4HrZ*v)F>K zJovJ0B3}Kfl-!we%qwO6eej*5gpFzSg-aPw5Kd0IqT8=Orze+P_I)d7L?9;^V*V9+ z1mNk+W;#@*EBI$!zLo9;7j{OWpbgkLZlrrsr5-DGLN0m35|`YcTz(urxKnu|VCRTw!t3yRTrur=#7`Au~t4|%aPTyDTF-}HNv0pU)2 z$usokWqnZMZ?Ns|tB9`Zccz`AiS{tj1J=2c#VE|>Zxaa;=ojm~>q3Q(H(ZWIbD1(g zEDA@5qBKyCTI-T!_3rJ>EF>eC54=5&!{H8&=j1a9&(au0GU0L5@QhfHCLg9H->bmE z+0tLLl7A0zwQ#4zfQj*}!rO@woMQg7MR~KjwW6%Mzr|~UeoTn3vFER)pf-l}Z^+&DK!(uIqdl$3*IulbtSt9qcvmSZcKcfpxZFpbO0KJdeA!+itw6 z*qlg>T>r%%LN~nSQ}vh^3HSuoOa>|VeH_Ma8-VR+xpF(ah+dQS2oP~X31_oHK|z8d z*Zm6{yRRqtUHwN1Bk)#g$ZNSLGmgpj=T#Qq<;|yf6wxEVr0V>2Z19edQPKp=6@o(r zHs2{0LZI5o>u5xmrwZ4S6F?nI>euhW1)x(8qV`F#o4!j~4NptCyhl2OM& zmqEHkaScy{Fsp_A$u#6yeeslwt@{2VgHCgF@zm4$0nV7**}Oc zZ!M@a2~0LQE6If{M$!K*s+JIn{kI4be+_!U0QB69RM307S1c5m0zrj+$}78{yX0av zduvh|k@{Tvh508cfeZhbj4w}g!C&~p5oz=jvo`&??|oIdh44XKUaLJZ|5MN6u@FA4 zXs{o{rCCQ4{Q2KsRG{vYr!MPQi0Rq^GgIZMc9VHvediM&V%WN~@SHa0kBDwlb41Lwf zBhKO^=-!lUUM1>DlTXVrAMM8=(m}!6c?T6rS}8#A@MlJQmRyX?bxVquTgym1AxCoc zj~rB=&goUeSZ*Y}iz2JhgE^lh^O%1=R;>2biq4O#3h_;N(h^05((K5}rRBI@93#hV zC}VG9zwQX$(pRTNHhhF!EIn|JP%9zPebekUrdw(|k>BRhuB7c_D?3T70R&$%D3=o8 znn2eM2AHC@Q@Hw$DGBz7OXQ4g*Vg$8av#c5kka)g?AOv)mj?_HJ?bziwQ8sR7y5}= zf^@nt;5x0*k+bwqVQ7rP&FOF9V5*|PrN&~I&AjG|H&q-7{m;KWylo(6ZZtFc&0EaU z{R6?YQIt>M58S3pK<#*eeS_-nUHu(u+VY`t;ka5oe~nMPAHAn0dH<~4NV9*yT#ixT zTzh89(%A&2f9ELtZAH%zRfGewuTyp%`bE$I9E82 z-I=xh4|Wb-oriNX?UFzi+xJ|U*Ay<{M6H;${0KoG?TxuC&Q1$M7AuNFZHP( zJ7yg{U?)cUt=erJ7rJ%xh;oOha(nJXZJ3U~8pLbV?8R^DrX96Kq$A;he)?UbGR*Io zd{(p)GXI0_G0foaSCA7jwN7a(lRK5L!$nx_uXo>5j#X8{y|(WQ<+Q!89u7(Y5}#G8 z142jo=A`(P_V022rER6cJQZu14fAPL>q#%o`>*a^I3{B6=o69p`ve#TQ;iyhtB-wk z%)sl2DiP7Mit-_1bK zo9^!JWR?-8^pP`VTLnl=ZZ^FTA2#EHb^4beqg>O1v+M&^(6>>!w=jgT?>+@9V)tlR z;Pm1F(0eo;0~N${9bduT?K~IDuk@9bST9ERTBEokwdyWpH~6v ze%Hs`9_&Ti2#Y;?{9In;1Zhi~%il)9Ya~K=Eb{A<{aQ6}*Xn=|^-%T&CLfwdiL%X& zS`?c)V8?t`LEx*^r7&Hs=`y{p(PXoi!p|Cw)&;)TM%TOVtaMP9S0#+Qtw;&l@EeR{nTA$8?j%eMUB94@O%6@-jca|WyKBqm$i?Am zh1Y=p&A)CAU-!rLEn2ae(s~t^F=k38ZKbR98ucF@J`OYY?MtiYI_koK|{usgpv(Z>5A8of|~kd$H;<7&|#lx#`wI^FBR*FtHx?T~uhc zu)0TH*A_j1W+*ffLOm1E9gz3halPo>a_TwgiDo0>UD$;2r`2&op~t4W0C68|%X-c9 zKN3gZ8#T$YHNK$Q(w(hyB-(~;c-O<6*Gt9v$MM_lqv~G};|k3rNWcf+Vw~oe_aNEz zng1k5wz+8)uPTXySk~5gF%k^jlL*O7b;#YD8Bm;%XNCQnMJZ7(9q4GRZo`8C2KC>( zRt75Aq7VS${fy@x>gx~ragOhCW=IrNj{a`CIyV@NXmOt;wMO>7|8O~&fP5TI>x ziOXFM@UhTBpwk66e+HsEnjdNU#3{)@t!baiBBbLMd z{^%x+s6*uY)V~i4;MOHuW^5^WYjv^BL=}cL#vT)%4DG4M56z!0#-4b=GCk zF+t-P(e(9m06io+p3Xh=#zxz$Hg+G?7|- ze$wiLYq#gS7EfyTAM#zNh<;H^F5fgPu-a;Pwo8Vp)OahZK<=2%WGtpF$j6>@1WS#q`HT&%y5`uQuq2>3HHiJTUJBXLrwUN=md9pf7SXw}-bG#m>Un?GR`VAqV&dM-@jHQva? zE@^qzySt6&Gj1~3fX{c|_2O3K8u<+$5641MKg|W@OSiTKsu^P&PGLZ^j2ByO z=8~vu6&)q;ySgZPu^&?g@$UdScJKqGHmiN@&=ugD*bWT0PK|v{zCdTjJ6i5p?)A-k zdBk-($lAtyIiKj^XsqC?m+G8iYI;p{6Y+%D>qf_}uivBlWqN-hcg>&h;B;!i=jvm* z%ZwRCF2#TC@pZUHp4EtRHkKcUWEoI>dFi-oGQys9q6)qN8w5B$y&wSYuo~&Y0yTEd zE4lYs_#ZVk|ErN^!GE#8Getr#wn9pJ=&AlC62z8zGobJwIwgPO{|qC^JiVe4`zZzd zu5*pXgu8U*?W-m*N2YE$)8>5QOj$Puo`r)n`Tv*lTHDo(a^%V(O-h>_M~50z~`TcwO&6apY3hU62HzZ_x;` z7?r0q$cFl>BaOF@aj5Fu^h@1-MZUOIu*})ZJnhVhY20mD3>o6rP=f;lk2=QF>W4S~ z`~L!PKAj63#J@;gbt|>VRjL+x87QAtK`HJ3n*rqcTTRZK(FBGIj$nA_xc2X=obL2B5 zpT>JNMcWj3ap#(Od`hU@(JM=0-#~`}ex>jEWZJ-h+TLcVrlh zOc{hR2ztIQ&FtK*p?A6``vSWuK}8Sr2^39#lZ5Z&#!6%m1r8}Pp0LEGOX*2kng-9Y@Sgm^OOTUF9rpj!xYl!n;c+>?J6AxWD5jcQa zD^92u|L}*6b0>4HrV{WH)LNVnGQ`b!Oxt6h{EX7NM^ImHBTmrNYLCT{&i$W$AMr{N z{rt_y!JK_f1e7?81BY>$)Kf;M1_3Kvb^a$SR2>Cm6u@5pufox{3I3VcoF&r4V@W4? zR%0kfb7B-kD|x5-be)Sm+T07IGqa%>O$cHOJz6nAF%&A%{D)3H(I7YQI(1?)JB89* ztrGN2mWBGN$u`}uATr3Wo8ilhFpf;GC~^7%nYWuMTPX%4FU{rn7h%`^IR%i&9nx}_ zGHwGPTJ*;pdPq64?r<&cj1_~#i)nu#B#5%y$IIizN-FRnp-3(}e95ksjof>ha zE+qLUu+PFLQ*IJU{axM3QKk2HgCbkdNoDc61n{B3j;w6zn-X^ifV@cyr-sJs8{)mI zTW(7s5TBUdx%o#>;IaV(XH?s8fmM7+>Cq#X>VZ!C!a=LpqS3jN(P6zgK|QM8M6-#Hb zK~JUng*F1nV1!Dq@FACj5;i70-m_`{G$fCKsKJI060Dt&bQe}bICHe^*2kqrTC|a> z1J($V?-16ExUwv6pfk9@SNCqxY4?6GBxk9U-|sFMkPMvhVd-oO)A?Lyo3&HgQ}@00 z1XRFg=ToTyNxofuTmXQ~0JU)ZyRH}n*F?4))%Pu7DD9D*KCi*|Up952r$2uba`1^+ z&KPa=$02Bn_K2 zk=EUaf&jni0HnQwLY3=r1PPlqD;xt>&EToAF<%mg{sG!YguL}s2;Dj|SAI}S1!mca zp<+g8yx%vH@GYR@krf}im&|4_;jHDN07a@;AWy{tWO)9Oo`9#fLT6CU zmAvP+LN6IrmWqObZ!s`TJG4tK=?%MP%=fkR^@ht$@Vdh(Po-#MCjWuF69dYg85Rv_ zwN>1rj(tq-K<^ls33y}Zlcy$Hyy=EX84=ZG=9r^H=q^RMtmg0$fQH$Z$(uRMy8+&3 zf0gdJ7~h5sThNEG7kYolX(u5eBt;FjvToA-w|Sky0Leb665VmoLRB8bQT!}117Ei~ zW-I+?Va-dMlw#t-a;FZ86)*=0>hp-~BWS41|7v%qb9fKu`#XT(;q*x`f(bZ$+p+PD z)cgMAp8k)(2A&innz2~gYrE!Zn322DuY`}j zF6ZkzDdZ*Y?XFg@>WFv`=Q=sl^vAMp2r@90n)BpGj#X0hO`Z$*k?RqgN?Y(5bo@$6 zTrf{I%H!&F=R1ni$rL=KW-M6MTE-01vGDX1fNKV?M-Bo2NvxJ=flj>Ei>iO!-_%7c zR>xc)w*HVcCnKI4y3UmVT!XX)#vidyUtO0>QUIPRiYWhjBIB;oB=w})?Z;Qh9EZZ3 zQZtV1P#EYFwT^&XY$g9#*M<;am0A>K^EQh`kIxauV(|TOC;+6p*Xho!%kWYz ziXZFi{o3YvAgMDJ=um<@GnZOv(fV>&lAi%y>1zH|$%1P547_fh+lHGb3n+li=`cXm)O=x$W5kYMQY6mFh* za^Q&T(S2%Yxckq(roQKU9F`^^y-P;{QBe@-Qi2L1H7Z@2f`Ig< zNKFK!Dj*^vH7L@XfPmD{dq;Zj9TE~C2`T@BzCYjB?+ILYt_N_k&&l31vu4ej*_T&- zEm)I{_O83cjgHwA!N;QB)RQwIW;z9_{}t12d^m?S;fW#cj>6W>=<|)OyWlS9 zx1KX&9+&^vlfHuoU(n^%^reJ#Zngn{3cze(sB27rfdoS^voY|VCGDA+4~n>o53J?x zHtl;Hp^;DeRM!d;brUD->ciz?M6J%V11(C+ZDyD(tt^*&736|H}F~+kN+kZBQ!FW<XA0Mym#F9o;Ur9p*^e~D^Xlj9*1E&`&o0BCQ@LX4PplYS^o5`T=Z_ZHMHSx!4@ z1ko@{?XZRLN-s08AjHcv!a=Ix|0-$kfJ*B7tAJ1jDcR{p^D3s+_{VD2s~fh7?@CqW z4OdKe#OBE;fI#Aqoz~O(*DXt)%d{pCCvT@V3fbU+WOWQEtlVq1XzDetnR$(Hkp5%U z`?B0$61iwnpZ~-n*;VY&K`!?G-^}1#@W^dw9veVDhd}_S?#D^ElMEtO(p4$)KgqQMT-4sb|Ni=iJ?V(>{3bG3t zpJlT>(tMsa&B}}X!qNnGL@MLWB+?Hbp2hVcmaFZDg||0N$U`9!`Y|ag<1w-|zOSnB zQlUmk81mEF*=%7b3s(L0O{E7$A2J%ZhwleIyDZZ*_%J||Na_C12QB|A=zp6ER>n0H zLEN>^Wy##_3;5Z`ZdzB@zWG#TT!r0RZ4XFjhPId{pG9vQBc#)nW6hknt^Q|39_ zlvhB4dHh_EAotXw2q=}`IMK8#2&BE<7XrJuO*8hgAYJ!u{xaX=r$};os!+F6Dk2bV#z<@@nQnFAf&-mV?$VrJ!VltE9+f z$UKYp`s-=AI6%RD#!LQ}!cYV=-`L{DtqCb~ZJtzZm;r}f^qz1=-L$Fwdb>zmH5LRQjunwI>;DuN558GnL!bAYofa;RV`{obsK@e>nA22f;W27o z@}r9>=2KFVN3?Vc)x`-DZKc+{|0vjx(7MJfkR^2dShBOyY0cTh4i&RflecEC z^I7mz{EC}RY`iU$xAb>lUm1Klaw4j69up=0KNhR*(7Ey(XcO%D94)r|JnwMsG>NFU zf1G(at?D|VDZ^1&U}w)>QE;*|#ulyJxl#rwKs(eLR%C2m@gsC_|0uZfGdWD@rAIS) z#av6E*=J#Wy_IJDB+V_!9xL<8>PG)c?ynlW6CKWcNCqy1jzmAkAmU#Y`6@X;%&@i0 zH0ZJHk^^drd_EPp`}LTQ`-cUFnpLOiduKhM!m1vW@CH1~f4U|fz?JRX!R@&zz5J5q zQCji+3`CS_zR~<+nIU_6)E#(9s;Fb#aOmPP8$BXHk&EAn3MGTrLj2nqKfu;+nMlkv z?p!n}Ip##~DC1s>kt!UCFMl^TAdba!hh6ro`Dd<@yyBIu+r*g7XmW8#EjMbbrvZtJ z)iKoG2@i>^H=)FEj;xAlD>GlE(XvH6is!*L!S}tP zUs3I+dizA77Ytpi{i|Mo|TZemt{Ll@m)ABgvAACJ&1Gf*Pv%4wp>=w!z| z6P*gxbMO2!&oH&M^y^2;wm_kX{^KQwI0f3mJ0(wC7sc3Zl%haxKY3nOt^V2OHtnRc zFV^-8$oR;UC7NU`HqUZZxk)lg;n3#m(3I{!o&bnc3|EI?ZI_bQ>VKajl@GIZ%eb;U zc6I!MZ=3Wy+Hj8UHIWU+goBr;AkE1wDBSiqNsp$|-^EiKE^Np8{9clTDe;&6V z+!&lCZof+Z$d?HbJfO&m$53`&K(NthcErDEzlIqd&iYHR(Ove4#)m>)*AGj!E`PvP z#(_K&0+z)+WKEYh)plyi`!5ul?9^K}oVC7*=Kwihv%vpCY;HFQWdV2?Yj6F?#0?|} zQ0TQ9j%+Q|GPlg?CncQxSIT3iR#RlvZi z+?KQu{c!qF*yktO{DpVT#S3jD8)Jm6&ge|cO8r0nh3@juYgz#2Cz#rq^9b!=lUAMv zAw{VJmd)F{WOR`{^}^xmV+`ssM^Z$!Hm)m-U(&${w)_*6m?QY#hF%8BX=$e@Nt`Ql z@v1e@&!0|yVo*hQuOQp~Hp8CCWcQsrhr{z#Yn=e{PVGgx3PRUy70;g)$VFKulb}PT zYCHsaJ^*?XHL^l}^NOD`+zPl%wr{7Jzs+MHE@S)8??)>A&lg%&pAj+31i)K6b~n2b zwJEaw*>p;~oTk2rvw421VxR)H5kjPF_!lTfXkOiXulWG};`Y!%$NNM=+}vu$oAJ6} zZ7w?tdUKAByMKzT9~%ZhJ-074+LA+T-cod{5AFzDq=@3WSQTvT;YbZ77y91;+5|605!3R{ z%J={wd>-QJbyT*tmZ4m-ATZ05bU)xjASFOuQtA)Fmf8R5Qlih{);zv1xNdbKJz!oy zEsU*dAj3lak8@YzjfM*w_5$(?Kh8m)%bBhe0&^MCBS4pV{)^kdgFG=1t{MqIKZTQR zcv2YrKO3ilaoa=Jt&Dh){7EcKg+LhYX!3AL)BR_den5@@R7b?< z&`OG5a0Xt#K0&)VZm2lmJ2F85wL~AAS3V=;Z^Y6ZilRi247>>M?X+l+)|-C-hmVZw z5s*JKW{t?y@@@1Gvk7Gzp51o!aRsHf6%1lXdmLsB(=s;;B(jWb)m@GpcL<)!RluO! zQZYz6$3G1<3*`{(9Sea$i09Jo&s&edV@XGX)pM&QV$tt|R_MBu);q3QJ(MR~|CaJC znoGO>7CHQx-U2n)Z38TL_dk`$;t#;gW1fZ)CAR?8!Z+e^CQ*5Srkn;+$9#MmQB+;B z252vVwEwl&wYa}p0rT2%nO+aa*TAdS@)Bw(pe|J69}HPaK72`9uPLy6P=s^xyb>E_ zA|Rf2K^P%@2z+rKP!m0q?W(QW0Z&P|0(B{`U1CpP!iO*EA@=Ndq_PvnOQ45Or2h09 z3j(A5(&IqYZu4oea;?R0cUH{=fVi`G=kl|WcV~%Q&sidu;%lEVd#hVK>T_$f(Zy>H z1qnt3Nct07TLr*)B#5*AYuQ39|L0Z2O&(aZ-0vKAZ(RlAKhRLobgXehxO%oR^`==~ z{a{bhczKDOU{=(}4ZxczNkZKk(JdFQB}V+MO#uSnPdPe8Mww?B{aN!gsB@tqvG1N~ z-Ss&wjnVP!yGIkC<(P&=F>}Zj;%p$!6NdSBI`}EW$1aQ(c`gAo1SzcPDY4z%E9q(v zpG-M+<3OdV=&()uze06@=k3rQnJ(#CwF$|%K)4ToFMKGW zbDDI#W3Hi!wNQz2H^#6znHlXt2y)s-h95eN(=ko-c=fMM_huEMK$3xnCj8fZ{(=t~ zKmoal*1}dJ9>CWuUdSbl9jqL`^*L`rGparntdgMrzDma6&VbLswN?&o?fMUtn0DO8 zH7x4z0N;V0_6CDb8kS=2R%V~bTc_r1z|wLy1X6RjU>P|{8Ob?GFj$VsUNx6>kY|cj zz!=0$H=(13!2(Bx@J*r(9wZRnVEKwC*s+qaK5L*g#m7?y?(6W{2r+YB=Sa8;Y0eq{ zD)tQbGOVf~Bs-rz{{!6}rmRG$|SZJOW7;WAqri-V&eEK`m=9t$Py!#&XP zgZiUf5ZjatDTDU&&1;PmH1)L>T=%=X_oZ!rJk5!|Fyn$wOqv_39A=h{RdUg$`_f~-LAd<(z zV;duJ@p8%T`d~j7EM0&YKuVmGu#-=N*`4-DPU`LY407<&q+JM}!xz%~SP@yRr7gb4 zKxh~iEUr?-y$pu7tk+_w0d9Q8&O zwVtd4OVzTd?>W&AUexJ+Jopp!ckA${Iy#vH3Tv|hb_4lmihb{S&IGBV&p_Hju(yz zR}z<;A(*%bd7P)b95XeKGNt?kr>8!Y$0ea?2qzzyU;e3&Z1c1yc^8cpDrVw72*;rL z{_YWm3>^HTJ^g}o8RtB}_FB9)h;-CLvHy0;de*TNa zUE%G*C%oT!F0mQCNy1r!tF3}WqvXZs5iu3#8SgH#1BaYkWDOVc9`E|_BRr{ZA!unG zhoi0oe(^?R_IG!mM+y3&^(FI^I@!~aOuDmEZ_s>Vh@C)1F=z&96AWe|&XHc#B0m%I zQ?bYEAVs~-QdqSYJ$V2y6i#F03iRwhohG&uCoA|qK`4E`jB)Ci#TU&!WU!U(IE^@y zt0U<2Z??gbprn;i3Q|AxRs%XfkWOM8;=Ldfgo2d_YcnfQk@uK5mUmf)QvubZUd3R@ z$?OnbFNgiE%s}Ee97_D!JiWVeO3>9jxPhz6IMZZ!Q2SaDJifPxtet;yY#fc(f(p6}7?>8ML(9vQG zH(XlZ<70@}Uw7dQ=XDq>;W~t4NuET{!ym#PD6*M%sN5dfmxQ~mw=95ZG}TknVn9LX z?FudAJ#u5c0<0g02SWq=)uC3M#J+)7B)FnpDfE7M3^}C_AS+{)Sapxu%rmp)2SQKM zA&8f|0NGqGk&s<{B+9+_yWWyJT=mMm$m&%p$lY+d#X%Hl*C7{#lO!#_T6qsw(ObZB z?Lmqr+Py2NgZY6|Myb7y$E)cPnPzUj0Nk~+7GXZ}k@g*_?iZQ6T{AYUwH4K1<$3o> zC63pOZ5`}q{HxXBMx{f>*GBXL5rLi^B*90~#n1u$2X%)xzmx~7-G7U>d-M@LEZI+k zo(bwt0g&GL>n1s~vSW5lGLPlB6F=Li&%>F$4NF=E;zJ_eY&H!hFI`?QqWNyY4iz9o zk!<}}EkSZ~m}vQFwu<1UJsS1K=(GpK%YP4T3P8K>=bIH+)Ud9OaUwcBF%g^b*-!R3 zkqqMC$^Pi@{&VnK+XEb1>7GUO6Fhi7B9p3g10rlt-<{Yf__gtP?_|b?v(T)1=7aNW z`EAl?uCsAyFKW7i+Nsgr1Uz(V9Y!j103kCty==achgUEqJPOJC#dnYUbH0pT{YM)1 zK%3x&hPA;M-q967!rIoBXN?`N<}|uisz|Pqq~XTKw^|xYj_vimOKhyh zp1wt##GM>}8<5kxX&yfU>Q#6Rj_G-*HkMOXATE`iFJuLD!HKR6ii4HqRM5>Q;5fpz zB)D`+FxH+T_}-Zxd;nA-*Ft3pgJryVwNtKF3FFt<`X;x--|(+xH`})q*+mw13R{^5 z!yb7Ibmg4}Hu@5kPm>pttuS~&eRrHRNTFfLeBq0rz;a?4+=}L*At6)*Ti8!=_-4yo za#(VW3N2o4i@c}l&@1Q{8r=ro=6c&B(hmNb>-)*eU$$@Tb{)V&_V&Y4CE>~{#9p|9 z3Z|J!w^SLa{y63E&gDhNSo|1R1)GWwszFu%<`LH`*xYpLTUf!NpR8Yx{BqEtoAklh zRkkY8Kj9eE(WbDBYNL}!)V;#{tqp7DNWw`fqr3~q^wA0EiBfL=4rab6Wnto*Vaa8n zXz2xbZJ;-G`>eK^XgYT>S?nT1BERoIvE9*~5~;>N5ccp8UnvfJG_QVIZ;xPr=U^Z% zVH5?$P>-fU5rg{HsNBYOYl*s@U^UOrcsu(9{N4|sbO^YWfGm-$;!m;Lw++kn9*@gv zCJv8xybKHXZ6<>_=J3cDc}-!xSFjtonjk!C^ge0TA;$#f_f#@{uZbnH@5`(RG#)tU zFytl_BNWV1YqU0pg_$GXE{HOve`Sqp6$~-yz*X5i9xAP+-UH3XKyByB2`z)*jE?`a7L5FF;iJ z-s6^u>{8$b|A>jt&wH0*B}J^0<2-?>-#oP6C?RBb75a1rVX4(QxvIp}QHldn)K-`z zVfb$Hg?U}gxA-y8!vuIh>;t%N{qRBiQNRjq?tWsKDC#%o&04BO){fhB!+*fHfs)JE7~kPoih zQZ5iz4U(O!^&1@&t)oyoJL;E6IQ7Op?8zgvx3KMMA>#??d`qzBfv&B^xH^j|`pPlXp2B|~yw+>O0z#;KBXb}Y-_+LGwWdg6j=wea&=$xgS5@E*NxHQ~r;J0yiVf{$2oZ0Qte@9<7f017~IdTqQ<;de>8 z5Zq1a}8OVwi!0RWmlS`&X%R8$aPKzcFn*>){f==|TNplNI zL37p6&R4&pG*+hAmk}Y%m=@Ae$*tp7R6M-P`ch+JW!FL&B{tw8yYZ1l+1#%Ql5Z7P zJ!}?xWLE~!)6Sy_aO~h7a2y@{k>;DOHDXEULwU^bGeY=*_q^ z44eL2vI%MPh6TVyB6+I^Uz4lk@BNM)vmpbbkKg5|9VC~kPw=iU@A|9Q4+#6{mJN`6!`3NaTCMQI zs^}aG3Nj@zz_|X#-976=)^E4n}Kcg+#UAf+O5c<_M z>z`N_{>hOlz}el3yEOmJ^G5x68#_Mjc4H_gxZP}H&{C!T7GauQrd1wn*;%^?2i%rQUiN4RXLzr}9&MdI%h zS;$uxR+}>ib+!k~z%n!Zw2u5D%fzVu?czT>U1nxdtnCFrAVN5-VZC=MxANmi9q3s> zJQ2Mvi21fvNW{Q~ie4xQlP6XHq4r>z9A}sC@L47)Loek(OE+3z~#eQ;L9ZnNp_7}Rwb6a7oAj8Pd#|;`uK6f!l$D4JjYY|1D<9zvEFS;gusMZH_ zEIp2D+)7O)YgIAE`SPajE@|fqm|&j>?yLYK-JW`VX;38@RH}^YLN9^=N7%AIT#MZn zzeDG;TIwqC2|kksXe&wC1hBMTKJk>b{nc+1$&}XfF2tHe z%p(|-WJ|2a4^fO#fQ|%64=}0nZX%AQ;H9EJvG~eFRQG}+HRZdU0;Pex!U+6p#ls1s zR5(^6i&);CrC-#i)|lqJca&dx#RloF6poRTY97>}C$`Pj8luiXMKT!b;*Su6+W0T& zSpQ*fl;g#U>&s&63>r6F(1o_orQ3-K@B;~`J9|lKh~)b=3Sirr^yD+35)SC+T&tyB=>C_}TlPe&er#cKG||e9=;rj@XiKbk z_A{f)`-F=}ZojL@?mio6jnutpunn?xfUwWkqgSDxSquPwx3t`rEk3w6Fw1N4WR zz%=n(<{9n$i5xU-)b-ddqjKE`xStPz8jKP001Q836PfVk{Nj$eP~@!0LSA8GYa<3TbRwAdSb>vGfM!=z6>DGZ15*nqFRDdL-O4Z`&A8tu*>s|Ik{O}(ex<%06&(>s&i340ak7*`Z6q}e!26G26^Nc|h6 z6;u!g01xU4eTW-(F3o;lV?l-0*$4%WL0C!Fg80q}7qLUB1yu98$BEO)Nla~B$H0KQ zM_lo-fokB0ak_8t%E!0XtQUzEg;;NVrF+^uo&b^wS`$27|4Ntig47PhNC1he(yG&+ z`m$I^yNd?tV48$EdDcy7IF78<{j8LK-Q_L6X2R<$2o~D_V7`f^JbrlgIq;?X$y@PE zBs6-mG|<<7ubqqjV{bW-5-eod$Go1ic2Tw?*3LV3gs*3jHjY5%(CeDvT1k}7bSvzg zekNaD*QEA%4o?7AewzMiqXHpmiEZ^YZa~s6iC;oDfg1z-*H2XchU6!`DmS{V$w7zg z*gD&9JOI*ePxFxTE%>cOkPFSG9ugj-Flp%eLYt3EB{K*R1lo6^J`0NxNyh_wCRMw4 zb}Hq0QOu&v>z_;nn3Wf8FWrAfiC0>>+}18Mk2(>$93Nq7!T$Z~Tly})bC-UGXpL0W z1x2|}{+^pdI(Eo+VAlDJf@&>9VKXJD)H#R#ojl7O^94+YX~lSn<-i5#yJ(ozpY%tG z@|{ps{0bA}!s#*>qjj8nDD)t%acYAUT_Dd4y+pWzibHMS;Ma*8?kr#a?5UjJc*1dG zEnUCG=0{bfzwQ?fHKV$)pi8ws$>8n4MHp6I?3=xDZgWso%zQPIHK`g7!3;pL>8;@T z)3Wr{>~X|F|C+TwH?k1F$M6Sc;z>XWx9Db)BPvyVweKCBhO82*r)}v^n<#(*nx_vz zpMMA1<1P@r74V2&>z;!F+!6^p()c_;%aId9J+vv0e2-~3G9w~FmQU_)Jr#M>7&Oil z-CR4$`$kE6%PS!2^6}!ti?<-mAQcXYZa86Fzz)K_F-s)k$dUmZU(E=^tisHk4)5`@ z8lZP5ZP2^~H|@-YK_mFir8f;f$_sjmLY$X8uBr3%HmgJTJk~ruR6nxQ4hI-Q7@8Kp z;YIKz#Wi|jI^bB7hy&t27b6(t3wph7$M>XnsC48t8vctHV6*SBW#mFOU_HZbMpv}H zy%#U*5h?V|%?Z2({i)vV%5t0a)-G@r$Pa@bpdau>P(ItINLYa&awZJ)Vc~Y5SmO`0 zraPhWhLr`-D(k*%((biv@AS|Zw zW;N@`JvuyR^CA<`;Kjq?co=@Q6Lcr=6hvHKN`)U`TLI7jxwT5gPG?vzgG+pN1Bl)9 zOgK>yV3CPIPWUO1Xb!I$(!P9Dxb2Jlk-RSZ9a4GnG|usS0787ANpR_rso~ESsKi*D zAe7)7@kW&-$+U2KVEqBeZXhp54RWhr{9^6Vj%M)-jX?pRzB| z>!&z9R{g}`Z}KhcoqHU=lK628J>KUTgAC_{&(N$b!XcW#y9WY;l<`8uoFyd~sj{`s z9cq`b=HTL5vd1-Ti(n$YPV0R|N4NqZxFAo#f%_FuqKT-i==x)G*D4}sRBKOXadE2!W9A zy~(-XBJP<)-Qo$YJXYXtZDa;6#|b4VLkYeog;0+n%?LPVwITvW(t)YY&P}Xj>7+=w ztX4YeRUCJ-1LuStIpmH$_1k{4`;0JBn)6$I`3F}9`5$?XYnGx95?zey*j9kWHcA#w z1|~)2ok|<|^hs2k5`#{qN0UJOXoiMR9F&9J$f53ppSLT1-#d{vI5w7=Z!bRuWK+7aloHf(sFRZ!CCZZ-t+ z>a6kLh};7b8dMVsC_9ezmbs7%WV30$R!F5cKR?sA=BY`3fHzqhZDoQSK=~l(#03z33yza#1OXxK`z-P|rR;EN2qkbVS>X4Xt)3A- zDVe@@H#0LJw*q<7862D6Iuc5=7coS_{3q#vmWZb!QalVX>cOBb;VnrF*mH$yyL0qp zK8AV`x`XKVWhRD_P8*h=2_6j#;fz$HAZ+l+1xYz*6#orHa{&%&VQ{42#S5RA-rB=E z$_3fP$qdW|*|dv)ay?lTV+M{2DrK`XV8MR_F#so4wo2mH#3HOQ;xLt2kaDK=lYESI z)D@f_{xU?|1O6MoaS4_6;l0zAgWZKnzMs?;)~Ff9vCc-;!oZhuh;tx}@SE4?65_go zWT1^-3}l*9Q>3leF-RP#3UcqRP)>z-@6p zH1T%616I+ob1p9Qfhcs6%STKh*!rA}8@)ucAU;=vi}2BY61XHSE=JKnXP61S62Ga5 zZ-_#ghh8qhnH5@Q<5BdX1@P!=RndOr(0A0tgdZ1yTc?J=%hX7NRB3l@ zBD0bycsW|>VxLmJXxtnkZJblh$eHJqy!)b2X>9H!0Lh!7+S0p{xvC)~7fSuIyPzRp zxw1`7<UZ)rp;K#= zoxHSJHQ_pfu91ck**yEPv+qY;%_m!{QCr}Sm;c_~JW7zs?{duwGtBa{0YLfnFt32;Ut{J^FrV5Drm+HqcnsVS-GOH*!Fv@5`Xnm9|SS?)J)*6tt5Ctgw-> z>Id7C!!o;Ux%7#7v|h_G5tMQ0;FGG;&y< z@lX02`Rs&i9|BGaU0%1TKHhXaXweG(5#rKEOm@qO7FX`d1~x7R=BJcChTBM&EKpTYn`a(b?u_+ zrQpazPurAG5JC)#q``}kCcQU4)<|c|OI�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 literal 0 HcmV?d00001 diff --git a/tests/integration/api/TopImageTest.php b/tests/integration/api/TopImageTest.php new file mode 100644 index 00000000..79aa62a9 --- /dev/null +++ b/tests/integration/api/TopImageTest.php @@ -0,0 +1,76 @@ +extension('fof-gamification'); + + $this->prepareDatabase([ + 'users' => [ + $this->normalUser(), + ] + ]); + } + + public function topImagesProvider() + { + return [ + [1], + [2], + [3], + ]; + } + + /** + * @dataProvider topImagesProvider + * @test + */ + public function normal_user_cannot_upload_top_image(int $imageNo) + { + $response = $this->send( + $this->request( + 'POST', + "/api/fof/gamification/topimage{$imageNo}", + [ + 'authenticatedAs' => 2, + ] + ) + ); + + $this->assertEquals(403, $response->getStatusCode()); + } + + /** + * @dataProvider topImagesProvider + * @test + */ + public function admin_can_upload_top_image(int $imageNo) + { + $response = $this->send( + $this->request( + 'POST', + "/api/fof/gamification/topimage{$imageNo}", + [ + 'authenticatedAs' => 1, + 'multipart' => [ + $this->uploadFile($this->fixtures('topimage.png')), + ], + ] + ) + ); + + $this->assertEquals(200, $response->getStatusCode()); + + // TODO: expand this test to check the actual image exists on disk, settings value to set correctly, etc. + } +} diff --git a/tests/integration/setup.php b/tests/integration/setup.php new file mode 100644 index 00000000..67039c08 --- /dev/null +++ b/tests/integration/setup.php @@ -0,0 +1,16 @@ +run(); diff --git a/tests/phpunit.integration.xml b/tests/phpunit.integration.xml new file mode 100644 index 00000000..90fbbff3 --- /dev/null +++ b/tests/phpunit.integration.xml @@ -0,0 +1,25 @@ + + + + + ../src/ + + + + + ./integration + ./integration/tmp + + + diff --git a/tests/phpunit.unit.xml b/tests/phpunit.unit.xml new file mode 100644 index 00000000..d3a4a3e3 --- /dev/null +++ b/tests/phpunit.unit.xml @@ -0,0 +1,27 @@ + + + + + ../src/ + + + + + ./unit + + + + + + diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 00000000..e69de29b From f471fb559afd2edda2a61ab0e90cec1b8ac12382 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 12 Dec 2023 19:10:10 +0000 Subject: [PATCH 4/5] Apply fixes from StyleCI --- .../Controllers/UploadTopImageController.php | 3 +-- tests/EnhancedTestCase.php | 9 +++++++++ tests/integration/api/TopImageTest.php | 17 ++++++++++++++--- tests/integration/setup.php | 8 +++++--- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Api/Controllers/UploadTopImageController.php b/src/Api/Controllers/UploadTopImageController.php index 364e95f3..8c4fec41 100755 --- a/src/Api/Controllers/UploadTopImageController.php +++ b/src/Api/Controllers/UploadTopImageController.php @@ -71,7 +71,6 @@ public function data(ServerRequestInterface $request, Document $document) if (!$file instanceof UploadedFile) { if (is_array($file)) { $file = Arr::first($file); - } else { throw new ValidationException(['file' => 'Not an UploadFile instance']); } @@ -82,7 +81,7 @@ public function data(ServerRequestInterface $request, Document $document) switch ($id) { case 1: - $size = 150;; + $size = 150; break; case 2: $size = 125; diff --git a/tests/EnhancedTestCase.php b/tests/EnhancedTestCase.php index 4c3dc569..f748a157 100644 --- a/tests/EnhancedTestCase.php +++ b/tests/EnhancedTestCase.php @@ -1,5 +1,14 @@ prepareDatabase([ 'users' => [ $this->normalUser(), - ] - ]); + ], + ]); } public function topImagesProvider() @@ -33,6 +42,7 @@ public function topImagesProvider() /** * @dataProvider topImagesProvider + * * @test */ public function normal_user_cannot_upload_top_image(int $imageNo) @@ -52,6 +62,7 @@ public function normal_user_cannot_upload_top_image(int $imageNo) /** * @dataProvider topImagesProvider + * * @test */ public function admin_can_upload_top_image(int $imageNo) diff --git a/tests/integration/setup.php b/tests/integration/setup.php index 67039c08..b8020e3d 100644 --- a/tests/integration/setup.php +++ b/tests/integration/setup.php @@ -1,10 +1,12 @@ Date: Tue, 12 Dec 2023 19:13:27 +0000 Subject: [PATCH 5/5] remove else --- src/Api/Controllers/UploadTopImageController.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Api/Controllers/UploadTopImageController.php b/src/Api/Controllers/UploadTopImageController.php index 8c4fec41..7aa796dd 100755 --- a/src/Api/Controllers/UploadTopImageController.php +++ b/src/Api/Controllers/UploadTopImageController.php @@ -71,8 +71,6 @@ public function data(ServerRequestInterface $request, Document $document) if (!$file instanceof UploadedFile) { if (is_array($file)) { $file = Arr::first($file); - } else { - throw new ValidationException(['file' => 'Not an UploadFile instance']); } }