Compare commits

...

5 commits

Author SHA1 Message Date
3db7b9d98f
Merge branch 'element-file-routing' 2026-06-21 20:24:37 +03:00
b280475314
remove legacy pdf path 2026-06-20 22:50:10 +03:00
28f4114def
test pdf path removal 2026-06-20 22:46:11 +03:00
9bbabc7fa6
route element file uploads
Add short and long PDF paths to elements, keep pdfPath as a short PDF compatibility alias, and route generic file uploads by fileType.
2026-06-14 23:17:56 +03:00
0d5783ba9c
test element file routing 2026-06-14 23:05:45 +03:00
41 changed files with 1566 additions and 623 deletions

View file

@ -57,8 +57,7 @@ class ElementController
public function update(Request $request): JsonResponse public function update(Request $request): JsonResponse
{ {
$iconImage = $this->uploadedFileInput($request, 'iconImage'); $file = $this->uploadedFileInput($request, 'file');
$pdf = $this->uploadedFileInput($request, 'pdf');
try { try {
$element = $this->updateElement->execute( $element = $this->updateElement->execute(
@ -71,16 +70,14 @@ class ElementController
'iconImageUrl', 'iconImageUrl',
), ),
richText: $this->stringInput($request, 'richText'), richText: $this->stringInput($request, 'richText'),
pdfPath: $this->stringInput($request, 'pdfPath'), shortPdfPath: $this->stringInput($request, 'shortPdfPath'),
longPdfPath: $this->stringInput($request, 'longPdfPath'),
youtubeUrl: $this->stringInput($request, 'youtubeUrl'), youtubeUrl: $this->stringInput($request, 'youtubeUrl'),
iconImageContents: $iconImage['contents'], fileType: $this->stringInput($request, 'fileType'),
iconImageOriginalName: $iconImage['originalName'], fileContents: $file['contents'],
iconImageMimeType: $iconImage['mimeType'], fileOriginalName: $file['originalName'],
iconImageSizeBytes: $iconImage['sizeBytes'], fileMimeType: $file['mimeType'],
pdfContents: $pdf['contents'], fileSizeBytes: $file['sizeBytes'],
pdfOriginalName: $pdf['originalName'],
pdfMimeType: $pdf['mimeType'],
pdfSizeBytes: $pdf['sizeBytes'],
) )
); );
} catch (BadRequestException $exception) { } catch (BadRequestException $exception) {
@ -176,7 +173,8 @@ class ElementController
* description: string, * description: string,
* iconImageUrl: string|null, * iconImageUrl: string|null,
* richText: string, * richText: string,
* pdfPath: string|null, * shortPdfPath: string|null,
* longPdfPath: string|null,
* youtubeUrl: string|null, * youtubeUrl: string|null,
* } * }
*/ */
@ -191,8 +189,12 @@ class ElementController
'element-icons/', 'element-icons/',
), ),
'richText' => $element->getRichText(), 'richText' => $element->getRichText(),
'pdfPath' => $this->storageUrl( 'shortPdfPath' => $this->storageUrl(
$element->getPdfPath(), $element->getShortPdfPath(),
'element-pdfs/',
),
'longPdfPath' => $this->storageUrl(
$element->getLongPdfPath(),
'element-pdfs/', 'element-pdfs/',
), ),
'youtubeUrl' => $element->getYoutubeUrl(), 'youtubeUrl' => $element->getYoutubeUrl(),

View file

@ -12,7 +12,8 @@ class CreateElementDto
public string $description, public string $description,
public ?string $iconImageUrl, public ?string $iconImageUrl,
public string $richText, public string $richText,
public ?string $pdfPath, public ?string $shortPdfPath,
public ?string $longPdfPath,
public ?string $youtubeUrl, public ?string $youtubeUrl,
public ?Element $parentElement, public ?Element $parentElement,
) { ) {

View file

@ -12,7 +12,8 @@ class Element
private string $description, private string $description,
private ?string $iconImageUrl, private ?string $iconImageUrl,
private string $richText, private string $richText,
private ?string $pdfPath, private ?string $shortPdfPath,
private ?string $longPdfPath,
private ?string $youtubeUrl, private ?string $youtubeUrl,
private Set $set, private Set $set,
private ?Element $parentElement, private ?Element $parentElement,
@ -64,14 +65,24 @@ class Element
$this->richText = $richText; $this->richText = $richText;
} }
public function getPdfPath(): ?string public function getShortPdfPath(): ?string
{ {
return $this->pdfPath; return $this->shortPdfPath;
} }
public function setPdfPath(?string $pdfPath): void public function setShortPdfPath(?string $shortPdfPath): void
{ {
$this->pdfPath = $pdfPath; $this->shortPdfPath = $shortPdfPath;
}
public function getLongPdfPath(): ?string
{
return $this->longPdfPath;
}
public function setLongPdfPath(?string $longPdfPath): void
{
$this->longPdfPath = $longPdfPath;
} }
public function getYoutubeUrl(): ?string public function getYoutubeUrl(): ?string

View file

@ -12,7 +12,8 @@ use Illuminate\Database\Eloquent\Model;
* @property string $description * @property string $description
* @property string|null $icon_image_url * @property string|null $icon_image_url
* @property string $rich_text * @property string $rich_text
* @property string|null $pdf_path * @property string|null $short_pdf_path
* @property string|null $long_pdf_path
* @property string|null $youtube_url * @property string|null $youtube_url
* @property int|null $parent_element_id * @property int|null $parent_element_id
* *
@ -26,7 +27,8 @@ use Illuminate\Database\Eloquent\Model;
* @method static Builder<static>|ElementModel whereDescription($value) * @method static Builder<static>|ElementModel whereDescription($value)
* @method static Builder<static>|ElementModel whereIconImageUrl($value) * @method static Builder<static>|ElementModel whereIconImageUrl($value)
* @method static Builder<static>|ElementModel whereRichText($value) * @method static Builder<static>|ElementModel whereRichText($value)
* @method static Builder<static>|ElementModel wherePdfPath($value) * @method static Builder<static>|ElementModel whereShortPdfPath($value)
* @method static Builder<static>|ElementModel whereLongPdfPath($value)
* @method static Builder<static>|ElementModel whereYoutubeUrl($value) * @method static Builder<static>|ElementModel whereYoutubeUrl($value)
* *
* @mixin \Eloquent * @mixin \Eloquent
@ -43,7 +45,8 @@ class ElementModel extends Model
'description', 'description',
'icon_image_url', 'icon_image_url',
'rich_text', 'rich_text',
'pdf_path', 'short_pdf_path',
'long_pdf_path',
'youtube_url', 'youtube_url',
'parent_element_id', 'parent_element_id',
]; ];

View file

@ -20,7 +20,8 @@ class EloquentElementRepository implements ElementRepository
'description' => $dto->description, 'description' => $dto->description,
'icon_image_url' => $dto->iconImageUrl, 'icon_image_url' => $dto->iconImageUrl,
'rich_text' => $dto->richText, 'rich_text' => $dto->richText,
'pdf_path' => $dto->pdfPath, 'short_pdf_path' => $dto->shortPdfPath,
'long_pdf_path' => $dto->longPdfPath,
'youtube_url' => $dto->youtubeUrl, 'youtube_url' => $dto->youtubeUrl,
'parent_element_id' => $dto->parentElement?->getId(), 'parent_element_id' => $dto->parentElement?->getId(),
]); ]);
@ -31,7 +32,8 @@ class EloquentElementRepository implements ElementRepository
description: $dto->description, description: $dto->description,
iconImageUrl: $dto->iconImageUrl, iconImageUrl: $dto->iconImageUrl,
richText: $dto->richText, richText: $dto->richText,
pdfPath: $dto->pdfPath, shortPdfPath: $dto->shortPdfPath,
longPdfPath: $dto->longPdfPath,
youtubeUrl: $dto->youtubeUrl, youtubeUrl: $dto->youtubeUrl,
set: $dto->set, set: $dto->set,
parentElement: $dto->parentElement, parentElement: $dto->parentElement,
@ -52,7 +54,8 @@ class EloquentElementRepository implements ElementRepository
$model->description = $element->getDescription(); $model->description = $element->getDescription();
$model->icon_image_url = $element->getIconImageUrl(); $model->icon_image_url = $element->getIconImageUrl();
$model->rich_text = $element->getRichText(); $model->rich_text = $element->getRichText();
$model->pdf_path = $element->getPdfPath(); $model->short_pdf_path = $element->getShortPdfPath();
$model->long_pdf_path = $element->getLongPdfPath();
$model->youtube_url = $element->getYoutubeUrl(); $model->youtube_url = $element->getYoutubeUrl();
$model->parent_element_id = $element->getParentElement()?->getId(); $model->parent_element_id = $element->getParentElement()?->getId();
$model->save(); $model->save();
@ -137,7 +140,8 @@ class EloquentElementRepository implements ElementRepository
description: $model->description, description: $model->description,
iconImageUrl: $model->icon_image_url, iconImageUrl: $model->icon_image_url,
richText: $model->rich_text, richText: $model->rich_text,
pdfPath: $model->pdf_path, shortPdfPath: $model->short_pdf_path,
longPdfPath: $model->long_pdf_path,
youtubeUrl: $model->youtube_url, youtubeUrl: $model->youtube_url,
set: $set, set: $set,
parentElement: $parentElement, parentElement: $parentElement,

View file

@ -35,7 +35,12 @@ class CreateElement
? null ? null
: $request->iconImageUrl; : $request->iconImageUrl;
$richText = $request->richText ?? ''; $richText = $request->richText ?? '';
$pdfPath = $request->pdfPath === '' ? null : $request->pdfPath; $shortPdfPath = $request->shortPdfPath === ''
? null
: $request->shortPdfPath;
$longPdfPath = $request->longPdfPath === ''
? null
: $request->longPdfPath;
$youtubeUrl = $request->youtubeUrl === '' $youtubeUrl = $request->youtubeUrl === ''
? null ? null
: $request->youtubeUrl; : $request->youtubeUrl;
@ -56,7 +61,8 @@ class CreateElement
description: $description, description: $description,
iconImageUrl: $iconImageUrl, iconImageUrl: $iconImageUrl,
richText: $richText, richText: $richText,
pdfPath: $pdfPath, shortPdfPath: $shortPdfPath,
longPdfPath: $longPdfPath,
youtubeUrl: $youtubeUrl, youtubeUrl: $youtubeUrl,
parentElement: null, parentElement: null,
)); ));
@ -82,7 +88,8 @@ class CreateElement
description: $description, description: $description,
iconImageUrl: $iconImageUrl, iconImageUrl: $iconImageUrl,
richText: $richText, richText: $richText,
pdfPath: $pdfPath, shortPdfPath: $shortPdfPath,
longPdfPath: $longPdfPath,
youtubeUrl: $youtubeUrl, youtubeUrl: $youtubeUrl,
parentElement: $parentElement, parentElement: $parentElement,
)); ));

View file

@ -10,7 +10,8 @@ class CreateElementRequest
public ?string $description, public ?string $description,
public ?string $iconImageUrl, public ?string $iconImageUrl,
public ?string $richText, public ?string $richText,
public ?string $pdfPath, public ?string $shortPdfPath,
public ?string $longPdfPath,
public ?string $youtubeUrl, public ?string $youtubeUrl,
public ?int $parentElementId, public ?int $parentElementId,
) { ) {

View file

@ -9,15 +9,23 @@ use App\Exceptions\NotFoundException;
class UpdateElement class UpdateElement
{ {
private const FILE_TYPE_ICON_IMAGE = 'iconImage';
private const FILE_TYPE_SHORT_PDF = 'shortPdf';
private const FILE_TYPE_LONG_PDF = 'longPdf';
public function __construct( public function __construct(
private UpdateTitle $updateTitle, private UpdateTitle $updateTitle,
private UpdateDescription $updateDescription, private UpdateDescription $updateDescription,
private UpdateIconImageUrl $updateIconImageUrl, private UpdateIconImageUrl $updateIconImageUrl,
private UpdateRichText $updateRichText, private UpdateRichText $updateRichText,
private UpdatePdfPath $updatePdfPath, private UpdateShortPdfPath $updateShortPdfPath,
private UpdateLongPdfPath $updateLongPdfPath,
private UpdateYoutubeUrl $updateYoutubeUrl, private UpdateYoutubeUrl $updateYoutubeUrl,
private UpdateIconImage $updateIconImage, private UpdateIconImage $updateIconImage,
private UpdatePdf $updatePdf, private UpdateShortPdf $updateShortPdf,
private UpdateLongPdf $updateLongPdf,
private ElementRepository $elementRepository, private ElementRepository $elementRepository,
) { ) {
} }
@ -36,10 +44,14 @@ class UpdateElement
&& $request->description === null && $request->description === null
&& $request->iconImageUrl === null && $request->iconImageUrl === null
&& $request->richText === null && $request->richText === null
&& $request->pdfPath === null && $request->shortPdfPath === null
&& $request->longPdfPath === null
&& $request->youtubeUrl === null && $request->youtubeUrl === null
&& $request->iconImageContents === null && $request->fileType === null
&& $request->pdfContents === null; && $request->fileContents === null
&& $request->fileOriginalName === null
&& $request->fileMimeType === null
&& $request->fileSizeBytes === null;
if ($hasNoFields) { if ($hasNoFields) {
throw new BadRequestException('nothing to update'); throw new BadRequestException('nothing to update');
} }
@ -70,10 +82,16 @@ class UpdateElement
richText: $request->richText, richText: $request->richText,
)); ));
} }
if ($request->pdfPath !== null) { if ($request->shortPdfPath !== null) {
$this->updatePdfPath->execute(new UpdatePdfPathRequest( $this->updateShortPdfPath->execute(new UpdateShortPdfPathRequest(
id: $request->id, id: $request->id,
pdfPath: $request->pdfPath, shortPdfPath: $request->shortPdfPath,
));
}
if ($request->longPdfPath !== null) {
$this->updateLongPdfPath->execute(new UpdateLongPdfPathRequest(
id: $request->id,
longPdfPath: $request->longPdfPath,
)); ));
} }
if ($request->youtubeUrl !== null) { if ($request->youtubeUrl !== null) {
@ -82,27 +100,8 @@ class UpdateElement
youtubeUrl: $request->youtubeUrl, youtubeUrl: $request->youtubeUrl,
)); ));
} }
if ($request->iconImageContents !== null) { if ($this->hasFileInput($request)) {
$this->updateIconImage->execute( $this->updateFile($request);
new UpdateIconImageRequest(
id: $request->id,
iconImageContents: $request->iconImageContents,
iconImageOriginalName: $request->iconImageOriginalName,
iconImageMimeType: $request->iconImageMimeType,
iconImageSizeBytes: $request->iconImageSizeBytes,
)
);
}
if ($request->pdfContents !== null) {
$this->updatePdf->execute(
new UpdatePdfRequest(
id: $request->id,
pdfContents: $request->pdfContents,
pdfOriginalName: $request->pdfOriginalName,
pdfMimeType: $request->pdfMimeType,
pdfSizeBytes: $request->pdfSizeBytes,
)
);
} }
$element = $this->elementRepository->find($request->id); $element = $this->elementRepository->find($request->id);
@ -112,4 +111,68 @@ class UpdateElement
return $element; return $element;
} }
/**
* @throws BadRequestException
* @throws NotFoundException
*/
private function updateFile(UpdateElementRequest $request): void
{
if ($request->fileType === null) {
throw new BadRequestException('fileType is required');
}
if ($request->fileType === self::FILE_TYPE_ICON_IMAGE) {
$this->updateIconImage->execute(
new UpdateIconImageRequest(
id: $request->id,
fileContents: $request->fileContents,
fileOriginalName: $request->fileOriginalName,
fileMimeType: $request->fileMimeType,
fileSizeBytes: $request->fileSizeBytes,
)
);
return;
}
if ($request->fileType === self::FILE_TYPE_SHORT_PDF) {
$this->updateShortPdf->execute(
new UpdateShortPdfRequest(
id: $request->id,
fileContents: $request->fileContents,
fileOriginalName: $request->fileOriginalName,
fileMimeType: $request->fileMimeType,
fileSizeBytes: $request->fileSizeBytes,
)
);
return;
}
if ($request->fileType === self::FILE_TYPE_LONG_PDF) {
$this->updateLongPdf->execute(
new UpdateLongPdfRequest(
id: $request->id,
fileContents: $request->fileContents,
fileOriginalName: $request->fileOriginalName,
fileMimeType: $request->fileMimeType,
fileSizeBytes: $request->fileSizeBytes,
)
);
return;
}
throw new BadRequestException('fileType is invalid');
}
private function hasFileInput(UpdateElementRequest $request): bool
{
return $request->fileType !== null
|| $request->fileContents !== null
|| $request->fileOriginalName !== null
|| $request->fileMimeType !== null
|| $request->fileSizeBytes !== null;
}
} }

View file

@ -10,16 +10,14 @@ class UpdateElementRequest
public ?string $description, public ?string $description,
public ?string $iconImageUrl, public ?string $iconImageUrl,
public ?string $richText, public ?string $richText,
public ?string $pdfPath, public ?string $shortPdfPath,
public ?string $longPdfPath,
public ?string $youtubeUrl, public ?string $youtubeUrl,
public ?string $iconImageContents, public ?string $fileType,
public ?string $iconImageOriginalName, public ?string $fileContents,
public ?string $iconImageMimeType, public ?string $fileOriginalName,
public ?int $iconImageSizeBytes, public ?string $fileMimeType,
public ?string $pdfContents, public ?int $fileSizeBytes,
public ?string $pdfOriginalName,
public ?string $pdfMimeType,
public ?int $pdfSizeBytes,
) { ) {
} }
} }

View file

@ -36,17 +36,17 @@ class UpdateIconImage
} }
if ( if (
$request->iconImageContents === null $request->fileContents === null
|| $request->iconImageOriginalName === null || $request->fileOriginalName === null
|| $request->iconImageMimeType === null || $request->fileMimeType === null
|| $request->iconImageSizeBytes === null || $request->fileSizeBytes === null
) { ) {
throw new BadRequestException('icon image is required'); throw new BadRequestException('icon image is required');
} }
if ( if (
! in_array( ! in_array(
$request->iconImageMimeType, $request->fileMimeType,
self::ALLOWED_MIME_TYPES, self::ALLOWED_MIME_TYPES,
true, true,
) )
@ -56,7 +56,7 @@ class UpdateIconImage
); );
} }
if ($request->iconImageSizeBytes > self::MAX_SIZE_BYTES) { if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) {
throw new BadRequestException( throw new BadRequestException(
'icon image must be 5MB or smaller', 'icon image must be 5MB or smaller',
); );
@ -68,10 +68,10 @@ class UpdateIconImage
} }
$iconImage = new FileToUpload( $iconImage = new FileToUpload(
contents: $request->iconImageContents, contents: $request->fileContents,
originalName: $request->iconImageOriginalName, originalName: $request->fileOriginalName,
mimeType: $request->iconImageMimeType, mimeType: $request->fileMimeType,
sizeBytes: $request->iconImageSizeBytes, sizeBytes: $request->fileSizeBytes,
); );
$path = $this->fileUploader->upload($iconImage, 'element-icons'); $path = $this->fileUploader->upload($iconImage, 'element-icons');
$element->setIconImageUrl($path); $element->setIconImageUrl($path);

View file

@ -6,10 +6,10 @@ class UpdateIconImageRequest
{ {
public function __construct( public function __construct(
public ?int $id, public ?int $id,
public ?string $iconImageContents, public ?string $fileContents,
public ?string $iconImageOriginalName, public ?string $fileOriginalName,
public ?string $iconImageMimeType, public ?string $fileMimeType,
public ?int $iconImageSizeBytes, public ?int $fileSizeBytes,
) { ) {
} }
} }

View file

@ -9,7 +9,7 @@ use App\Exceptions\NotFoundException;
use App\Shared\Files\FileToUpload; use App\Shared\Files\FileToUpload;
use App\Shared\Files\FileUploader; use App\Shared\Files\FileUploader;
class UpdatePdf class UpdateLongPdf
{ {
private const ALLOWED_MIME_TYPES = [ private const ALLOWED_MIME_TYPES = [
'application/pdf', 'application/pdf',
@ -27,24 +27,24 @@ class UpdatePdf
* @throws BadRequestException * @throws BadRequestException
* @throws NotFoundException * @throws NotFoundException
*/ */
public function execute(UpdatePdfRequest $request): Element public function execute(UpdateLongPdfRequest $request): Element
{ {
if ($request->id === null) { if ($request->id === null) {
throw new BadRequestException('id is required'); throw new BadRequestException('id is required');
} }
if ( if (
$request->pdfContents === null $request->fileContents === null
|| $request->pdfOriginalName === null || $request->fileOriginalName === null
|| $request->pdfMimeType === null || $request->fileMimeType === null
|| $request->pdfSizeBytes === null || $request->fileSizeBytes === null
) { ) {
throw new BadRequestException('pdf is required'); throw new BadRequestException('pdf is required');
} }
if ( if (
! in_array( ! in_array(
$request->pdfMimeType, $request->fileMimeType,
self::ALLOWED_MIME_TYPES, self::ALLOWED_MIME_TYPES,
true, true,
) )
@ -52,7 +52,7 @@ class UpdatePdf
throw new BadRequestException('pdf must be a pdf file'); throw new BadRequestException('pdf must be a pdf file');
} }
if ($request->pdfSizeBytes > self::MAX_SIZE_BYTES) { if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) {
throw new BadRequestException('pdf must be 10MB or smaller'); throw new BadRequestException('pdf must be 10MB or smaller');
} }
@ -62,13 +62,13 @@ class UpdatePdf
} }
$pdf = new FileToUpload( $pdf = new FileToUpload(
contents: $request->pdfContents, contents: $request->fileContents,
originalName: $request->pdfOriginalName, originalName: $request->fileOriginalName,
mimeType: $request->pdfMimeType, mimeType: $request->fileMimeType,
sizeBytes: $request->pdfSizeBytes, sizeBytes: $request->fileSizeBytes,
); );
$path = $this->fileUploader->upload($pdf, 'element-pdfs'); $path = $this->fileUploader->upload($pdf, 'element-pdfs/long');
$element->setPdfPath($path); $element->setLongPdfPath($path);
return $this->elementRepository->update($element); return $this->elementRepository->update($element);
} }

View file

@ -8,7 +8,7 @@ use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException; use App\Exceptions\NotFoundException;
use App\Shared\Files\FileUploader; use App\Shared\Files\FileUploader;
class UpdatePdfPath class UpdateLongPdfPath
{ {
public function __construct( public function __construct(
private ElementRepository $elementRepository, private ElementRepository $elementRepository,
@ -20,14 +20,14 @@ class UpdatePdfPath
* @throws BadRequestException * @throws BadRequestException
* @throws NotFoundException * @throws NotFoundException
*/ */
public function execute(UpdatePdfPathRequest $request): Element public function execute(UpdateLongPdfPathRequest $request): Element
{ {
if ($request->id === null) { if ($request->id === null) {
throw new BadRequestException('id is required'); throw new BadRequestException('id is required');
} }
if ($request->pdfPath === null) { if ($request->longPdfPath === null) {
throw new BadRequestException('pdfPath is required'); throw new BadRequestException('longPdfPath is required');
} }
$element = $this->elementRepository->find($request->id); $element = $this->elementRepository->find($request->id);
@ -35,12 +35,12 @@ class UpdatePdfPath
throw new NotFoundException('Element not found'); throw new NotFoundException('Element not found');
} }
$pdfPath = $this->nullableString($request->pdfPath); $longPdfPath = $this->nullableString($request->longPdfPath);
if ($pdfPath === null) { if ($longPdfPath === null) {
$this->deleteManagedFile($element->getPdfPath()); $this->deleteManagedFile($element->getLongPdfPath());
} }
$element->setPdfPath($pdfPath); $element->setLongPdfPath($longPdfPath);
return $this->elementRepository->update($element); return $this->elementRepository->update($element);
} }

View file

@ -2,11 +2,11 @@
namespace App\Element\UseCases\UpdateElement; namespace App\Element\UseCases\UpdateElement;
class UpdatePdfPathRequest class UpdateLongPdfPathRequest
{ {
public function __construct( public function __construct(
public ?int $id, public ?int $id,
public ?string $pdfPath, public ?string $longPdfPath,
) { ) {
} }
} }

View file

@ -0,0 +1,15 @@
<?php
namespace App\Element\UseCases\UpdateElement;
class UpdateLongPdfRequest
{
public function __construct(
public ?int $id,
public ?string $fileContents,
public ?string $fileOriginalName,
public ?string $fileMimeType,
public ?int $fileSizeBytes,
) {
}
}

View file

@ -1,15 +0,0 @@
<?php
namespace App\Element\UseCases\UpdateElement;
class UpdatePdfRequest
{
public function __construct(
public ?int $id,
public ?string $pdfContents,
public ?string $pdfOriginalName,
public ?string $pdfMimeType,
public ?int $pdfSizeBytes,
) {
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace App\Element\UseCases\UpdateElement;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Shared\Files\FileToUpload;
use App\Shared\Files\FileUploader;
class UpdateShortPdf
{
private const ALLOWED_MIME_TYPES = [
'application/pdf',
];
private const MAX_SIZE_BYTES = 10 * 1024 * 1024;
public function __construct(
private ElementRepository $elementRepository,
private FileUploader $fileUploader,
) {
}
/**
* @throws BadRequestException
* @throws NotFoundException
*/
public function execute(UpdateShortPdfRequest $request): Element
{
if ($request->id === null) {
throw new BadRequestException('id is required');
}
if (
$request->fileContents === null
|| $request->fileOriginalName === null
|| $request->fileMimeType === null
|| $request->fileSizeBytes === null
) {
throw new BadRequestException('pdf is required');
}
if (
! in_array(
$request->fileMimeType,
self::ALLOWED_MIME_TYPES,
true,
)
) {
throw new BadRequestException('pdf must be a pdf file');
}
if ($request->fileSizeBytes > self::MAX_SIZE_BYTES) {
throw new BadRequestException('pdf must be 10MB or smaller');
}
$element = $this->elementRepository->find($request->id);
if ($element === null) {
throw new NotFoundException('Element not found');
}
$pdf = new FileToUpload(
contents: $request->fileContents,
originalName: $request->fileOriginalName,
mimeType: $request->fileMimeType,
sizeBytes: $request->fileSizeBytes,
);
$path = $this->fileUploader->upload($pdf, 'element-pdfs/short');
$element->setShortPdfPath($path);
return $this->elementRepository->update($element);
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Element\UseCases\UpdateElement;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Shared\Files\FileUploader;
class UpdateShortPdfPath
{
public function __construct(
private ElementRepository $elementRepository,
private FileUploader $fileUploader,
) {
}
/**
* @throws BadRequestException
* @throws NotFoundException
*/
public function execute(UpdateShortPdfPathRequest $request): Element
{
if ($request->id === null) {
throw new BadRequestException('id is required');
}
if ($request->shortPdfPath === null) {
throw new BadRequestException('shortPdfPath is required');
}
$element = $this->elementRepository->find($request->id);
if ($element === null) {
throw new NotFoundException('Element not found');
}
$shortPdfPath = $this->nullableString($request->shortPdfPath);
if ($shortPdfPath === null) {
$this->deleteManagedFile($element->getShortPdfPath());
}
$element->setShortPdfPath($shortPdfPath);
return $this->elementRepository->update($element);
}
private function nullableString(string $value): ?string
{
if ($value === '') {
return null;
}
return $value;
}
private function deleteManagedFile(?string $path): void
{
if ($path === null) {
return;
}
$this->fileUploader->delete($path);
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace App\Element\UseCases\UpdateElement;
class UpdateShortPdfPathRequest
{
public function __construct(
public ?int $id,
public ?string $shortPdfPath,
) {
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Element\UseCases\UpdateElement;
class UpdateShortPdfRequest
{
public function __construct(
public ?int $id,
public ?string $fileContents,
public ?string $fileOriginalName,
public ?string $fileMimeType,
public ?int $fileSizeBytes,
) {
}
}

View file

@ -15,7 +15,8 @@ return new class extends Migration
$table->text('description')->default(''); $table->text('description')->default('');
$table->string('icon_image_url')->nullable(); $table->string('icon_image_url')->nullable();
$table->text('rich_text')->default(''); $table->text('rich_text')->default('');
$table->string('pdf_path')->nullable(); $table->string('short_pdf_path')->nullable();
$table->string('long_pdf_path')->nullable();
$table->string('youtube_url')->nullable(); $table->string('youtube_url')->nullable();
$table->foreignId('parent_element_id') $table->foreignId('parent_element_id')
->nullable() ->nullable()

View file

@ -20,7 +20,7 @@ class ElementSeeder extends Seeder
'element-icons/baderech-haavodah-icon.png', 'element-icons/baderech-haavodah-icon.png',
); );
$introductionPdfPath = $this->seedStorageFile( $introductionPdfPath = $this->seedStorageFile(
'element-pdfs/baderech.pdf', 'element-pdfs/short/baderech.pdf',
); );
$rootElement = $elementRepository->create(new CreateElementDto( $rootElement = $elementRepository->create(new CreateElementDto(
@ -29,7 +29,8 @@ class ElementSeeder extends Seeder
description: $baderechSet->getDescription(), description: $baderechSet->getDescription(),
iconImageUrl: $rootIconImageUrl, iconImageUrl: $rootIconImageUrl,
richText: '', richText: '',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElement: null, parentElement: null,
)); ));
@ -44,7 +45,7 @@ class ElementSeeder extends Seeder
. 'unlock the ability to live with strength, confidence, ' . 'unlock the ability to live with strength, confidence, '
. 'and hope.', . 'and hope.',
'richText' => $this->introductionRichText(), 'richText' => $this->introductionRichText(),
'pdfPath' => $introductionPdfPath, 'shortPdfPath' => $introductionPdfPath,
], ],
[ [
'title' => '2. Foundations', 'title' => '2. Foundations',
@ -54,7 +55,7 @@ class ElementSeeder extends Seeder
. 'ability to uplift the physical world with spiritual ' . 'ability to uplift the physical world with spiritual '
. 'intention.', . 'intention.',
'richText' => '', 'richText' => '',
'pdfPath' => null, 'shortPdfPath' => null,
], ],
[ [
'title' => '3. Divine Plan', 'title' => '3. Divine Plan',
@ -62,14 +63,14 @@ class ElementSeeder extends Seeder
. 'generation and the call to remain hopeful, ' . 'generation and the call to remain hopeful, '
. 'optimistic, and resilient.', . 'optimistic, and resilient.',
'richText' => '', 'richText' => '',
'pdfPath' => null, 'shortPdfPath' => null,
], ],
[ [
'title' => '4. Architecture of the Soul', 'title' => '4. Architecture of the Soul',
'description' => "Becoming a Baal Da'at - A Master of " 'description' => "Becoming a Baal Da'at - A Master of "
. 'Inner Awareness', . 'Inner Awareness',
'richText' => '', 'richText' => '',
'pdfPath' => null, 'shortPdfPath' => null,
], ],
[ [
'title' => '5. Arba Yesodot', 'title' => '5. Arba Yesodot',
@ -77,7 +78,7 @@ class ElementSeeder extends Seeder
. 'Physical & Financial Health, Relationships, Wisdom ' . 'Physical & Financial Health, Relationships, Wisdom '
. 'and Guidance.', . 'and Guidance.',
'richText' => '', 'richText' => '',
'pdfPath' => null, 'shortPdfPath' => null,
], ],
[ [
'title' => '6. Fluid Integration', 'title' => '6. Fluid Integration',
@ -85,7 +86,7 @@ class ElementSeeder extends Seeder
. 'engender a shift in mindset, embed healthy routines, ' . 'engender a shift in mindset, embed healthy routines, '
. 'positive life practices.', . 'positive life practices.',
'richText' => '', 'richText' => '',
'pdfPath' => null, 'shortPdfPath' => null,
], ],
]; ];
@ -97,7 +98,8 @@ class ElementSeeder extends Seeder
description: $baderechChildElement['description'], description: $baderechChildElement['description'],
iconImageUrl: null, iconImageUrl: null,
richText: $baderechChildElement['richText'], richText: $baderechChildElement['richText'],
pdfPath: $baderechChildElement['pdfPath'], shortPdfPath: $baderechChildElement['shortPdfPath'],
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElement: $rootElement, parentElement: $rootElement,
)); ));
@ -117,7 +119,8 @@ class ElementSeeder extends Seeder
description: '', description: '',
iconImageUrl: null, iconImageUrl: null,
richText: '', richText: '',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: 'https://www.youtube.com/watch?v=' youtubeUrl: 'https://www.youtube.com/watch?v='
. 'yHx-r4p6hHU&t=1s', . 'yHx-r4p6hHU&t=1s',
parentElement: $introductionElement, parentElement: $introductionElement,

View file

@ -23,7 +23,8 @@ class FakeElementRepository implements ElementRepository
description: $dto->description, description: $dto->description,
iconImageUrl: $dto->iconImageUrl, iconImageUrl: $dto->iconImageUrl,
richText: $dto->richText, richText: $dto->richText,
pdfPath: $dto->pdfPath, shortPdfPath: $dto->shortPdfPath,
longPdfPath: $dto->longPdfPath,
youtubeUrl: $dto->youtubeUrl, youtubeUrl: $dto->youtubeUrl,
set: $dto->set, set: $dto->set,
parentElement: $dto->parentElement, parentElement: $dto->parentElement,
@ -111,7 +112,8 @@ class FakeElementRepository implements ElementRepository
description: $element->getDescription(), description: $element->getDescription(),
iconImageUrl: $element->getIconImageUrl(), iconImageUrl: $element->getIconImageUrl(),
richText: $element->getRichText(), richText: $element->getRichText(),
pdfPath: $element->getPdfPath(), shortPdfPath: $element->getShortPdfPath(),
longPdfPath: $element->getLongPdfPath(),
youtubeUrl: $element->getYoutubeUrl(), youtubeUrl: $element->getYoutubeUrl(),
set: $element->getSet(), set: $element->getSet(),
parentElement: $parentElement, parentElement: $parentElement,

View file

@ -43,7 +43,8 @@ class DemoSeedDataTest extends TestCase
Storage::disk('public')->assertExists( Storage::disk('public')->assertExists(
'element-icons/baderech-haavodah-icon.png', 'element-icons/baderech-haavodah-icon.png',
); );
$this->assertNull($rootElement->getPdfPath()); $this->assertNull($rootElement->getShortPdfPath());
$this->assertNull($rootElement->getLongPdfPath());
$this->assertNull($rootElement->getYoutubeUrl()); $this->assertNull($rootElement->getYoutubeUrl());
$this->assertCount(6, $childElements); $this->assertCount(6, $childElements);
$this->assertSame( $this->assertSame(
@ -96,10 +97,13 @@ class DemoSeedDataTest extends TestCase
$childElements[0]->getRichText(), $childElements[0]->getRichText(),
); );
$this->assertSame( $this->assertSame(
'element-pdfs/baderech.pdf', 'element-pdfs/short/baderech.pdf',
$childElements[0]->getPdfPath(), $childElements[0]->getShortPdfPath(),
);
$this->assertNull($childElements[0]->getLongPdfPath());
Storage::disk('public')->assertExists(
'element-pdfs/short/baderech.pdf',
); );
Storage::disk('public')->assertExists('element-pdfs/baderech.pdf');
$introductionChildElements = $elementRepository->findByParentElement( $introductionChildElements = $elementRepository->findByParentElement(
$childElements[0], $childElements[0],
@ -114,7 +118,8 @@ class DemoSeedDataTest extends TestCase
$this->assertSame('', $introductionAudioElement->getDescription()); $this->assertSame('', $introductionAudioElement->getDescription());
$this->assertNull($introductionAudioElement->getIconImageUrl()); $this->assertNull($introductionAudioElement->getIconImageUrl());
$this->assertSame('', $introductionAudioElement->getRichText()); $this->assertSame('', $introductionAudioElement->getRichText());
$this->assertNull($introductionAudioElement->getPdfPath()); $this->assertNull($introductionAudioElement->getShortPdfPath());
$this->assertNull($introductionAudioElement->getLongPdfPath());
$this->assertSame( $this->assertSame(
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
$introductionAudioElement->getYoutubeUrl(), $introductionAudioElement->getYoutubeUrl(),

View file

@ -5,8 +5,10 @@ namespace Tests\Feature;
use App\Auth\CreateSessionDto; use App\Auth\CreateSessionDto;
use App\Auth\SessionRepository; use App\Auth\SessionRepository;
use App\Element\CreateElementDto; use App\Element\CreateElementDto;
use App\Element\Element;
use App\Element\ElementRepository; use App\Element\ElementRepository;
use App\Set\CreateSetDto; use App\Set\CreateSetDto;
use App\Set\Set;
use App\Set\SetRepository; use App\Set\SetRepository;
use App\Shared\ValueObject\EmailAddress; use App\Shared\ValueObject\EmailAddress;
use App\User\CreateUserDto; use App\User\CreateUserDto;
@ -28,41 +30,43 @@ class ElementsEndpointTest extends TestCase
. 'yHx-r4p6hHU&t=1s'; . 'yHx-r4p6hHU&t=1s';
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, '/assets/baderech-haavodah-icon.png',
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', '/assets/pdfs/baderech.pdf',
iconImageUrl: '/assets/baderech-haavodah-icon.png', '/assets/pdfs/baderech-long.pdf',
richText: '<p>A structured path for growth</p>', $sampleYoutubeUrl,
pdfPath: '/assets/pdfs/baderech.pdf', null,
youtubeUrl: $sampleYoutubeUrl, );
parentElement: null, $firstChildElement = $this->createElement(
)); $elementRepository,
$firstChildElement = $elementRepository->create(new CreateElementDto( $set,
set: $set, 'Avodah Foundations',
title: 'Avodah Foundations', 'Foundations for steady avodah',
description: 'Foundations for steady avodah', null,
iconImageUrl: null, '<p>Foundations rich text</p>',
richText: '<p>Foundations rich text</p>', '/assets/pdfs/foundations.pdf',
pdfPath: '/assets/pdfs/foundations.pdf', null,
youtubeUrl: null, null,
parentElement: $element, $element,
)); );
$secondChildElement = $elementRepository->create(new CreateElementDto( $secondChildElement = $this->createElement(
set: $set, $elementRepository,
title: 'Daily Practice', $set,
description: 'Daily practices for growth', 'Daily Practice',
iconImageUrl: null, 'Daily practices for growth',
richText: '<p>Daily practice rich text</p>', null,
pdfPath: null, '<p>Daily practice rich text</p>',
youtubeUrl: null, null,
parentElement: $element, null,
)); null,
$element,
);
$response = $this->getJson("/api/elements/{$element->getId()}"); $response = $this->getJson("/api/elements/{$element->getId()}");
@ -86,7 +90,8 @@ class ElementsEndpointTest extends TestCase
'description' => 'A structured path for growth', 'description' => 'A structured path for growth',
'iconImageUrl' => '/assets/baderech-haavodah-icon.png', 'iconImageUrl' => '/assets/baderech-haavodah-icon.png',
'richText' => '<p>A structured path for growth</p>', 'richText' => '<p>A structured path for growth</p>',
'pdfPath' => '/assets/pdfs/baderech.pdf', 'shortPdfPath' => '/assets/pdfs/baderech.pdf',
'longPdfPath' => '/assets/pdfs/baderech-long.pdf',
'youtubeUrl' => $sampleYoutubeUrl, 'youtubeUrl' => $sampleYoutubeUrl,
], ],
]); ]);
@ -106,21 +111,19 @@ class ElementsEndpointTest extends TestCase
{ {
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, '/assets/baderech-haavodah-icon.png',
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', '/assets/pdfs/baderech.pdf',
iconImageUrl: '/assets/baderech-haavodah-icon.png', null,
richText: '<p>A structured path for growth</p>', null,
pdfPath: '/assets/pdfs/baderech.pdf', null,
youtubeUrl: null, );
parentElement: null,
));
$response = $this->postJson('/api/element/update', [ $response = $this->postJson('/api/element/update', [
'elementId' => $element->getId(), 'elementId' => $element->getId(),
@ -128,7 +131,8 @@ class ElementsEndpointTest extends TestCase
'description' => 'Updated description', 'description' => 'Updated description',
'iconImageUrl' => '/assets/updated-icon.png', 'iconImageUrl' => '/assets/updated-icon.png',
'richText' => '<p>Updated rich text</p>', 'richText' => '<p>Updated rich text</p>',
'pdfPath' => '/assets/pdfs/updated.pdf', 'shortPdfPath' => '/assets/pdfs/updated.pdf',
'longPdfPath' => '/assets/pdfs/updated-long.pdf',
'youtubeUrl' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'youtubeUrl' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
]); ]);
@ -144,21 +148,19 @@ class ElementsEndpointTest extends TestCase
. 'dQw4w9WgXcQ'; . 'dQw4w9WgXcQ';
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, '/assets/baderech-haavodah-icon.png',
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', '/assets/pdfs/baderech.pdf',
iconImageUrl: '/assets/baderech-haavodah-icon.png', null,
richText: '<p>A structured path for growth</p>', null,
pdfPath: '/assets/pdfs/baderech.pdf', null,
youtubeUrl: null, );
parentElement: null,
));
$this->createSession('valid-token'); $this->createSession('valid-token');
$response = $this->withCredentials() $response = $this->withCredentials()
@ -169,7 +171,8 @@ class ElementsEndpointTest extends TestCase
'description' => 'Updated description', 'description' => 'Updated description',
'iconImageUrl' => '/assets/updated-icon.png', 'iconImageUrl' => '/assets/updated-icon.png',
'richText' => '<p>Updated rich text</p>', 'richText' => '<p>Updated rich text</p>',
'pdfPath' => '/assets/pdfs/updated.pdf', 'shortPdfPath' => '/assets/pdfs/updated.pdf',
'longPdfPath' => '/assets/pdfs/updated-long.pdf',
'youtubeUrl' => $sampleYoutubeUrl, 'youtubeUrl' => $sampleYoutubeUrl,
]); ]);
@ -181,7 +184,8 @@ class ElementsEndpointTest extends TestCase
'description' => 'Updated description', 'description' => 'Updated description',
'iconImageUrl' => '/assets/updated-icon.png', 'iconImageUrl' => '/assets/updated-icon.png',
'richText' => '<p>Updated rich text</p>', 'richText' => '<p>Updated rich text</p>',
'pdfPath' => '/assets/pdfs/updated.pdf', 'shortPdfPath' => '/assets/pdfs/updated.pdf',
'longPdfPath' => '/assets/pdfs/updated-long.pdf',
'youtubeUrl' => $sampleYoutubeUrl, 'youtubeUrl' => $sampleYoutubeUrl,
], ],
]); ]);
@ -191,6 +195,42 @@ class ElementsEndpointTest extends TestCase
'<p>Updated rich text</p>', '<p>Updated rich text</p>',
$updatedElement->getRichText(), $updatedElement->getRichText(),
); );
$this->assertSame(
'/assets/pdfs/updated-long.pdf',
$updatedElement->getLongPdfPath(),
);
}
public function testAuthenticatedUpdateRejectsLegacyPdfPath(): void
{
$setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class);
$set = $this->createSet($setRepository);
$element = $this->createElement(
$elementRepository,
$set,
'Baderech HaAvodah',
'A structured path for growth',
null,
'<p>A structured path for growth</p>',
'/assets/pdfs/baderech.pdf',
'/assets/pdfs/baderech-long.pdf',
null,
null,
);
$this->createSession('valid-token');
$response = $this->withCredentials()
->withUnencryptedCookie('auth_token', 'valid-token')
->postJson('/api/element/update', [
'elementId' => $element->getId(),
'pdfPath' => '/assets/pdfs/legacy-updated.pdf',
]);
$response->assertBadRequest();
$response->assertExactJson([
'error' => 'nothing to update',
]);
} }
public function testAuthenticatedUpdateClearsUploadedMedia(): void public function testAuthenticatedUpdateClearsUploadedMedia(): void
@ -201,26 +241,28 @@ class ElementsEndpointTest extends TestCase
'icon bytes', 'icon bytes',
); );
Storage::disk('public')->put( Storage::disk('public')->put(
'element-pdfs/original.pdf', 'element-pdfs/short/original.pdf',
'pdf bytes', 'short pdf bytes',
);
Storage::disk('public')->put(
'element-pdfs/long/original-long.pdf',
'long pdf bytes',
); );
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, 'element-icons/original-icon.png',
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', 'element-pdfs/short/original.pdf',
iconImageUrl: 'element-icons/original-icon.png', 'element-pdfs/long/original-long.pdf',
richText: '<p>A structured path for growth</p>', null,
pdfPath: 'element-pdfs/original.pdf', null,
youtubeUrl: null, );
parentElement: null,
));
$this->createSession('valid-token'); $this->createSession('valid-token');
$response = $this->withCredentials() $response = $this->withCredentials()
@ -228,22 +270,29 @@ class ElementsEndpointTest extends TestCase
->postJson('/api/element/update', [ ->postJson('/api/element/update', [
'elementId' => $element->getId(), 'elementId' => $element->getId(),
'iconImageUrl' => '', 'iconImageUrl' => '',
'pdfPath' => '', 'shortPdfPath' => '',
'longPdfPath' => '',
]); ]);
$response->assertOk(); $response->assertOk();
$body = $response->json(); $body = $response->json();
$this->assertNull($body['element']['iconImageUrl']); $this->assertNull($body['element']['iconImageUrl']);
$this->assertNull($body['element']['pdfPath']); $this->assertNull($body['element']['shortPdfPath']);
$this->assertNull($body['element']['longPdfPath']);
$this->assertArrayNotHasKey('pdfPath', $body['element']);
$updatedElement = $elementRepository->find($element->getId()); $updatedElement = $elementRepository->find($element->getId());
$this->assertNotNull($updatedElement); $this->assertNotNull($updatedElement);
$this->assertNull($updatedElement->getIconImageUrl()); $this->assertNull($updatedElement->getIconImageUrl());
$this->assertNull($updatedElement->getPdfPath()); $this->assertNull($updatedElement->getShortPdfPath());
$this->assertNull($updatedElement->getLongPdfPath());
Storage::disk('public')->assertMissing( Storage::disk('public')->assertMissing(
'element-icons/original-icon.png', 'element-icons/original-icon.png',
); );
Storage::disk('public')->assertMissing( Storage::disk('public')->assertMissing(
'element-pdfs/original.pdf', 'element-pdfs/short/original.pdf',
);
Storage::disk('public')->assertMissing(
'element-pdfs/long/original-long.pdf',
); );
} }
@ -251,27 +300,26 @@ class ElementsEndpointTest extends TestCase
{ {
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, null,
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', null,
iconImageUrl: null, null,
richText: '<p>A structured path for growth</p>', null,
pdfPath: null, null,
youtubeUrl: null, );
parentElement: null,
));
$response = $this->post( $response = $this->post(
'/api/element/update', '/api/element/update',
[ [
'elementId' => (string) $element->getId(), 'elementId' => (string) $element->getId(),
'iconImage' => $this->iconImageUpload(), 'fileType' => 'iconImage',
'file' => $this->iconImageUpload(),
], ],
); );
@ -281,26 +329,24 @@ class ElementsEndpointTest extends TestCase
]); ]);
} }
public function testAuthenticatedIconImageUploadReturnsElementPayload(): void public function testAuthenticatedIconImageUploadReturnsPayload(): void
{ {
Storage::fake('public'); Storage::fake('public');
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, null,
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', null,
iconImageUrl: null, null,
richText: '<p>A structured path for growth</p>', null,
pdfPath: null, null,
youtubeUrl: null, );
parentElement: null,
));
$this->createSession('valid-token'); $this->createSession('valid-token');
$response = $this->withCredentials() $response = $this->withCredentials()
@ -309,7 +355,8 @@ class ElementsEndpointTest extends TestCase
'/api/element/update', '/api/element/update',
[ [
'elementId' => (string) $element->getId(), 'elementId' => (string) $element->getId(),
'iconImage' => $this->iconImageUpload(), 'fileType' => 'iconImage',
'file' => $this->iconImageUpload(),
], ],
); );
@ -331,31 +378,30 @@ class ElementsEndpointTest extends TestCase
Storage::disk('public')->assertExists($updatedIconImageUrl); Storage::disk('public')->assertExists($updatedIconImageUrl);
} }
public function testPdfUploadRequiresAuthentication(): void public function testShortPdfUploadRequiresAuthentication(): void
{ {
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, null,
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', null,
iconImageUrl: null, null,
richText: '<p>A structured path for growth</p>', null,
pdfPath: null, null,
youtubeUrl: null, );
parentElement: null,
));
$response = $this->post( $response = $this->post(
'/api/element/update', '/api/element/update',
[ [
'elementId' => (string) $element->getId(), 'elementId' => (string) $element->getId(),
'pdf' => $this->pdfUpload(), 'fileType' => 'shortPdf',
'file' => $this->pdfUpload(),
], ],
); );
@ -365,26 +411,24 @@ class ElementsEndpointTest extends TestCase
]); ]);
} }
public function testAuthenticatedPdfUploadReturnsElementPayload(): void public function testAuthenticatedShortPdfUploadReturnsElementPayload(): void
{ {
Storage::fake('public'); Storage::fake('public');
$setRepository = app(SetRepository::class); $setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class); $elementRepository = app(ElementRepository::class);
$set = $setRepository->create(new CreateSetDto( $set = $this->createSet($setRepository);
name: 'Baderech HaAvodah', $element = $this->createElement(
description: 'A structured path for growth', $elementRepository,
iconImageUrl: '/assets/baderech-haavodah-icon.png', $set,
)); 'Baderech HaAvodah',
$element = $elementRepository->create(new CreateElementDto( 'A structured path for growth',
set: $set, null,
title: 'Baderech HaAvodah', '<p>A structured path for growth</p>',
description: 'A structured path for growth', null,
iconImageUrl: null, null,
richText: '<p>A structured path for growth</p>', null,
pdfPath: null, null,
youtubeUrl: null, );
parentElement: null,
));
$this->createSession('valid-token'); $this->createSession('valid-token');
$response = $this->withCredentials() $response = $this->withCredentials()
@ -393,7 +437,8 @@ class ElementsEndpointTest extends TestCase
'/api/element/update', '/api/element/update',
[ [
'elementId' => (string) $element->getId(), 'elementId' => (string) $element->getId(),
'pdf' => $this->pdfUpload(), 'fileType' => 'shortPdf',
'file' => $this->pdfUpload(),
], ],
); );
@ -401,17 +446,104 @@ class ElementsEndpointTest extends TestCase
$body = $response->json(); $body = $response->json();
$this->assertSame($element->getId(), $body['element']['id']); $this->assertSame($element->getId(), $body['element']['id']);
$this->assertStringContainsString( $this->assertStringContainsString(
'/storage/element-pdfs/', '/storage/element-pdfs/short/',
$body['element']['pdfPath'], $body['element']['shortPdfPath'],
);
$this->assertArrayNotHasKey('pdfPath', $body['element']);
$updatedElement = $elementRepository->find($element->getId());
$this->assertNotNull($updatedElement);
$updatedPdfPath = $updatedElement->getShortPdfPath();
$this->assertIsString($updatedPdfPath);
$this->assertStringStartsWith(
'element-pdfs/short/',
$updatedPdfPath,
);
Storage::disk('public')->assertExists($updatedPdfPath);
}
public function testAuthenticatedLongPdfUploadReturnsElementPayload(): void
{
Storage::fake('public');
$setRepository = app(SetRepository::class);
$elementRepository = app(ElementRepository::class);
$set = $this->createSet($setRepository);
$element = $this->createElement(
$elementRepository,
$set,
'Baderech HaAvodah',
'A structured path for growth',
null,
'<p>A structured path for growth</p>',
null,
null,
null,
null,
);
$this->createSession('valid-token');
$response = $this->withCredentials()
->withUnencryptedCookie('auth_token', 'valid-token')
->post(
'/api/element/update',
[
'elementId' => (string) $element->getId(),
'fileType' => 'longPdf',
'file' => $this->pdfUpload(),
],
);
$response->assertOk();
$body = $response->json();
$this->assertSame($element->getId(), $body['element']['id']);
$this->assertStringContainsString(
'/storage/element-pdfs/long/',
$body['element']['longPdfPath'],
); );
$updatedElement = $elementRepository->find($element->getId()); $updatedElement = $elementRepository->find($element->getId());
$this->assertNotNull($updatedElement); $this->assertNotNull($updatedElement);
$updatedPdfPath = $updatedElement->getPdfPath(); $updatedPdfPath = $updatedElement->getLongPdfPath();
$this->assertIsString($updatedPdfPath); $this->assertIsString($updatedPdfPath);
$this->assertStringStartsWith('element-pdfs/', $updatedPdfPath); $this->assertStringStartsWith(
'element-pdfs/long/',
$updatedPdfPath,
);
Storage::disk('public')->assertExists($updatedPdfPath); Storage::disk('public')->assertExists($updatedPdfPath);
} }
private function createSet(SetRepository $setRepository): Set
{
return $setRepository->create(new CreateSetDto(
name: 'Baderech HaAvodah',
description: 'A structured path for growth',
iconImageUrl: '/assets/baderech-haavodah-icon.png',
));
}
private function createElement(
ElementRepository $elementRepository,
Set $set,
string $title,
string $description,
?string $iconImageUrl,
string $richText,
?string $shortPdfPath,
?string $longPdfPath,
?string $youtubeUrl,
?Element $parentElement,
): Element {
return $elementRepository->create(new CreateElementDto(
set: $set,
title: $title,
description: $description,
iconImageUrl: $iconImageUrl,
richText: $richText,
shortPdfPath: $shortPdfPath,
longPdfPath: $longPdfPath,
youtubeUrl: $youtubeUrl,
parentElement: $parentElement,
));
}
private function createSession(string $token): void private function createSession(string $token): void
{ {
$userRepository = app(UserRepository::class); $userRepository = app(UserRepository::class);

View file

@ -34,7 +34,8 @@ class SetsEndpointTest extends TestCase
description: $baderechSet->getDescription(), description: $baderechSet->getDescription(),
iconImageUrl: null, iconImageUrl: null,
richText: '', richText: '',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElement: null, parentElement: null,
) )

View file

@ -10,9 +10,11 @@ use App\Element\UseCases\UpdateElement\UpdateDescription;
use App\Element\UseCases\UpdateElement\UpdateElement; use App\Element\UseCases\UpdateElement\UpdateElement;
use App\Element\UseCases\UpdateElement\UpdateIconImage; use App\Element\UseCases\UpdateElement\UpdateIconImage;
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl; use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
use App\Element\UseCases\UpdateElement\UpdatePdf; use App\Element\UseCases\UpdateElement\UpdateLongPdf;
use App\Element\UseCases\UpdateElement\UpdatePdfPath; use App\Element\UseCases\UpdateElement\UpdateLongPdfPath;
use App\Element\UseCases\UpdateElement\UpdateRichText; use App\Element\UseCases\UpdateElement\UpdateRichText;
use App\Element\UseCases\UpdateElement\UpdateShortPdf;
use App\Element\UseCases\UpdateElement\UpdateShortPdfPath;
use App\Element\UseCases\UpdateElement\UpdateTitle; use App\Element\UseCases\UpdateElement\UpdateTitle;
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl; use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
use App\Set\Set as DomainSet; use App\Set\Set as DomainSet;
@ -40,13 +42,18 @@ class ElementControllerTest extends TestCase
new UpdateDescription($this->elementRepo), new UpdateDescription($this->elementRepo),
new UpdateIconImageUrl($this->elementRepo, $this->fileUploader), new UpdateIconImageUrl($this->elementRepo, $this->fileUploader),
new UpdateRichText($this->elementRepo), new UpdateRichText($this->elementRepo),
new UpdatePdfPath($this->elementRepo, $this->fileUploader), new UpdateShortPdfPath($this->elementRepo, $this->fileUploader),
new UpdateLongPdfPath($this->elementRepo, $this->fileUploader),
new UpdateYoutubeUrl($this->elementRepo), new UpdateYoutubeUrl($this->elementRepo),
new UpdateIconImage( new UpdateIconImage(
$this->elementRepo, $this->elementRepo,
$this->fileUploader, $this->fileUploader,
), ),
new UpdatePdf( new UpdateShortPdf(
$this->elementRepo,
$this->fileUploader,
),
new UpdateLongPdf(
$this->elementRepo, $this->elementRepo,
$this->fileUploader, $this->fileUploader,
), ),
@ -69,6 +76,7 @@ class ElementControllerTest extends TestCase
'/assets/baderech-icon.png', '/assets/baderech-icon.png',
'<p>A structured path for growth</p>', '<p>A structured path for growth</p>',
'/assets/pdfs/baderech.pdf', '/assets/pdfs/baderech.pdf',
'/assets/pdfs/baderech-long.pdf',
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
null, null,
); );
@ -80,6 +88,7 @@ class ElementControllerTest extends TestCase
'<p>Foundations rich text</p>', '<p>Foundations rich text</p>',
'/assets/pdfs/foundations.pdf', '/assets/pdfs/foundations.pdf',
null, null,
null,
$element, $element,
); );
$secondChildElement = $this->createElement( $secondChildElement = $this->createElement(
@ -90,6 +99,7 @@ class ElementControllerTest extends TestCase
'<p>Daily practice rich text</p>', '<p>Daily practice rich text</p>',
null, null,
null, null,
null,
$element, $element,
); );
@ -113,8 +123,13 @@ class ElementControllerTest extends TestCase
); );
$this->assertSame( $this->assertSame(
'/assets/pdfs/baderech.pdf', '/assets/pdfs/baderech.pdf',
$body['element']['pdfPath'], $body['element']['shortPdfPath'],
); );
$this->assertSame(
'/assets/pdfs/baderech-long.pdf',
$body['element']['longPdfPath'],
);
$this->assertArrayNotHasKey('pdfPath', $body['element']);
$this->assertSame( $this->assertSame(
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
$body['element']['youtubeUrl'], $body['element']['youtubeUrl'],
@ -158,21 +173,13 @@ class ElementControllerTest extends TestCase
public function testUpdateWithIconImageReturnsElementPayload(): void public function testUpdateWithIconImageReturnsElementPayload(): void
{ {
$set = $this->createSet(1, 'Baderech'); $set = $this->createSet(1, 'Baderech');
$element = $this->createElement( $element = $this->createEmptyElement($set);
$set,
'Baderech HaAvodah',
'A structured path for growth',
null,
'<p>A structured path for growth</p>',
null,
null,
null,
);
$fixturePath = __DIR__ . '/../../fixtures/icon.png'; $fixturePath = __DIR__ . '/../../fixtures/icon.png';
$request = new Request([], [ $request = new Request([], [
'elementId' => (string) $element->getId(), 'elementId' => (string) $element->getId(),
'fileType' => 'iconImage',
], [], [], [ ], [], [], [
'iconImage' => new UploadedFile( 'file' => new UploadedFile(
$fixturePath, $fixturePath,
'icon.png', 'icon.png',
'image/png', 'image/png',
@ -196,24 +203,16 @@ class ElementControllerTest extends TestCase
); );
} }
public function testUpdateWithPdfReturnsElementPayload(): void public function testUpdateWithShortPdfReturnsElementPayload(): void
{ {
$set = $this->createSet(1, 'Baderech'); $set = $this->createSet(1, 'Baderech');
$element = $this->createElement( $element = $this->createEmptyElement($set);
$set,
'Baderech HaAvodah',
'A structured path for growth',
null,
'<p>A structured path for growth</p>',
null,
null,
null,
);
$fixturePath = __DIR__ . '/../../fixtures/baderech.pdf'; $fixturePath = __DIR__ . '/../../fixtures/baderech.pdf';
$request = new Request([], [ $request = new Request([], [
'elementId' => (string) $element->getId(), 'elementId' => (string) $element->getId(),
'fileType' => 'shortPdf',
], [], [], [ ], [], [], [
'pdf' => new UploadedFile( 'file' => new UploadedFile(
$fixturePath, $fixturePath,
'baderech.pdf', 'baderech.pdf',
'application/pdf', 'application/pdf',
@ -228,11 +227,45 @@ class ElementControllerTest extends TestCase
$body = json_decode($response->getContent(), true); $body = json_decode($response->getContent(), true);
$this->assertSame($element->getId(), $body['element']['id']); $this->assertSame($element->getId(), $body['element']['id']);
$this->assertSame( $this->assertSame(
'https://test.local/storage/element-pdfs/fake-baderech.pdf', 'https://test.local/storage/element-pdfs/short/fake-baderech.pdf',
$body['element']['pdfPath'], $body['element']['shortPdfPath'],
);
$this->assertArrayNotHasKey('pdfPath', $body['element']);
$this->assertSame(
'element-pdfs/short',
$this->fileUploader->uploads[0]['folder'],
);
}
public function testUpdateWithLongPdfReturnsElementPayload(): void
{
$set = $this->createSet(1, 'Baderech');
$element = $this->createEmptyElement($set);
$fixturePath = __DIR__ . '/../../fixtures/baderech.pdf';
$request = new Request([], [
'elementId' => (string) $element->getId(),
'fileType' => 'longPdf',
], [], [], [
'file' => new UploadedFile(
$fixturePath,
'baderech.pdf',
'application/pdf',
null,
true,
),
], ['REQUEST_METHOD' => 'POST']);
$response = $this->controller->update($request);
$this->assertEquals(200, $response->getStatusCode());
$body = json_decode($response->getContent(), true);
$this->assertSame($element->getId(), $body['element']['id']);
$this->assertSame(
'https://test.local/storage/element-pdfs/long/fake-baderech.pdf',
$body['element']['longPdfPath'],
); );
$this->assertSame( $this->assertSame(
'element-pdfs', 'element-pdfs/long',
$this->fileUploader->uploads[0]['folder'], $this->fileUploader->uploads[0]['folder'],
); );
} }
@ -247,13 +280,29 @@ class ElementControllerTest extends TestCase
); );
} }
private function createEmptyElement(DomainSet $set): Element
{
return $this->createElement(
$set,
'Baderech HaAvodah',
'A structured path for growth',
null,
'<p>A structured path for growth</p>',
null,
null,
null,
null,
);
}
private function createElement( private function createElement(
DomainSet $set, DomainSet $set,
string $title, string $title,
string $description, string $description,
?string $iconImageUrl, ?string $iconImageUrl,
string $richText, string $richText,
?string $pdfPath, ?string $shortPdfPath,
?string $longPdfPath,
?string $youtubeUrl, ?string $youtubeUrl,
?Element $parentElement, ?Element $parentElement,
): Element { ): Element {
@ -263,7 +312,8 @@ class ElementControllerTest extends TestCase
description: $description, description: $description,
iconImageUrl: $iconImageUrl, iconImageUrl: $iconImageUrl,
richText: $richText, richText: $richText,
pdfPath: $pdfPath, shortPdfPath: $shortPdfPath,
longPdfPath: $longPdfPath,
youtubeUrl: $youtubeUrl, youtubeUrl: $youtubeUrl,
parentElement: $parentElement, parentElement: $parentElement,
)); ));

View file

@ -22,7 +22,8 @@ class ElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
set: $set, set: $set,
parentElement: null, parentElement: null,
@ -33,7 +34,8 @@ class ElementTest extends TestCase
description: 'Child description', description: 'Child description',
iconImageUrl: '/assets/child-icon.svg', iconImageUrl: '/assets/child-icon.svg',
richText: '<p>Child rich text</p>', richText: '<p>Child rich text</p>',
pdfPath: '/assets/pdfs/child.pdf', shortPdfPath: '/assets/pdfs/child.pdf',
longPdfPath: '/assets/pdfs/child-long.pdf',
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
set: $set, set: $set,
parentElement: $rootElement, parentElement: $rootElement,
@ -55,7 +57,11 @@ class ElementTest extends TestCase
); );
$this->assertSame( $this->assertSame(
'/assets/pdfs/child.pdf', '/assets/pdfs/child.pdf',
$childElement->getPdfPath(), $childElement->getShortPdfPath(),
);
$this->assertSame(
'/assets/pdfs/child-long.pdf',
$childElement->getLongPdfPath(),
); );
$this->assertSame( $this->assertSame(
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
@ -64,7 +70,8 @@ class ElementTest extends TestCase
$this->assertSame($set, $childElement->getSet()); $this->assertSame($set, $childElement->getSet());
$this->assertSame($rootElement, $childElement->getParentElement()); $this->assertSame($rootElement, $childElement->getParentElement());
$this->assertNull($rootElement->getIconImageUrl()); $this->assertNull($rootElement->getIconImageUrl());
$this->assertNull($rootElement->getPdfPath()); $this->assertNull($rootElement->getShortPdfPath());
$this->assertNull($rootElement->getLongPdfPath());
$this->assertNull($rootElement->getYoutubeUrl()); $this->assertNull($rootElement->getYoutubeUrl());
$this->assertNull($rootElement->getParentElement()); $this->assertNull($rootElement->getParentElement());
} }
@ -83,7 +90,8 @@ class ElementTest extends TestCase
description: 'Original description', description: 'Original description',
iconImageUrl: '/assets/original.svg', iconImageUrl: '/assets/original.svg',
richText: '<p>Original</p>', richText: '<p>Original</p>',
pdfPath: '/assets/pdfs/original.pdf', shortPdfPath: '/assets/pdfs/original.pdf',
longPdfPath: '/assets/pdfs/original-long.pdf',
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU',
set: $set, set: $set,
parentElement: null, parentElement: null,
@ -93,14 +101,16 @@ class ElementTest extends TestCase
$element->setDescription('Updated description'); $element->setDescription('Updated description');
$element->setIconImageUrl(null); $element->setIconImageUrl(null);
$element->setRichText('<p>Updated</p>'); $element->setRichText('<p>Updated</p>');
$element->setPdfPath(null); $element->setShortPdfPath(null);
$element->setLongPdfPath(null);
$element->setYoutubeUrl(null); $element->setYoutubeUrl(null);
$this->assertSame('Updated', $element->getTitle()); $this->assertSame('Updated', $element->getTitle());
$this->assertSame('Updated description', $element->getDescription()); $this->assertSame('Updated description', $element->getDescription());
$this->assertNull($element->getIconImageUrl()); $this->assertNull($element->getIconImageUrl());
$this->assertSame('<p>Updated</p>', $element->getRichText()); $this->assertSame('<p>Updated</p>', $element->getRichText());
$this->assertNull($element->getPdfPath()); $this->assertNull($element->getShortPdfPath());
$this->assertNull($element->getLongPdfPath());
$this->assertNull($element->getYoutubeUrl()); $this->assertNull($element->getYoutubeUrl());
} }
} }

View file

@ -50,7 +50,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: '/assets/root-icon.svg', iconImageUrl: '/assets/root-icon.svg',
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: '/assets/pdfs/root.pdf', shortPdfPath: '/assets/pdfs/root.pdf',
longPdfPath: '/assets/pdfs/root-long.pdf',
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
parentElementId: null, parentElementId: null,
)); ));
@ -63,7 +64,14 @@ class CreateElementTest extends TestCase
$element->getIconImageUrl(), $element->getIconImageUrl(),
); );
$this->assertSame('<p>Root rich text</p>', $element->getRichText()); $this->assertSame('<p>Root rich text</p>', $element->getRichText());
$this->assertSame('/assets/pdfs/root.pdf', $element->getPdfPath()); $this->assertSame(
'/assets/pdfs/root.pdf',
$element->getShortPdfPath(),
);
$this->assertSame(
'/assets/pdfs/root-long.pdf',
$element->getLongPdfPath(),
);
$this->assertSame( $this->assertSame(
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
$element->getYoutubeUrl(), $element->getYoutubeUrl(),
@ -82,7 +90,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
) )
@ -95,7 +104,8 @@ class CreateElementTest extends TestCase
description: 'Child description', description: 'Child description',
iconImageUrl: '/assets/child-icon.svg', iconImageUrl: '/assets/child-icon.svg',
richText: '<p>Child rich text</p>', richText: '<p>Child rich text</p>',
pdfPath: '/assets/pdfs/child.pdf', shortPdfPath: '/assets/pdfs/child.pdf',
longPdfPath: '/assets/pdfs/child-long.pdf',
youtubeUrl: 'https://youtu.be/yHx-r4p6hHU', youtubeUrl: 'https://youtu.be/yHx-r4p6hHU',
parentElementId: $rootElement->getId(), parentElementId: $rootElement->getId(),
) )
@ -116,7 +126,11 @@ class CreateElementTest extends TestCase
); );
$this->assertSame( $this->assertSame(
'/assets/pdfs/child.pdf', '/assets/pdfs/child.pdf',
$childElement->getPdfPath(), $childElement->getShortPdfPath(),
);
$this->assertSame(
'/assets/pdfs/child-long.pdf',
$childElement->getLongPdfPath(),
); );
$this->assertSame( $this->assertSame(
'https://youtu.be/yHx-r4p6hHU', 'https://youtu.be/yHx-r4p6hHU',
@ -138,7 +152,8 @@ class CreateElementTest extends TestCase
description: null, description: null,
iconImageUrl: null, iconImageUrl: null,
richText: null, richText: null,
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
@ -146,7 +161,8 @@ class CreateElementTest extends TestCase
$this->assertSame('', $element->getDescription()); $this->assertSame('', $element->getDescription());
$this->assertNull($element->getIconImageUrl()); $this->assertNull($element->getIconImageUrl());
$this->assertSame('', $element->getRichText()); $this->assertSame('', $element->getRichText());
$this->assertNull($element->getPdfPath()); $this->assertNull($element->getShortPdfPath());
$this->assertNull($element->getLongPdfPath());
$this->assertNull($element->getYoutubeUrl()); $this->assertNull($element->getYoutubeUrl());
} }
@ -160,7 +176,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: '', iconImageUrl: '',
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
@ -168,7 +185,7 @@ class CreateElementTest extends TestCase
$this->assertNull($element->getIconImageUrl()); $this->assertNull($element->getIconImageUrl());
} }
public function testCreatesElementWithNullPdfPathWhenBlank(): void public function testCreatesElementWithNullPdfPathsWhenBlank(): void
{ {
$set = $this->createSet('Daily learning'); $set = $this->createSet('Daily learning');
@ -178,12 +195,14 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: '', shortPdfPath: '',
longPdfPath: '',
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
$this->assertNull($element->getPdfPath()); $this->assertNull($element->getShortPdfPath());
$this->assertNull($element->getLongPdfPath());
} }
public function testCreatesElementWithNullYoutubeUrlWhenBlank(): void public function testCreatesElementWithNullYoutubeUrlWhenBlank(): void
@ -196,7 +215,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: '', youtubeUrl: '',
parentElementId: null, parentElementId: null,
)); ));
@ -215,7 +235,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
@ -232,7 +253,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
@ -249,7 +271,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
@ -270,7 +293,8 @@ class CreateElementTest extends TestCase
description: 'Child description', description: 'Child description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Child rich text</p>', richText: '<p>Child rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: 99, parentElementId: 99,
)); ));
@ -285,7 +309,8 @@ class CreateElementTest extends TestCase
description: 'Root description', description: 'Root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Root rich text</p>', richText: '<p>Root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
@ -301,7 +326,8 @@ class CreateElementTest extends TestCase
description: 'Another root description', description: 'Another root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Another root rich text</p>', richText: '<p>Another root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
)); ));
@ -318,7 +344,8 @@ class CreateElementTest extends TestCase
description: 'Parent root description', description: 'Parent root description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Parent root rich text</p>', richText: '<p>Parent root rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: null, parentElementId: null,
) )
@ -335,7 +362,8 @@ class CreateElementTest extends TestCase
description: 'Invalid child description', description: 'Invalid child description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Invalid child rich text</p>', richText: '<p>Invalid child rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElementId: $parentElement->getId(), parentElementId: $parentElement->getId(),
)); ));

View file

@ -60,7 +60,7 @@ class GetElementTest extends TestCase
); );
$this->assertSame( $this->assertSame(
'/assets/pdfs/baderech.pdf', '/assets/pdfs/baderech.pdf',
$foundElement->getPdfPath(), $foundElement->getShortPdfPath(),
); );
$this->assertSame( $this->assertSame(
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
@ -191,7 +191,7 @@ class GetElementTest extends TestCase
string $description, string $description,
?string $iconImageUrl, ?string $iconImageUrl,
string $richText, string $richText,
?string $pdfPath, ?string $shortPdfPath,
?string $youtubeUrl, ?string $youtubeUrl,
?Element $parentElement, ?Element $parentElement,
): Element { ): Element {
@ -201,7 +201,8 @@ class GetElementTest extends TestCase
description: $description, description: $description,
iconImageUrl: $iconImageUrl, iconImageUrl: $iconImageUrl,
richText: $richText, richText: $richText,
pdfPath: $pdfPath, shortPdfPath: $shortPdfPath,
longPdfPath: null,
youtubeUrl: $youtubeUrl, youtubeUrl: $youtubeUrl,
parentElement: $parentElement, parentElement: $parentElement,
)); ));

View file

@ -8,8 +8,10 @@ use App\Element\UseCases\UpdateElement\UpdateDescription;
use App\Element\UseCases\UpdateElement\UpdateDescriptionRequest; use App\Element\UseCases\UpdateElement\UpdateDescriptionRequest;
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl; use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
use App\Element\UseCases\UpdateElement\UpdateIconImageUrlRequest; use App\Element\UseCases\UpdateElement\UpdateIconImageUrlRequest;
use App\Element\UseCases\UpdateElement\UpdatePdfPath; use App\Element\UseCases\UpdateElement\UpdateLongPdfPath;
use App\Element\UseCases\UpdateElement\UpdatePdfPathRequest; use App\Element\UseCases\UpdateElement\UpdateLongPdfPathRequest;
use App\Element\UseCases\UpdateElement\UpdateShortPdfPath;
use App\Element\UseCases\UpdateElement\UpdateShortPdfPathRequest;
use App\Element\UseCases\UpdateElement\UpdateRichText; use App\Element\UseCases\UpdateElement\UpdateRichText;
use App\Element\UseCases\UpdateElement\UpdateRichTextRequest; use App\Element\UseCases\UpdateElement\UpdateRichTextRequest;
use App\Element\UseCases\UpdateElement\UpdateTitle; use App\Element\UseCases\UpdateElement\UpdateTitle;
@ -134,19 +136,26 @@ class UpdateElementFieldsTest extends TestCase
); );
} }
public function testUpdatePdfPathClearsEmptyString(): void public function testUpdateShortPdfPathClearsEmptyString(): void
{ {
$element = $this->createFilledElement(); $element = $this->createFilledElement();
$updatePdfPath = new UpdatePdfPath( $updateShortPdfPath = new UpdateShortPdfPath(
$this->elementRepo, $this->elementRepo,
$this->fileUploader, $this->fileUploader,
); );
$updatedElement = $updatePdfPath->execute( $updatedElement = $updateShortPdfPath->execute(
new UpdatePdfPathRequest(id: $element->getId(), pdfPath: '') new UpdateShortPdfPathRequest(
id: $element->getId(),
shortPdfPath: '',
)
); );
$this->assertNull($updatedElement->getPdfPath()); $this->assertNull($updatedElement->getShortPdfPath());
$this->assertSame(
'/assets/pdfs/original-long.pdf',
$updatedElement->getLongPdfPath(),
);
$this->assertSame($element->getTitle(), $updatedElement->getTitle()); $this->assertSame($element->getTitle(), $updatedElement->getTitle());
$this->assertSame( $this->assertSame(
['/assets/pdfs/original.pdf'], ['/assets/pdfs/original.pdf'],
@ -154,6 +163,33 @@ class UpdateElementFieldsTest extends TestCase
); );
} }
public function testUpdateLongPdfPathClearsEmptyString(): void
{
$element = $this->createFilledElement();
$updateLongPdfPath = new UpdateLongPdfPath(
$this->elementRepo,
$this->fileUploader,
);
$updatedElement = $updateLongPdfPath->execute(
new UpdateLongPdfPathRequest(
id: $element->getId(),
longPdfPath: '',
)
);
$this->assertSame(
'/assets/pdfs/original.pdf',
$updatedElement->getShortPdfPath(),
);
$this->assertNull($updatedElement->getLongPdfPath());
$this->assertSame($element->getTitle(), $updatedElement->getTitle());
$this->assertSame(
['/assets/pdfs/original-long.pdf'],
$this->fileUploader->deletedPaths,
);
}
public function testUpdateYoutubeUrlClearsEmptyString(): void public function testUpdateYoutubeUrlClearsEmptyString(): void
{ {
$element = $this->createFilledElement(); $element = $this->createFilledElement();
@ -175,12 +211,14 @@ class UpdateElementFieldsTest extends TestCase
return $this->createElementWithMedia( return $this->createElementWithMedia(
'/assets/original-icon.png', '/assets/original-icon.png',
'/assets/pdfs/original.pdf', '/assets/pdfs/original.pdf',
'/assets/pdfs/original-long.pdf',
); );
} }
private function createElementWithMedia( private function createElementWithMedia(
?string $iconImageUrl, ?string $iconImageUrl,
?string $pdfPath, ?string $shortPdfPath,
?string $longPdfPath,
): Element { ): Element {
$set = new DomainSet( $set = new DomainSet(
id: 1, id: 1,
@ -195,7 +233,8 @@ class UpdateElementFieldsTest extends TestCase
description: 'Original description', description: 'Original description',
iconImageUrl: $iconImageUrl, iconImageUrl: $iconImageUrl,
richText: '<p>Original rich text</p>', richText: '<p>Original rich text</p>',
pdfPath: $pdfPath, shortPdfPath: $shortPdfPath,
longPdfPath: $longPdfPath,
youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU', youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU',
parentElement: null, parentElement: null,
)); ));

View file

@ -9,9 +9,11 @@ use App\Element\UseCases\UpdateElement\UpdateElement;
use App\Element\UseCases\UpdateElement\UpdateElementRequest; use App\Element\UseCases\UpdateElement\UpdateElementRequest;
use App\Element\UseCases\UpdateElement\UpdateIconImage; use App\Element\UseCases\UpdateElement\UpdateIconImage;
use App\Element\UseCases\UpdateElement\UpdateIconImageUrl; use App\Element\UseCases\UpdateElement\UpdateIconImageUrl;
use App\Element\UseCases\UpdateElement\UpdatePdfPath; use App\Element\UseCases\UpdateElement\UpdateLongPdf;
use App\Element\UseCases\UpdateElement\UpdatePdf; use App\Element\UseCases\UpdateElement\UpdateLongPdfPath;
use App\Element\UseCases\UpdateElement\UpdateRichText; use App\Element\UseCases\UpdateElement\UpdateRichText;
use App\Element\UseCases\UpdateElement\UpdateShortPdf;
use App\Element\UseCases\UpdateElement\UpdateShortPdfPath;
use App\Element\UseCases\UpdateElement\UpdateTitle; use App\Element\UseCases\UpdateElement\UpdateTitle;
use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl; use App\Element\UseCases\UpdateElement\UpdateYoutubeUrl;
use App\Exceptions\BadRequestException; use App\Exceptions\BadRequestException;
@ -38,13 +40,18 @@ class UpdateElementTest extends TestCase
new UpdateDescription($this->elementRepo), new UpdateDescription($this->elementRepo),
new UpdateIconImageUrl($this->elementRepo, $this->fileUploader), new UpdateIconImageUrl($this->elementRepo, $this->fileUploader),
new UpdateRichText($this->elementRepo), new UpdateRichText($this->elementRepo),
new UpdatePdfPath($this->elementRepo, $this->fileUploader), new UpdateShortPdfPath($this->elementRepo, $this->fileUploader),
new UpdateLongPdfPath($this->elementRepo, $this->fileUploader),
new UpdateYoutubeUrl($this->elementRepo), new UpdateYoutubeUrl($this->elementRepo),
new UpdateIconImage( new UpdateIconImage(
$this->elementRepo, $this->elementRepo,
$this->fileUploader, $this->fileUploader,
), ),
new UpdatePdf( new UpdateShortPdf(
$this->elementRepo,
$this->fileUploader,
),
new UpdateLongPdf(
$this->elementRepo, $this->elementRepo,
$this->fileUploader, $this->fileUploader,
), ),
@ -62,6 +69,7 @@ class UpdateElementTest extends TestCase
'/assets/original-icon.png', '/assets/original-icon.png',
'<p>Original rich text</p>', '<p>Original rich text</p>',
'/assets/pdfs/original.pdf', '/assets/pdfs/original.pdf',
'/assets/pdfs/original-long.pdf',
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
null, null,
); );
@ -73,16 +81,14 @@ class UpdateElementTest extends TestCase
description: 'Updated description', description: 'Updated description',
iconImageUrl: '/assets/updated-icon.png', iconImageUrl: '/assets/updated-icon.png',
richText: '<p>Updated rich text</p>', richText: '<p>Updated rich text</p>',
pdfPath: '/assets/pdfs/updated.pdf', shortPdfPath: '/assets/pdfs/updated.pdf',
longPdfPath: '/assets/pdfs/updated-long.pdf',
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
iconImageContents: null, fileType: null,
iconImageOriginalName: null, fileContents: null,
iconImageMimeType: null, fileOriginalName: null,
iconImageSizeBytes: null, fileMimeType: null,
pdfContents: null, fileSizeBytes: null,
pdfOriginalName: null,
pdfMimeType: null,
pdfSizeBytes: null,
) )
); );
@ -102,7 +108,11 @@ class UpdateElementTest extends TestCase
); );
$this->assertSame( $this->assertSame(
'/assets/pdfs/updated.pdf', '/assets/pdfs/updated.pdf',
$updatedElement->getPdfPath(), $updatedElement->getShortPdfPath(),
);
$this->assertSame(
'/assets/pdfs/updated-long.pdf',
$updatedElement->getLongPdfPath(),
); );
$this->assertSame( $this->assertSame(
'https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
@ -124,6 +134,7 @@ class UpdateElementTest extends TestCase
'/assets/original-icon.png', '/assets/original-icon.png',
'<p>Original rich text</p>', '<p>Original rich text</p>',
'/assets/pdfs/original.pdf', '/assets/pdfs/original.pdf',
'/assets/pdfs/original-long.pdf',
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
null, null,
); );
@ -135,21 +146,20 @@ class UpdateElementTest extends TestCase
description: 'Updated description', description: 'Updated description',
iconImageUrl: '', iconImageUrl: '',
richText: '<p>Updated rich text</p>', richText: '<p>Updated rich text</p>',
pdfPath: '', shortPdfPath: '',
longPdfPath: '',
youtubeUrl: '', youtubeUrl: '',
iconImageContents: null, fileType: null,
iconImageOriginalName: null, fileContents: null,
iconImageMimeType: null, fileOriginalName: null,
iconImageSizeBytes: null, fileMimeType: null,
pdfContents: null, fileSizeBytes: null,
pdfOriginalName: null,
pdfMimeType: null,
pdfSizeBytes: null,
) )
); );
$this->assertNull($updatedElement->getIconImageUrl()); $this->assertNull($updatedElement->getIconImageUrl());
$this->assertNull($updatedElement->getPdfPath()); $this->assertNull($updatedElement->getShortPdfPath());
$this->assertNull($updatedElement->getLongPdfPath());
$this->assertNull($updatedElement->getYoutubeUrl()); $this->assertNull($updatedElement->getYoutubeUrl());
} }
@ -163,6 +173,7 @@ class UpdateElementTest extends TestCase
'/assets/original-icon.png', '/assets/original-icon.png',
'<p>Original rich text</p>', '<p>Original rich text</p>',
'/assets/pdfs/original.pdf', '/assets/pdfs/original.pdf',
'/assets/pdfs/original-long.pdf',
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
null, null,
); );
@ -174,16 +185,14 @@ class UpdateElementTest extends TestCase
description: null, description: null,
iconImageUrl: null, iconImageUrl: null,
richText: null, richText: null,
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
iconImageContents: null, fileType: null,
iconImageOriginalName: null, fileContents: null,
iconImageMimeType: null, fileOriginalName: null,
iconImageSizeBytes: null, fileMimeType: null,
pdfContents: null, fileSizeBytes: null,
pdfOriginalName: null,
pdfMimeType: null,
pdfSizeBytes: null,
) )
); );
@ -202,7 +211,11 @@ class UpdateElementTest extends TestCase
); );
$this->assertSame( $this->assertSame(
'/assets/pdfs/original.pdf', '/assets/pdfs/original.pdf',
$updatedElement->getPdfPath(), $updatedElement->getShortPdfPath(),
);
$this->assertSame(
'/assets/pdfs/original-long.pdf',
$updatedElement->getLongPdfPath(),
); );
$this->assertSame( $this->assertSame(
'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s',
@ -210,7 +223,7 @@ class UpdateElementTest extends TestCase
); );
} }
public function testUpdatesUploadedFiles(): void public function testRoutesUploadedFilesByFileType(): void
{ {
$set = $this->createSet(1, 'Baderech'); $set = $this->createSet(1, 'Baderech');
$element = $this->createElement( $element = $this->createElement(
@ -222,8 +235,39 @@ class UpdateElementTest extends TestCase
null, null,
null, null,
null, null,
null,
); );
$this->updateElement->execute(new UpdateElementRequest(
id: $element->getId(),
title: null,
description: null,
iconImageUrl: null,
richText: null,
shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null,
fileType: 'iconImage',
fileContents: 'binary-image-bytes',
fileOriginalName: 'icon.png',
fileMimeType: 'image/png',
fileSizeBytes: 1024,
));
$this->updateElement->execute(new UpdateElementRequest(
id: $element->getId(),
title: null,
description: null,
iconImageUrl: null,
richText: null,
shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null,
fileType: 'shortPdf',
fileContents: 'binary-short-pdf-bytes',
fileOriginalName: 'baderech.pdf',
fileMimeType: 'application/pdf',
fileSizeBytes: 1024,
));
$updatedElement = $this->updateElement->execute( $updatedElement = $this->updateElement->execute(
new UpdateElementRequest( new UpdateElementRequest(
id: $element->getId(), id: $element->getId(),
@ -231,16 +275,14 @@ class UpdateElementTest extends TestCase
description: null, description: null,
iconImageUrl: null, iconImageUrl: null,
richText: null, richText: null,
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
iconImageContents: 'binary-image-bytes', fileType: 'longPdf',
iconImageOriginalName: 'icon.png', fileContents: 'binary-long-pdf-bytes',
iconImageMimeType: 'image/png', fileOriginalName: 'baderech-long.pdf',
iconImageSizeBytes: 1024, fileMimeType: 'application/pdf',
pdfContents: 'binary-pdf-bytes', fileSizeBytes: 1024,
pdfOriginalName: 'baderech.pdf',
pdfMimeType: 'application/pdf',
pdfSizeBytes: 1024,
) )
); );
@ -249,17 +291,69 @@ class UpdateElementTest extends TestCase
$updatedElement->getIconImageUrl(), $updatedElement->getIconImageUrl(),
); );
$this->assertSame( $this->assertSame(
'element-pdfs/fake-baderech.pdf', 'element-pdfs/short/fake-baderech.pdf',
$updatedElement->getPdfPath(), $updatedElement->getShortPdfPath(),
);
$this->assertSame(
'element-pdfs/long/fake-baderech-long.pdf',
$updatedElement->getLongPdfPath(),
); );
$this->assertSame( $this->assertSame(
'element-icons', 'element-icons',
$this->fileUploader->uploads[0]['folder'], $this->fileUploader->uploads[0]['folder'],
); );
$this->assertSame( $this->assertSame(
'element-pdfs', 'element-pdfs/short',
$this->fileUploader->uploads[1]['folder'], $this->fileUploader->uploads[1]['folder'],
); );
$this->assertSame(
'element-pdfs/long',
$this->fileUploader->uploads[2]['folder'],
);
}
public function testThrowsWhenFileTypeMissingForUpload(): void
{
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('fileType is required');
$this->updateElement->execute(new UpdateElementRequest(
id: 1,
title: null,
description: null,
iconImageUrl: null,
richText: null,
shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null,
fileType: null,
fileContents: 'binary-image-bytes',
fileOriginalName: 'icon.png',
fileMimeType: 'image/png',
fileSizeBytes: 1024,
));
}
public function testThrowsWhenFileTypeInvalid(): void
{
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('fileType is invalid');
$this->updateElement->execute(new UpdateElementRequest(
id: 1,
title: null,
description: null,
iconImageUrl: null,
richText: null,
shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null,
fileType: 'audio',
fileContents: 'binary-audio-bytes',
fileOriginalName: 'shiur.mp3',
fileMimeType: 'audio/mpeg',
fileSizeBytes: 1024,
));
} }
public function testThrowsWhenIdMissing(): void public function testThrowsWhenIdMissing(): void
@ -273,16 +367,14 @@ class UpdateElementTest extends TestCase
description: 'Updated description', description: 'Updated description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Updated rich text</p>', richText: '<p>Updated rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
iconImageContents: null, fileType: null,
iconImageOriginalName: null, fileContents: null,
iconImageMimeType: null, fileOriginalName: null,
iconImageSizeBytes: null, fileMimeType: null,
pdfContents: null, fileSizeBytes: null,
pdfOriginalName: null,
pdfMimeType: null,
pdfSizeBytes: null,
)); ));
} }
@ -297,16 +389,14 @@ class UpdateElementTest extends TestCase
description: null, description: null,
iconImageUrl: null, iconImageUrl: null,
richText: null, richText: null,
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
iconImageContents: null, fileType: null,
iconImageOriginalName: null, fileContents: null,
iconImageMimeType: null, fileOriginalName: null,
iconImageSizeBytes: null, fileMimeType: null,
pdfContents: null, fileSizeBytes: null,
pdfOriginalName: null,
pdfMimeType: null,
pdfSizeBytes: null,
)); ));
} }
@ -321,16 +411,14 @@ class UpdateElementTest extends TestCase
description: 'Updated description', description: 'Updated description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Updated rich text</p>', richText: '<p>Updated rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
iconImageContents: null, fileType: null,
iconImageOriginalName: null, fileContents: null,
iconImageMimeType: null, fileOriginalName: null,
iconImageSizeBytes: null, fileMimeType: null,
pdfContents: null, fileSizeBytes: null,
pdfOriginalName: null,
pdfMimeType: null,
pdfSizeBytes: null,
)); ));
} }
@ -345,16 +433,14 @@ class UpdateElementTest extends TestCase
description: 'Updated description', description: 'Updated description',
iconImageUrl: null, iconImageUrl: null,
richText: '<p>Updated rich text</p>', richText: '<p>Updated rich text</p>',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
iconImageContents: null, fileType: null,
iconImageOriginalName: null, fileContents: null,
iconImageMimeType: null, fileOriginalName: null,
iconImageSizeBytes: null, fileMimeType: null,
pdfContents: null, fileSizeBytes: null,
pdfOriginalName: null,
pdfMimeType: null,
pdfSizeBytes: null,
)); ));
} }
@ -374,7 +460,8 @@ class UpdateElementTest extends TestCase
string $description, string $description,
?string $iconImageUrl, ?string $iconImageUrl,
string $richText, string $richText,
?string $pdfPath, ?string $shortPdfPath,
?string $longPdfPath,
?string $youtubeUrl, ?string $youtubeUrl,
?Element $parentElement, ?Element $parentElement,
): Element { ): Element {
@ -384,7 +471,8 @@ class UpdateElementTest extends TestCase
description: $description, description: $description,
iconImageUrl: $iconImageUrl, iconImageUrl: $iconImageUrl,
richText: $richText, richText: $richText,
pdfPath: $pdfPath, shortPdfPath: $shortPdfPath,
longPdfPath: $longPdfPath,
youtubeUrl: $youtubeUrl, youtubeUrl: $youtubeUrl,
parentElement: $parentElement, parentElement: $parentElement,
)); ));

View file

@ -33,19 +33,19 @@ class UpdateIconImageTest extends TestCase
/** /**
* @return array{ * @return array{
* iconImageContents: string, * fileContents: string,
* iconImageOriginalName: string, * fileOriginalName: string,
* iconImageMimeType: string, * fileMimeType: string,
* iconImageSizeBytes: int * fileSizeBytes: int
* } * }
*/ */
private function imageArgs(): array private function imageArgs(): array
{ {
return [ return [
'iconImageContents' => 'binary-image-bytes', 'fileContents' => 'binary-image-bytes',
'iconImageOriginalName' => 'icon.png', 'fileOriginalName' => 'icon.png',
'iconImageMimeType' => 'image/png', 'fileMimeType' => 'image/png',
'iconImageSizeBytes' => 1024, 'fileSizeBytes' => 1024,
]; ];
} }
@ -97,10 +97,10 @@ class UpdateIconImageTest extends TestCase
$this->useCase->execute( $this->useCase->execute(
new UpdateIconImageRequest( new UpdateIconImageRequest(
id: 1, id: 1,
iconImageContents: null, fileContents: null,
iconImageOriginalName: null, fileOriginalName: null,
iconImageMimeType: null, fileMimeType: null,
iconImageSizeBytes: null, fileSizeBytes: null,
) )
); );
} }
@ -115,10 +115,10 @@ class UpdateIconImageTest extends TestCase
$this->useCase->execute( $this->useCase->execute(
new UpdateIconImageRequest( new UpdateIconImageRequest(
id: 1, id: 1,
iconImageContents: 'not-an-image', fileContents: 'not-an-image',
iconImageOriginalName: 'icon.pdf', fileOriginalName: 'icon.pdf',
iconImageMimeType: 'application/pdf', fileMimeType: 'application/pdf',
iconImageSizeBytes: 1024, fileSizeBytes: 1024,
) )
); );
} }
@ -131,10 +131,10 @@ class UpdateIconImageTest extends TestCase
$this->useCase->execute( $this->useCase->execute(
new UpdateIconImageRequest( new UpdateIconImageRequest(
id: 1, id: 1,
iconImageContents: 'big', fileContents: 'big',
iconImageOriginalName: 'icon.png', fileOriginalName: 'icon.png',
iconImageMimeType: 'image/png', fileMimeType: 'image/png',
iconImageSizeBytes: 6 * 1024 * 1024, fileSizeBytes: 6 * 1024 * 1024,
) )
); );
} }
@ -167,7 +167,8 @@ class UpdateIconImageTest extends TestCase
description: 'Original description', description: 'Original description',
iconImageUrl: null, iconImageUrl: null,
richText: '', richText: '',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElement: null, parentElement: null,
)); ));

View file

@ -0,0 +1,95 @@
<?php
namespace Tests\Unit\Element\UseCases;
use App\Element\CreateElementDto;
use App\Element\Element;
use App\Element\UseCases\UpdateElement\UpdateLongPdf;
use App\Element\UseCases\UpdateElement\UpdateLongPdfRequest;
use App\Exceptions\BadRequestException;
use App\Set\Set as DomainSet;
use Tests\Fakes\FakeElementRepository;
use Tests\Fakes\FakeFileUploader;
use Tests\TestCase;
class UpdateLongPdfTest extends TestCase
{
private FakeElementRepository $elementRepository;
private FakeFileUploader $fileUploader;
private UpdateLongPdf $useCase;
protected function setUp(): void
{
$this->elementRepository = new FakeElementRepository();
$this->fileUploader = new FakeFileUploader();
$this->useCase = new UpdateLongPdf(
$this->elementRepository,
$this->fileUploader,
);
}
public function testUploadsLongPdfAndStoresPath(): void
{
$element = $this->createElement();
$updatedElement = $this->useCase->execute(
new UpdateLongPdfRequest(
id: $element->getId(),
fileContents: 'binary-pdf-bytes',
fileOriginalName: 'baderech-long.pdf',
fileMimeType: 'application/pdf',
fileSizeBytes: 1024,
)
);
$this->assertNull($updatedElement->getShortPdfPath());
$this->assertSame(
'element-pdfs/long/fake-baderech-long.pdf',
$updatedElement->getLongPdfPath(),
);
$this->assertSame(
'element-pdfs/long',
$this->fileUploader->uploads[0]['folder'],
);
}
public function testDisallowedMimeThrowsBadRequest(): void
{
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('pdf must be a pdf file');
$this->useCase->execute(
new UpdateLongPdfRequest(
id: 1,
fileContents: 'not-a-pdf',
fileOriginalName: 'icon.png',
fileMimeType: 'image/png',
fileSizeBytes: 1024,
)
);
}
private function createElement(): Element
{
$set = new DomainSet(
id: 1,
name: 'Baderech',
description: 'Baderech description',
iconImageUrl: '/assets/baderech-icon.png',
);
return $this->elementRepository->create(new CreateElementDto(
set: $set,
title: 'Original title',
description: 'Original description',
iconImageUrl: null,
richText: '',
shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null,
parentElement: null,
));
}
}

View file

@ -4,8 +4,8 @@ namespace Tests\Unit\Element\UseCases;
use App\Element\CreateElementDto; use App\Element\CreateElementDto;
use App\Element\Element; use App\Element\Element;
use App\Element\UseCases\UpdateElement\UpdatePdf; use App\Element\UseCases\UpdateElement\UpdateShortPdf;
use App\Element\UseCases\UpdateElement\UpdatePdfRequest; use App\Element\UseCases\UpdateElement\UpdateShortPdfRequest;
use App\Exceptions\BadRequestException; use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException; use App\Exceptions\NotFoundException;
use App\Set\Set as DomainSet; use App\Set\Set as DomainSet;
@ -13,19 +13,19 @@ use Tests\Fakes\FakeElementRepository;
use Tests\Fakes\FakeFileUploader; use Tests\Fakes\FakeFileUploader;
use Tests\TestCase; use Tests\TestCase;
class UpdatePdfTest extends TestCase class UpdateShortPdfTest extends TestCase
{ {
private FakeElementRepository $elementRepository; private FakeElementRepository $elementRepository;
private FakeFileUploader $fileUploader; private FakeFileUploader $fileUploader;
private UpdatePdf $useCase; private UpdateShortPdf $useCase;
protected function setUp(): void protected function setUp(): void
{ {
$this->elementRepository = new FakeElementRepository(); $this->elementRepository = new FakeElementRepository();
$this->fileUploader = new FakeFileUploader(); $this->fileUploader = new FakeFileUploader();
$this->useCase = new UpdatePdf( $this->useCase = new UpdateShortPdf(
$this->elementRepository, $this->elementRepository,
$this->fileUploader, $this->fileUploader,
); );
@ -33,45 +33,46 @@ class UpdatePdfTest extends TestCase
/** /**
* @return array{ * @return array{
* pdfContents: string, * fileContents: string,
* pdfOriginalName: string, * fileOriginalName: string,
* pdfMimeType: string, * fileMimeType: string,
* pdfSizeBytes: int * fileSizeBytes: int
* } * }
*/ */
private function pdfArgs(): array private function pdfArgs(): array
{ {
return [ return [
'pdfContents' => 'binary-pdf-bytes', 'fileContents' => 'binary-pdf-bytes',
'pdfOriginalName' => 'baderech.pdf', 'fileOriginalName' => 'baderech.pdf',
'pdfMimeType' => 'application/pdf', 'fileMimeType' => 'application/pdf',
'pdfSizeBytes' => 1024, 'fileSizeBytes' => 1024,
]; ];
} }
public function testUploadsPdfAndStoresPath(): void public function testUploadsShortPdfAndStoresPath(): void
{ {
$element = $this->createElement(); $element = $this->createElement();
$updatedElement = $this->useCase->execute( $updatedElement = $this->useCase->execute(
new UpdatePdfRequest( new UpdateShortPdfRequest(
...$this->pdfArgs(), ...$this->pdfArgs(),
id: $element->getId(), id: $element->getId(),
) )
); );
$this->assertSame( $this->assertSame(
'element-pdfs/fake-baderech.pdf', 'element-pdfs/short/fake-baderech.pdf',
$updatedElement->getPdfPath(), $updatedElement->getShortPdfPath(),
); );
$this->assertNull($updatedElement->getLongPdfPath());
$storedElement = $this->elementRepository->find($element->getId()); $storedElement = $this->elementRepository->find($element->getId());
$this->assertNotNull($storedElement); $this->assertNotNull($storedElement);
$this->assertSame( $this->assertSame(
'element-pdfs/fake-baderech.pdf', 'element-pdfs/short/fake-baderech.pdf',
$storedElement->getPdfPath(), $storedElement->getShortPdfPath(),
); );
$this->assertSame( $this->assertSame(
'element-pdfs', 'element-pdfs/short',
$this->fileUploader->uploads[0]['folder'], $this->fileUploader->uploads[0]['folder'],
); );
} }
@ -82,7 +83,7 @@ class UpdatePdfTest extends TestCase
$this->expectExceptionMessage('id is required'); $this->expectExceptionMessage('id is required');
$this->useCase->execute( $this->useCase->execute(
new UpdatePdfRequest( new UpdateShortPdfRequest(
...$this->pdfArgs(), ...$this->pdfArgs(),
id: null, id: null,
) )
@ -95,12 +96,12 @@ class UpdatePdfTest extends TestCase
$this->expectExceptionMessage('pdf is required'); $this->expectExceptionMessage('pdf is required');
$this->useCase->execute( $this->useCase->execute(
new UpdatePdfRequest( new UpdateShortPdfRequest(
id: 1, id: 1,
pdfContents: null, fileContents: null,
pdfOriginalName: null, fileOriginalName: null,
pdfMimeType: null, fileMimeType: null,
pdfSizeBytes: null, fileSizeBytes: null,
) )
); );
} }
@ -111,12 +112,12 @@ class UpdatePdfTest extends TestCase
$this->expectExceptionMessage('pdf must be a pdf file'); $this->expectExceptionMessage('pdf must be a pdf file');
$this->useCase->execute( $this->useCase->execute(
new UpdatePdfRequest( new UpdateShortPdfRequest(
id: 1, id: 1,
pdfContents: 'not-a-pdf', fileContents: 'not-a-pdf',
pdfOriginalName: 'icon.png', fileOriginalName: 'icon.png',
pdfMimeType: 'image/png', fileMimeType: 'image/png',
pdfSizeBytes: 1024, fileSizeBytes: 1024,
) )
); );
} }
@ -127,12 +128,12 @@ class UpdatePdfTest extends TestCase
$this->expectExceptionMessage('pdf must be 10MB or smaller'); $this->expectExceptionMessage('pdf must be 10MB or smaller');
$this->useCase->execute( $this->useCase->execute(
new UpdatePdfRequest( new UpdateShortPdfRequest(
id: 1, id: 1,
pdfContents: 'big', fileContents: 'big',
pdfOriginalName: 'baderech.pdf', fileOriginalName: 'baderech.pdf',
pdfMimeType: 'application/pdf', fileMimeType: 'application/pdf',
pdfSizeBytes: 11 * 1024 * 1024, fileSizeBytes: 11 * 1024 * 1024,
) )
); );
} }
@ -143,7 +144,7 @@ class UpdatePdfTest extends TestCase
$this->expectExceptionMessage('Element not found'); $this->expectExceptionMessage('Element not found');
$this->useCase->execute( $this->useCase->execute(
new UpdatePdfRequest( new UpdateShortPdfRequest(
...$this->pdfArgs(), ...$this->pdfArgs(),
id: 999, id: 999,
) )
@ -165,7 +166,8 @@ class UpdatePdfTest extends TestCase
description: 'Original description', description: 'Original description',
iconImageUrl: null, iconImageUrl: null,
richText: '', richText: '',
pdfPath: null, shortPdfPath: null,
longPdfPath: null,
youtubeUrl: null, youtubeUrl: null,
parentElement: null, parentElement: null,
)); ));

View file

@ -126,28 +126,45 @@ describe('admin element editing', () => {
.and('have.attr', 'src') .and('have.attr', 'src')
.and('include', '/storage/element-icons/') .and('include', '/storage/element-icons/')
cy.get('[data-cy="admin-element-pdf-input"]').selectFile( cy.get('[data-cy="admin-element-short-pdf-input"]').selectFile(
'public/assets/pdfs/baderech.pdf', 'public/assets/pdfs/baderech.pdf',
{ force: true }, { force: true },
) )
cy.wait('@updateElement') cy.wait('@updateElement')
cy.get('[data-cy="admin-element-pdf-status"]') cy.get('[data-cy="admin-element-short-pdf-status"]')
.should('be.visible') .should('be.visible')
.and('contain.text', 'PDF updated') .and('contain.text', 'Short PDF updated')
cy.get('[data-cy="admin-element-current-pdf"]') cy.get('[data-cy="admin-element-current-short-pdf"]')
.should('be.visible') .should('be.visible')
.and('have.attr', 'href') .and('have.attr', 'href')
.and('include', '/storage/element-pdfs/') .and('include', '/storage/element-pdfs/short/')
cy.get('[data-cy="admin-element-long-pdf-input"]').selectFile(
'public/assets/pdfs/baderech.pdf',
{ force: true },
)
cy.wait('@updateElement')
cy.get('[data-cy="admin-element-long-pdf-status"]')
.should('be.visible')
.and('contain.text', 'Long PDF updated')
cy.get('[data-cy="admin-element-current-long-pdf"]')
.should('be.visible')
.and('have.attr', 'href')
.and('include', '/storage/element-pdfs/long/')
cy.contains('header.site-header a', 'View Element').click() cy.contains('header.site-header a', 'View Element').click()
cy.get('[data-cy="element-icon"]') cy.get('[data-cy="element-icon"]')
.should('be.visible') .should('be.visible')
.and('have.attr', 'src') .and('have.attr', 'src')
.and('include', '/storage/element-icons/') .and('include', '/storage/element-icons/')
cy.get('[data-cy="element-pdf-link"]') cy.get('[data-cy="element-short-pdf-link"]')
.should('be.visible') .should('be.visible')
.and('have.attr', 'href') .and('have.attr', 'href')
.and('include', '/storage/element-pdfs/') .and('include', '/storage/element-pdfs/short/')
cy.get('[data-cy="element-long-pdf-link"]')
.should('be.visible')
.and('have.attr', 'href')
.and('include', '/storage/element-pdfs/long/')
cy.resetDb() cy.resetDb()
}) })
@ -163,7 +180,12 @@ describe('admin element editing', () => {
{ force: true }, { force: true },
) )
cy.wait('@updateElement') cy.wait('@updateElement')
cy.get('[data-cy="admin-element-pdf-input"]').selectFile( cy.get('[data-cy="admin-element-short-pdf-input"]').selectFile(
'public/assets/pdfs/baderech.pdf',
{ force: true },
)
cy.wait('@updateElement')
cy.get('[data-cy="admin-element-long-pdf-input"]').selectFile(
'public/assets/pdfs/baderech.pdf', 'public/assets/pdfs/baderech.pdf',
{ force: true }, { force: true },
) )
@ -177,17 +199,26 @@ describe('admin element editing', () => {
cy.get('[data-cy="admin-element-current-icon"]').should('not.exist') cy.get('[data-cy="admin-element-current-icon"]').should('not.exist')
cy.contains('No icon image').should('be.visible') cy.contains('No icon image').should('be.visible')
cy.contains('button', 'Remove PDF').click() cy.contains('button', 'Remove short PDF').click()
cy.wait('@updateElement') cy.wait('@updateElement')
cy.get('[data-cy="admin-element-pdf-status"]') cy.get('[data-cy="admin-element-short-pdf-status"]')
.should('be.visible') .should('be.visible')
.and('contain.text', 'PDF removed') .and('contain.text', 'Short PDF removed')
cy.get('[data-cy="admin-element-current-pdf"]').should('not.exist') cy.get('[data-cy="admin-element-current-short-pdf"]').should('not.exist')
cy.contains('No PDF').should('be.visible') cy.contains('No short PDF').should('be.visible')
cy.contains('button', 'Remove long PDF').click()
cy.wait('@updateElement')
cy.get('[data-cy="admin-element-long-pdf-status"]')
.should('be.visible')
.and('contain.text', 'Long PDF removed')
cy.get('[data-cy="admin-element-current-long-pdf"]').should('not.exist')
cy.contains('No long PDF').should('be.visible')
cy.contains('header.site-header a', 'View Element').click() cy.contains('header.site-header a', 'View Element').click()
cy.get('[data-cy="element-icon"]').should('not.exist') cy.get('[data-cy="element-icon"]').should('not.exist')
cy.get('[data-cy="element-pdf-link"]').should('not.exist') cy.get('[data-cy="element-short-pdf-link"]').should('not.exist')
cy.get('[data-cy="element-long-pdf-link"]').should('not.exist')
cy.resetDb() cy.resetDb()
}) })

View file

@ -51,7 +51,8 @@ describe('media page sets', () => {
cy.get('[data-cy="element-rich-text"]').should('not.exist') cy.get('[data-cy="element-rich-text"]').should('not.exist')
cy.get('[data-cy="element-youtube-embed"]') cy.get('[data-cy="element-youtube-embed"]')
.should('not.exist') .should('not.exist')
cy.get('[data-cy="element-pdf-link"]').should('not.exist') cy.get('[data-cy="element-short-pdf-link"]').should('not.exist')
cy.get('[data-cy="element-long-pdf-link"]').should('not.exist')
cy.get('[data-cy="child-element-list"]').should('be.visible') cy.get('[data-cy="child-element-list"]').should('be.visible')
cy.get('[data-cy="child-element-list"]') cy.get('[data-cy="child-element-list"]')
.should( .should(
@ -116,13 +117,14 @@ describe('media page sets', () => {
) )
.should('have.css', 'text-align', 'center') .should('have.css', 'text-align', 'center')
cy.get('[data-cy="element-youtube-embed"]').should('not.exist') cy.get('[data-cy="element-youtube-embed"]').should('not.exist')
cy.contains('[data-cy="element-pdf-link"]', 'View PDF') cy.contains('[data-cy="element-short-pdf-link"]', 'View Short PDF')
.as('pdfLink') .as('shortPdfLink')
.should('be.visible') .should('be.visible')
.and('have.attr', 'target', '_blank') .and('have.attr', 'target', '_blank')
cy.get('@pdfLink') cy.get('@shortPdfLink')
.and('have.attr', 'href') .and('have.attr', 'href')
.and('include', '/storage/element-pdfs/baderech.pdf') .and('include', '/storage/element-pdfs/short/baderech.pdf')
cy.get('[data-cy="element-long-pdf-link"]').should('not.exist')
cy.contains('[data-cy="child-element-link"]', introductionAudioTitle) cy.contains('[data-cy="child-element-link"]', introductionAudioTitle)
.as('introductionAudioLink') .as('introductionAudioLink')
.should('have.attr', 'href', '/element/8') .should('have.attr', 'href', '/element/8')
@ -132,7 +134,8 @@ describe('media page sets', () => {
cy.contains('h1', introductionAudioTitle).should('be.visible') cy.contains('h1', introductionAudioTitle).should('be.visible')
cy.get('[data-cy="element-icon"]').should('not.exist') cy.get('[data-cy="element-icon"]').should('not.exist')
cy.get('[data-cy="element-rich-text"]').should('not.exist') cy.get('[data-cy="element-rich-text"]').should('not.exist')
cy.get('[data-cy="element-pdf-link"]').should('not.exist') cy.get('[data-cy="element-short-pdf-link"]').should('not.exist')
cy.get('[data-cy="element-long-pdf-link"]').should('not.exist')
cy.get('[data-cy="child-element-list"]').should('not.exist') cy.get('[data-cy="child-element-list"]').should('not.exist')
cy.get('[data-cy="element-youtube-embed"]') cy.get('[data-cy="element-youtube-embed"]')
.should('be.visible') .should('be.visible')

View file

@ -1,4 +1,4 @@
import { ref } from 'vue' import { ref, type Ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export interface ChildElement { export interface ChildElement {
@ -10,10 +10,13 @@ export interface ChildElement {
export interface Element extends ChildElement { export interface Element extends ChildElement {
iconImageUrl: string | null iconImageUrl: string | null
richText: string richText: string
pdfPath: string | null shortPdfPath: string | null
longPdfPath: string | null
youtubeUrl: string | null youtubeUrl: string | null
} }
type ElementFileType = 'iconImage' | 'shortPdf' | 'longPdf'
interface ElementResponse { interface ElementResponse {
element: Element element: Element
childElements: ChildElement[] childElements: ChildElement[]
@ -35,7 +38,8 @@ interface ElementPatchInput {
description?: string description?: string
iconImageUrl?: string iconImageUrl?: string
richText?: string richText?: string
pdfPath?: string shortPdfPath?: string
longPdfPath?: string
youtubeUrl?: string youtubeUrl?: string
} }
@ -53,7 +57,8 @@ export const useElementsStore = defineStore('elements', () => {
const error = ref<string | null>(null) const error = ref<string | null>(null)
const isSaving = ref(false) const isSaving = ref(false)
const isUploadingIconImage = ref(false) const isUploadingIconImage = ref(false)
const isUploadingPdf = ref(false) const isUploadingShortPdf = ref(false)
const isUploadingLongPdf = ref(false)
const saveError = ref<string | null>(null) const saveError = ref<string | null>(null)
async function fetchElement(elementId: string): Promise<void> { async function fetchElement(elementId: string): Promise<void> {
@ -89,16 +94,35 @@ export const useElementsStore = defineStore('elements', () => {
} }
} }
async function updateElement(elementId: string, input: UpdateElementInput): Promise<boolean> { async function updateElement(
elementId: string,
input: UpdateElementInput,
): Promise<boolean> {
return await saveElementPatch(elementId, input, 'Could not save element') return await saveElementPatch(elementId, input, 'Could not save element')
} }
async function clearElementIconImage(elementId: string): Promise<boolean> { async function clearElementIconImage(elementId: string): Promise<boolean> {
return await saveElementPatch(elementId, { iconImageUrl: '' }, 'Could not remove icon image') return await saveElementPatch(
elementId,
{ iconImageUrl: '' },
'Could not remove icon image',
)
} }
async function clearElementPdf(elementId: string): Promise<boolean> { async function clearElementShortPdf(elementId: string): Promise<boolean> {
return await saveElementPatch(elementId, { pdfPath: '' }, 'Could not remove PDF') return await saveElementPatch(
elementId,
{ shortPdfPath: '' },
'Could not remove short PDF',
)
}
async function clearElementLongPdf(elementId: string): Promise<boolean> {
return await saveElementPatch(
elementId,
{ longPdfPath: '' },
'Could not remove long PDF',
)
} }
async function saveElementPatch( async function saveElementPatch(
@ -126,38 +150,64 @@ export const useElementsStore = defineStore('elements', () => {
} }
} }
async function uploadElementIconImage(elementId: string, file: File): Promise<boolean> { async function uploadElementIconImage(
saveError.value = null elementId: string,
isUploadingIconImage.value = true file: File,
): Promise<boolean> {
try { return await uploadElementFile(
const formData = new FormData() elementId,
formData.append('elementId', elementId) file,
formData.append('iconImage', file) 'iconImage',
const response = await fetch(ELEMENT_UPDATE_URL, { isUploadingIconImage,
method: 'POST', 'Could not upload icon image',
headers: { Accept: 'application/json' }, 'Network error - could not upload icon image',
credentials: 'include', )
body: formData,
})
return await handleElementResponse(response, 'Could not upload icon image')
} catch {
saveError.value = 'Network error - could not upload icon image'
return false
} finally {
isUploadingIconImage.value = false
}
} }
async function uploadElementPdf(elementId: string, file: File): Promise<boolean> { async function uploadElementShortPdf(
elementId: string,
file: File,
): Promise<boolean> {
return await uploadElementFile(
elementId,
file,
'shortPdf',
isUploadingShortPdf,
'Could not upload short PDF',
'Network error - could not upload short PDF',
)
}
async function uploadElementLongPdf(
elementId: string,
file: File,
): Promise<boolean> {
return await uploadElementFile(
elementId,
file,
'longPdf',
isUploadingLongPdf,
'Could not upload long PDF',
'Network error - could not upload long PDF',
)
}
async function uploadElementFile(
elementId: string,
file: File,
fileType: ElementFileType,
isUploadingFile: Ref<boolean>,
failureMessage: string,
networkErrorMessage: string,
): Promise<boolean> {
saveError.value = null saveError.value = null
isUploadingPdf.value = true isUploadingFile.value = true
try { try {
const formData = new FormData() const formData = new FormData()
formData.append('elementId', elementId) formData.append('elementId', elementId)
formData.append('pdf', file) formData.append('fileType', fileType)
formData.append('file', file)
const response = await fetch(ELEMENT_UPDATE_URL, { const response = await fetch(ELEMENT_UPDATE_URL, {
method: 'POST', method: 'POST',
headers: { Accept: 'application/json' }, headers: { Accept: 'application/json' },
@ -165,12 +215,12 @@ export const useElementsStore = defineStore('elements', () => {
body: formData, body: formData,
}) })
return await handleElementResponse(response, 'Could not upload PDF') return await handleElementResponse(response, failureMessage)
} catch { } catch {
saveError.value = 'Network error - could not upload PDF' saveError.value = networkErrorMessage
return false return false
} finally { } finally {
isUploadingPdf.value = false isUploadingFile.value = false
} }
} }
@ -203,7 +253,10 @@ export const useElementsStore = defineStore('elements', () => {
return true return true
} }
async function errorMessage(response: Response, fallbackMessage: string): Promise<string> { async function errorMessage(
response: Response,
fallbackMessage: string,
): Promise<string> {
try { try {
const data: ErrorResponse = await response.json() const data: ErrorResponse = await response.json()
return data.error ?? fallbackMessage return data.error ?? fallbackMessage
@ -219,13 +272,16 @@ export const useElementsStore = defineStore('elements', () => {
error, error,
isSaving, isSaving,
isUploadingIconImage, isUploadingIconImage,
isUploadingPdf, isUploadingShortPdf,
isUploadingLongPdf,
saveError, saveError,
fetchElement, fetchElement,
updateElement, updateElement,
uploadElementIconImage, uploadElementIconImage,
uploadElementPdf, uploadElementShortPdf,
uploadElementLongPdf,
clearElementIconImage, clearElementIconImage,
clearElementPdf, clearElementShortPdf,
clearElementLongPdf,
} }
}) })

View file

@ -14,13 +14,23 @@ interface ElementForm {
const route = useRoute() const route = useRoute()
const elementsStore = useElementsStore() const elementsStore = useElementsStore()
const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } = const {
storeToRefs(elementsStore) element,
isLoading,
error,
isSaving,
isUploadingIconImage,
isUploadingShortPdf,
isUploadingLongPdf,
saveError,
} = storeToRefs(elementsStore)
const savedMessage = ref<string | null>(null) const savedMessage = ref<string | null>(null)
const iconImageStatus = ref<string | null>(null) const iconImageStatus = ref<string | null>(null)
const pdfStatus = ref<string | null>(null) const shortPdfStatus = ref<string | null>(null)
const longPdfStatus = ref<string | null>(null)
const iconImageInput = ref<HTMLInputElement | null>(null) const iconImageInput = ref<HTMLInputElement | null>(null)
const pdfInput = ref<HTMLInputElement | null>(null) const shortPdfInput = ref<HTMLInputElement | null>(null)
const longPdfInput = ref<HTMLInputElement | null>(null)
const form = reactive<ElementForm>({ const form = reactive<ElementForm>({
title: '', title: '',
@ -56,7 +66,8 @@ watch(
savedMessage.value = null savedMessage.value = null
iconImageStatus.value = null iconImageStatus.value = null
pdfStatus.value = null shortPdfStatus.value = null
longPdfStatus.value = null
void elementsStore.fetchElement(currentElementId) void elementsStore.fetchElement(currentElementId)
}, },
{ immediate: true }, { immediate: true },
@ -95,8 +106,12 @@ function chooseIconImage(): void {
iconImageInput.value?.click() iconImageInput.value?.click()
} }
function choosePdf(): void { function chooseShortPdf(): void {
pdfInput.value?.click() shortPdfInput.value?.click()
}
function chooseLongPdf(): void {
longPdfInput.value?.click()
} }
async function handleIconImageChange(changeEvent: Event): Promise<void> { async function handleIconImageChange(changeEvent: Event): Promise<void> {
@ -107,7 +122,10 @@ async function handleIconImageChange(changeEvent: Event): Promise<void> {
savedMessage.value = null savedMessage.value = null
iconImageStatus.value = null iconImageStatus.value = null
const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile) const uploaded = await elementsStore.uploadElementIconImage(
elementId.value,
selectedFile,
)
resetInput(changeEvent) resetInput(changeEvent)
if (uploaded) { if (uploaded) {
@ -115,19 +133,41 @@ async function handleIconImageChange(changeEvent: Event): Promise<void> {
} }
} }
async function handlePdfChange(changeEvent: Event): Promise<void> { async function handleShortPdfChange(changeEvent: Event): Promise<void> {
const selectedFile = selectedInputFile(changeEvent) const selectedFile = selectedInputFile(changeEvent)
if (selectedFile === null || elementId.value === '') { if (selectedFile === null || elementId.value === '') {
return return
} }
savedMessage.value = null savedMessage.value = null
pdfStatus.value = null shortPdfStatus.value = null
const uploaded = await elementsStore.uploadElementPdf(elementId.value, selectedFile) const uploaded = await elementsStore.uploadElementShortPdf(
elementId.value,
selectedFile,
)
resetInput(changeEvent) resetInput(changeEvent)
if (uploaded) { if (uploaded) {
pdfStatus.value = 'PDF updated' shortPdfStatus.value = 'Short PDF updated'
}
}
async function handleLongPdfChange(changeEvent: Event): Promise<void> {
const selectedFile = selectedInputFile(changeEvent)
if (selectedFile === null || elementId.value === '') {
return
}
savedMessage.value = null
longPdfStatus.value = null
const uploaded = await elementsStore.uploadElementLongPdf(
elementId.value,
selectedFile,
)
resetInput(changeEvent)
if (uploaded) {
longPdfStatus.value = 'Long PDF updated'
} }
} }
@ -144,16 +184,29 @@ async function handleRemoveIconImage(): Promise<void> {
} }
} }
async function handleRemovePdf(): Promise<void> { async function handleRemoveShortPdf(): Promise<void> {
if (elementId.value === '') { if (elementId.value === '') {
return return
} }
savedMessage.value = null savedMessage.value = null
pdfStatus.value = null shortPdfStatus.value = null
const removed = await elementsStore.clearElementPdf(elementId.value) const removed = await elementsStore.clearElementShortPdf(elementId.value)
if (removed) { if (removed) {
pdfStatus.value = 'PDF removed' shortPdfStatus.value = 'Short PDF removed'
}
}
async function handleRemoveLongPdf(): Promise<void> {
if (elementId.value === '') {
return
}
savedMessage.value = null
longPdfStatus.value = null
const removed = await elementsStore.clearElementLongPdf(elementId.value)
if (removed) {
longPdfStatus.value = 'Long PDF removed'
} }
} }
@ -186,7 +239,9 @@ function resetInput(changeEvent: Event): void {
<h1 class="admin-element-page__heading">Edit Element</h1> <h1 class="admin-element-page__heading">Edit Element</h1>
</header> </header>
<p v-if="isLoading" class="admin-element-page__status">Loading element...</p> <p v-if="isLoading" class="admin-element-page__status">
Loading element...
</p>
<p <p
v-else-if="error" v-else-if="error"
class="admin-element-page__status admin-element-page__status--error" class="admin-element-page__status admin-element-page__status--error"
@ -194,7 +249,11 @@ function resetInput(changeEvent: Event): void {
> >
{{ error }} {{ error }}
</p> </p>
<form v-else-if="element" class="admin-element-page__form" @submit.prevent="handleSubmit"> <form
v-else-if="element"
class="admin-element-page__form"
@submit.prevent="handleSubmit"
>
<label class="admin-element-page__field"> <label class="admin-element-page__field">
<span class="admin-element-page__label">Title</span> <span class="admin-element-page__label">Title</span>
<input <input
@ -255,7 +314,10 @@ function resetInput(changeEvent: Event): void {
</div> </div>
<p <p
v-if="iconImageStatus !== null" v-if="iconImageStatus !== null"
class="admin-element-page__status admin-element-page__status--inline" :class="[
'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-icon-image-status" data-cy="admin-element-icon-image-status"
aria-live="polite" aria-live="polite"
> >
@ -274,53 +336,110 @@ function resetInput(changeEvent: Event): void {
</label> </label>
<section class="admin-element-page__media-field"> <section class="admin-element-page__media-field">
<span class="admin-element-page__label">PDF</span> <span class="admin-element-page__label">Short PDF</span>
<a <a
v-if="element.pdfPath !== null" v-if="element.shortPdfPath !== null"
:href="element.pdfPath" :href="element.shortPdfPath"
class="admin-element-page__pdf-link" class="admin-element-page__pdf-link"
data-cy="admin-element-current-pdf" data-cy="admin-element-current-short-pdf"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
View current PDF View current short PDF
</a> </a>
<p v-else class="admin-element-page__empty-media">No PDF</p> <p v-else class="admin-element-page__empty-media">No short PDF</p>
<div class="admin-element-page__media-actions"> <div class="admin-element-page__media-actions">
<input <input
ref="pdfInput" ref="shortPdfInput"
class="admin-element-page__file-input" class="admin-element-page__file-input"
data-cy="admin-element-pdf-input" data-cy="admin-element-short-pdf-input"
type="file" type="file"
accept="application/pdf" accept="application/pdf"
:disabled="isUploadingPdf" :disabled="isUploadingShortPdf"
@change="handlePdfChange" @change="handleShortPdfChange"
/> />
<button <button
class="admin-element-page__secondary-button" class="admin-element-page__secondary-button"
type="button" type="button"
:disabled="isUploadingPdf" :disabled="isUploadingShortPdf"
@click="choosePdf" @click="chooseShortPdf"
> >
{{ isUploadingPdf ? 'Uploading...' : 'Choose PDF' }} {{ isUploadingShortPdf ? 'Uploading...' : 'Choose short PDF' }}
</button> </button>
<button <button
v-if="element.pdfPath !== null" v-if="element.shortPdfPath !== null"
class="admin-element-page__secondary-button" class="admin-element-page__secondary-button"
type="button" type="button"
:disabled="isSaving || isUploadingPdf" :disabled="isSaving || isUploadingShortPdf"
@click="handleRemovePdf" @click="handleRemoveShortPdf"
> >
Remove PDF Remove short PDF
</button> </button>
</div> </div>
<p <p
v-if="pdfStatus !== null" v-if="shortPdfStatus !== null"
class="admin-element-page__status admin-element-page__status--inline" :class="[
data-cy="admin-element-pdf-status" 'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-short-pdf-status"
aria-live="polite" aria-live="polite"
> >
{{ pdfStatus }} {{ shortPdfStatus }}
</p>
</section>
<section class="admin-element-page__media-field">
<span class="admin-element-page__label">Long PDF</span>
<a
v-if="element.longPdfPath !== null"
:href="element.longPdfPath"
class="admin-element-page__pdf-link"
data-cy="admin-element-current-long-pdf"
target="_blank"
rel="noreferrer"
>
View current long PDF
</a>
<p v-else class="admin-element-page__empty-media">No long PDF</p>
<div class="admin-element-page__media-actions">
<input
ref="longPdfInput"
class="admin-element-page__file-input"
data-cy="admin-element-long-pdf-input"
type="file"
accept="application/pdf"
:disabled="isUploadingLongPdf"
@change="handleLongPdfChange"
/>
<button
class="admin-element-page__secondary-button"
type="button"
:disabled="isUploadingLongPdf"
@click="chooseLongPdf"
>
{{ isUploadingLongPdf ? 'Uploading...' : 'Choose long PDF' }}
</button>
<button
v-if="element.longPdfPath !== null"
class="admin-element-page__secondary-button"
type="button"
:disabled="isSaving || isUploadingLongPdf"
@click="handleRemoveLongPdf"
>
Remove long PDF
</button>
</div>
<p
v-if="longPdfStatus !== null"
:class="[
'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-long-pdf-status"
aria-live="polite"
>
{{ longPdfStatus }}
</p> </p>
</section> </section>
@ -345,7 +464,10 @@ function resetInput(changeEvent: Event): void {
</button> </button>
<p <p
v-if="statusMessage !== null" v-if="statusMessage !== null"
:class="['admin-element-page__status', 'admin-element-page__status--inline']" :class="[
'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-status" data-cy="admin-element-status"
aria-live="polite" aria-live="polite"
> >

View file

@ -242,15 +242,29 @@ function isShortYoutubeHost(hostname: string): boolean {
/> />
</div> </div>
<div v-if="element.pdfPath !== null" class="element-page__actions"> <div
v-if="element.shortPdfPath !== null || element.longPdfPath !== null"
class="element-page__actions"
>
<a <a
:href="element.pdfPath" v-if="element.shortPdfPath !== null"
:href="element.shortPdfPath"
class="element-page__pdf-link" class="element-page__pdf-link"
data-cy="element-pdf-link" data-cy="element-short-pdf-link"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
View PDF View Short PDF
</a>
<a
v-if="element.longPdfPath !== null"
:href="element.longPdfPath"
class="element-page__pdf-link"
data-cy="element-long-pdf-link"
target="_blank"
rel="noreferrer"
>
View Long PDF
</a> </a>
</div> </div>
@ -360,7 +374,9 @@ function isShortYoutubeHost(hostname: string): boolean {
.element-page__actions { .element-page__actions {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 0.75rem;
margin-top: 1.75rem; margin-top: 1.75rem;
} }