Stream tenant uploads
This commit is contained in:
@@ -40,7 +40,7 @@
|
|||||||
{"id":"fotospiel-app-auq","title":"Security review checklist: Media pipeline/storage dynamic tests","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:04:57.616770583+01:00","created_by":"soeren","updated_at":"2026-01-01T16:04:57.616770583+01:00"}
|
{"id":"fotospiel-app-auq","title":"Security review checklist: Media pipeline/storage dynamic tests","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:04:57.616770583+01:00","created_by":"soeren","updated_at":"2026-01-01T16:04:57.616770583+01:00"}
|
||||||
{"id":"fotospiel-app-b0h","title":"Security review: trust boundaries/entrypoints mapped","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:03:43.175087637+01:00","created_by":"soeren","updated_at":"2026-01-01T16:03:48.799343248+01:00","closed_at":"2026-01-01T16:03:48.799343248+01:00","close_reason":"Completed in codebase (verified)"}
|
{"id":"fotospiel-app-b0h","title":"Security review: trust boundaries/entrypoints mapped","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:03:43.175087637+01:00","created_by":"soeren","updated_at":"2026-01-01T16:03:48.799343248+01:00","closed_at":"2026-01-01T16:03:48.799343248+01:00","close_reason":"Completed in codebase (verified)"}
|
||||||
{"id":"fotospiel-app-bep","title":"SEC-IO-01 Document PAT revocation/rotation playbook","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:51:44.568780967+01:00","created_by":"soeren","updated_at":"2026-01-01T15:51:44.568780967+01:00"}
|
{"id":"fotospiel-app-bep","title":"SEC-IO-01 Document PAT revocation/rotation playbook","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:51:44.568780967+01:00","created_by":"soeren","updated_at":"2026-01-01T15:51:44.568780967+01:00"}
|
||||||
{"id":"fotospiel-app-bit","title":"Superadmin control surface roadmap","description":"Roadmap to implement practical superadmin control over tenant admin + guest experience. Tracks lifecycle, moderation, policies, ops health, compliance, audit, announcements, integrations.","status":"open","priority":1,"issue_type":"epic","created_at":"2026-01-01T14:21:01.852988935+01:00","updated_at":"2026-01-01T14:21:01.852988935+01:00","dependencies":[{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-ihd","type":"blocks","created_at":"2026-01-01T14:21:14.445938122+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-wde","type":"blocks","created_at":"2026-01-01T14:21:16.788922347+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-hbt","type":"blocks","created_at":"2026-01-01T14:21:18.300493488+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-arp","type":"blocks","created_at":"2026-01-01T14:21:20.731646568+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-tym","type":"blocks","created_at":"2026-01-01T14:21:23.219093242+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-sbs","type":"blocks","created_at":"2026-01-01T14:21:24.67996941+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-iyc","type":"blocks","created_at":"2026-01-01T14:21:27.027185624+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-097","type":"blocks","created_at":"2026-01-01T14:21:29.668197239+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-lqp","type":"blocks","created_at":"2026-01-01T14:21:31.238481004+01:00","created_by":"soeren"}]}
|
{"id":"fotospiel-app-bit","title":"Superadmin control surface roadmap","description":"Roadmap to implement practical superadmin control over tenant admin + guest experience. Tracks lifecycle, moderation, policies, ops health, compliance, audit, announcements, integrations.","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-01-01T14:21:01.852988935+01:00","updated_at":"2026-01-02T20:18:55.835531926+01:00","closed_at":"2026-01-02T20:18:55.835531926+01:00","close_reason":"Closed","dependencies":[{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-ihd","type":"blocks","created_at":"2026-01-01T14:21:14.445938122+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-wde","type":"blocks","created_at":"2026-01-01T14:21:16.788922347+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-hbt","type":"blocks","created_at":"2026-01-01T14:21:18.300493488+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-arp","type":"blocks","created_at":"2026-01-01T14:21:20.731646568+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-tym","type":"blocks","created_at":"2026-01-01T14:21:23.219093242+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-sbs","type":"blocks","created_at":"2026-01-01T14:21:24.67996941+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-iyc","type":"blocks","created_at":"2026-01-01T14:21:27.027185624+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-097","type":"blocks","created_at":"2026-01-01T14:21:29.668197239+01:00","created_by":"soeren"},{"issue_id":"fotospiel-app-bit","depends_on_id":"fotospiel-app-lqp","type":"blocks","created_at":"2026-01-01T14:21:31.238481004+01:00","created_by":"soeren"}]}
|
||||||
{"id":"fotospiel-app-bjd","title":"Checkout refactor: auth/login/register flow alignment","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:06:09.920731675+01:00","created_by":"soeren","updated_at":"2026-01-01T16:06:15.500724195+01:00","closed_at":"2026-01-01T16:06:15.500724195+01:00","close_reason":"Completed in codebase (verified)"}
|
{"id":"fotospiel-app-bjd","title":"Checkout refactor: auth/login/register flow alignment","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:06:09.920731675+01:00","created_by":"soeren","updated_at":"2026-01-01T16:06:15.500724195+01:00","closed_at":"2026-01-01T16:06:15.500724195+01:00","close_reason":"Completed in codebase (verified)"}
|
||||||
{"id":"fotospiel-app-bqm","title":"Paddle catalog sync: unit tests for service + jobs","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:01:22.090498843+01:00","created_by":"soeren","updated_at":"2026-01-01T16:01:27.71412122+01:00","closed_at":"2026-01-01T16:01:27.71412122+01:00","close_reason":"Completed in codebase (verified)"}
|
{"id":"fotospiel-app-bqm","title":"Paddle catalog sync: unit tests for service + jobs","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:01:22.090498843+01:00","created_by":"soeren","updated_at":"2026-01-01T16:01:27.71412122+01:00","closed_at":"2026-01-01T16:01:27.71412122+01:00","close_reason":"Completed in codebase (verified)"}
|
||||||
{"id":"fotospiel-app-bxu","title":"Checkout refactor: Stripe/Paddle payment integration + webhooks","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:06:32.279485614+01:00","created_by":"soeren","updated_at":"2026-01-01T16:06:37.876950599+01:00","closed_at":"2026-01-01T16:06:37.876950599+01:00","close_reason":"Completed in codebase (verified)"}
|
{"id":"fotospiel-app-bxu","title":"Checkout refactor: Stripe/Paddle payment integration + webhooks","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:06:32.279485614+01:00","created_by":"soeren","updated_at":"2026-01-01T16:06:37.876950599+01:00","closed_at":"2026-01-01T16:06:37.876950599+01:00","close_reason":"Completed in codebase (verified)"}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
{"id":"fotospiel-app-jqy","title":"Tenant admin onboarding: Playwright skeleton for welcome flow","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:08:11.226297707+01:00","created_by":"soeren","updated_at":"2026-01-01T16:08:16.827679424+01:00","closed_at":"2026-01-01T16:08:16.827679424+01:00","close_reason":"Completed in codebase (verified)"}
|
{"id":"fotospiel-app-jqy","title":"Tenant admin onboarding: Playwright skeleton for welcome flow","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:08:11.226297707+01:00","created_by":"soeren","updated_at":"2026-01-01T16:08:16.827679424+01:00","closed_at":"2026-01-01T16:08:16.827679424+01:00","close_reason":"Completed in codebase (verified)"}
|
||||||
{"id":"fotospiel-app-ko0","title":"Security review checklist: Webhooks/Billing dynamic tests","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:04:51.987093237+01:00","created_by":"soeren","updated_at":"2026-01-01T16:04:51.987093237+01:00"}
|
{"id":"fotospiel-app-ko0","title":"Security review checklist: Webhooks/Billing dynamic tests","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:04:51.987093237+01:00","created_by":"soeren","updated_at":"2026-01-01T16:04:51.987093237+01:00"}
|
||||||
{"id":"fotospiel-app-kry","title":"Paddle catalog sync: add DTO helpers for Paddle product/price responses","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:58:57.817750548+01:00","created_by":"soeren","updated_at":"2026-01-01T15:58:57.817750548+01:00"}
|
{"id":"fotospiel-app-kry","title":"Paddle catalog sync: add DTO helpers for Paddle product/price responses","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:58:57.817750548+01:00","created_by":"soeren","updated_at":"2026-01-01T15:58:57.817750548+01:00"}
|
||||||
{"id":"fotospiel-app-kso","title":"SEC-MS-02 Streaming upload refactor + tests","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:53:03.729137616+01:00","created_by":"soeren","updated_at":"2026-01-01T15:53:03.729137616+01:00"}
|
{"id":"fotospiel-app-kso","title":"SEC-MS-02 Streaming upload refactor + tests","description":"Current state (code scan)\n- Guest uploads: App\\\\Http\\\\Controllers\\\\Api\\\\EventPublicController@upload uses Storage::disk()-\u003eputFile (stream-friendly) but still does watermark/thumbnail work inline.\n- Tenant admin uploads: App\\\\Http\\\\Controllers\\\\Api\\\\Tenant\\\\PhotoController@store and @uploadDirect use Storage::disk()-\u003eput($path, file_get_contents(...)) which loads entire file into memory.\n- Photobooth ingest already streams from import disk via readStream -\u003e Storage::disk()-\u003eput($path, $stream).\n- Presigned upload flow is stubbed to a local upload-direct endpoint; no true presigned S3 handling yet.\n- No tenant upload feature tests exist; guest upload tests exist and cover limits/security.\n\nGoal\n- Stream uploads to disk (avoid full in-memory buffers) for tenant-admin upload endpoints and keep behavior consistent across sources.\n\nPlan\n1) Introduce a small streaming upload helper/service\n - New service (e.g. App\\\\Services\\\\Storage\\\\UploadStreamService) that accepts UploadedFile + disk + destination path.\n - Use fopen on UploadedFile::getRealPath (or $file-\u003egetStream()) and Storage::disk($disk)-\u003eput($path, $stream) / writeStream.\n - Always close stream; return stored size and checksum (hash_file on stored path) for asset metadata.\n\n2) Refactor tenant upload endpoints to use streaming\n - Update PhotoController@store and @uploadDirect to use the helper instead of file_get_contents.\n - Use Storage::disk()-\u003eputFileAs (or helper) to preserve deterministic paths without buffering.\n - Keep existing validation, watermark, thumbnail, asset recording, and package usage logic.\n\n3) Optional consistency pass on guest upload\n - Consider routing EventPublicController@upload through the same helper for consistent storage + checksum handling, while keeping current validation/limits.\n\n4) Tests\n - Add Feature tests for tenant upload endpoints:\n - /api/v1/tenant/events/{slug}/photos (store) uploads a fake image and persists Photo + EventMediaAsset with expected path/size.\n - /api/v1/tenant/events/{slug}/upload-direct (presigned) uploads a fake image and stores asset + thumbnail.\n - Ensure existing guest upload tests still pass (no behavioral changes).\n\n5) Safety/ops\n - Verify streaming logic handles empty/invalid files gracefully and still reports errors via ApiError.\n - Keep request-time processing (thumb/watermark) unchanged for now; consider queuing in a follow-up if CPU spikes persist.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T15:53:03.729137616+01:00","created_by":"soeren","updated_at":"2026-01-02T20:51:17.752365339+01:00","closed_at":"2026-01-02T20:51:17.752365339+01:00","close_reason":"Closed"}
|
||||||
{"id":"fotospiel-app-kxe","title":"Paddle customer success metrics (tenant ↔ Paddle sync, sandbox seeding, rollout/rollback)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:10:34.454400334+01:00","created_by":"soeren","updated_at":"2026-01-02T17:03:51.48872094+01:00","closed_at":"2026-01-02T17:03:51.48872094+01:00","close_reason":"Closed"}
|
{"id":"fotospiel-app-kxe","title":"Paddle customer success metrics (tenant ↔ Paddle sync, sandbox seeding, rollout/rollback)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:10:34.454400334+01:00","created_by":"soeren","updated_at":"2026-01-02T17:03:51.48872094+01:00","closed_at":"2026-01-02T17:03:51.48872094+01:00","close_reason":"Closed"}
|
||||||
{"id":"fotospiel-app-l3n","title":"Session changes 2025-09-08 (PRP split, PWA scaffolding, Filament resources, API)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:10:18.204088457+01:00","created_by":"soeren","updated_at":"2026-01-01T16:10:23.815135505+01:00","closed_at":"2026-01-01T16:10:23.815135505+01:00","close_reason":"Completed in codebase (verified)"}
|
{"id":"fotospiel-app-l3n","title":"Session changes 2025-09-08 (PRP split, PWA scaffolding, Filament resources, API)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:10:18.204088457+01:00","created_by":"soeren","updated_at":"2026-01-01T16:10:23.815135505+01:00","closed_at":"2026-01-01T16:10:23.815135505+01:00","close_reason":"Completed in codebase (verified)"}
|
||||||
{"id":"fotospiel-app-l6a","title":"Registration flow fixes: JSON redirect, error clearing, role handling","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:07:16.253760139+01:00","created_by":"soeren","updated_at":"2026-01-01T16:07:21.964843904+01:00","closed_at":"2026-01-01T16:07:21.964843904+01:00","close_reason":"Completed in codebase (verified)"}
|
{"id":"fotospiel-app-l6a","title":"Registration flow fixes: JSON redirect, error clearing, role handling","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:07:16.253760139+01:00","created_by":"soeren","updated_at":"2026-01-01T16:07:21.964843904+01:00","closed_at":"2026-01-01T16:07:21.964843904+01:00","close_reason":"Completed in codebase (verified)"}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
fotospiel-app-sbs
|
fotospiel-app-kso
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Services\Packages\PackageUsageTracker;
|
|||||||
use App\Services\Storage\EventStorageManager;
|
use App\Services\Storage\EventStorageManager;
|
||||||
use App\Support\ApiError;
|
use App\Support\ApiError;
|
||||||
use App\Support\ImageHelper;
|
use App\Support\ImageHelper;
|
||||||
|
use App\Support\UploadStream;
|
||||||
use App\Support\WatermarkConfigResolver;
|
use App\Support\WatermarkConfigResolver;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -318,7 +319,7 @@ class PhotoController extends Controller
|
|||||||
$path = "events/{$eventSlug}/photos/{$filename}";
|
$path = "events/{$eventSlug}/photos/{$filename}";
|
||||||
|
|
||||||
// Store original file
|
// Store original file
|
||||||
Storage::disk($disk)->put($path, file_get_contents($file->getRealPath()));
|
UploadStream::putUploadedFile($disk, $path, $file);
|
||||||
|
|
||||||
// Generate thumbnail
|
// Generate thumbnail
|
||||||
$thumbnailPath = "events/{$eventSlug}/thumbnails/{$filename}";
|
$thumbnailPath = "events/{$eventSlug}/thumbnails/{$filename}";
|
||||||
@@ -354,6 +355,7 @@ class PhotoController extends Controller
|
|||||||
|
|
||||||
$photoAttributes = [
|
$photoAttributes = [
|
||||||
'event_id' => $event->id,
|
'event_id' => $event->id,
|
||||||
|
'guest_name' => Photo::SOURCE_TENANT_ADMIN,
|
||||||
'original_name' => $file->getClientOriginalName(),
|
'original_name' => $file->getClientOriginalName(),
|
||||||
'mime_type' => $file->getMimeType(),
|
'mime_type' => $file->getMimeType(),
|
||||||
'size' => $file->getSize(),
|
'size' => $file->getSize(),
|
||||||
@@ -904,7 +906,7 @@ class PhotoController extends Controller
|
|||||||
$path = "events/{$eventSlug}/photos/{$filename}";
|
$path = "events/{$eventSlug}/photos/{$filename}";
|
||||||
|
|
||||||
// Store file
|
// Store file
|
||||||
Storage::disk($disk)->put($path, file_get_contents($file->getRealPath()));
|
UploadStream::putUploadedFile($disk, $path, $file);
|
||||||
|
|
||||||
// Generate thumbnail
|
// Generate thumbnail
|
||||||
$thumbnailPath = "events/{$eventSlug}/thumbnails/{$filename}";
|
$thumbnailPath = "events/{$eventSlug}/thumbnails/{$filename}";
|
||||||
@@ -915,6 +917,7 @@ class PhotoController extends Controller
|
|||||||
|
|
||||||
$photoAttributes = [
|
$photoAttributes = [
|
||||||
'event_id' => $event->id,
|
'event_id' => $event->id,
|
||||||
|
'guest_name' => Photo::SOURCE_TENANT_ADMIN,
|
||||||
'original_name' => $request->original_name,
|
'original_name' => $request->original_name,
|
||||||
'mime_type' => $file->getMimeType(),
|
'mime_type' => $file->getMimeType(),
|
||||||
'size' => $file->getSize(),
|
'size' => $file->getSize(),
|
||||||
|
|||||||
34
app/Support/UploadStream.php
Normal file
34
app/Support/UploadStream.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class UploadStream
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Store an uploaded file without loading it fully into memory.
|
||||||
|
*/
|
||||||
|
public static function putUploadedFile(string $disk, string $path, UploadedFile $file): bool
|
||||||
|
{
|
||||||
|
$sourcePath = $file->getRealPath() ?: $file->getPathname();
|
||||||
|
if (! $sourcePath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = fopen($sourcePath, 'rb');
|
||||||
|
|
||||||
|
if (! $stream) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Storage::disk($disk)->put($path, $stream);
|
||||||
|
} finally {
|
||||||
|
if (is_resource($stream)) {
|
||||||
|
fclose($stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
tests/Feature/Api/Tenant/TenantPhotoUploadTest.php
Normal file
81
tests/Feature/Api/Tenant/TenantPhotoUploadTest.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Api\Tenant;
|
||||||
|
|
||||||
|
use App\Jobs\ProcessPhotoSecurityScan;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Models\EventMediaAsset;
|
||||||
|
use App\Models\EventPackage;
|
||||||
|
use App\Models\MediaStorageTarget;
|
||||||
|
use App\Models\Package;
|
||||||
|
use App\Models\Photo;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Tests\Feature\Tenant\TenantTestCase;
|
||||||
|
|
||||||
|
class TenantPhotoUploadTest extends TenantTestCase
|
||||||
|
{
|
||||||
|
public function test_tenant_can_upload_photo_and_store_media_assets(): void
|
||||||
|
{
|
||||||
|
config(['filesystems.default' => 'public']);
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
|
MediaStorageTarget::create([
|
||||||
|
'key' => 'public',
|
||||||
|
'name' => 'Public (test)',
|
||||||
|
'driver' => 'local',
|
||||||
|
'config' => [
|
||||||
|
'root' => Storage::disk('public')->path(''),
|
||||||
|
'url' => 'http://localhost/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
],
|
||||||
|
'is_hot' => true,
|
||||||
|
'is_default' => true,
|
||||||
|
'is_active' => true,
|
||||||
|
'priority' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$package = Package::factory()->endcustomer()->create([
|
||||||
|
'max_photos' => 100,
|
||||||
|
'gallery_days' => 14,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$event = Event::factory()->create([
|
||||||
|
'tenant_id' => $this->tenant->id,
|
||||||
|
'photo_upload_enabled' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EventPackage::create([
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
'purchased_price' => $package->price,
|
||||||
|
'purchased_at' => now(),
|
||||||
|
'used_photos' => 0,
|
||||||
|
'gallery_expires_at' => now()->addDays(14),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Bus::fake();
|
||||||
|
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
])->postJson("/api/v1/tenant/events/{$event->slug}/photos", [
|
||||||
|
'photo' => UploadedFile::fake()->image('photo.jpg', 800, 600),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated()
|
||||||
|
->assertJsonPath('data.status', 'approved');
|
||||||
|
|
||||||
|
$photo = Photo::query()->first();
|
||||||
|
$this->assertNotNull($photo);
|
||||||
|
Storage::disk('public')->assertExists($photo->file_path);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas(EventMediaAsset::class, [
|
||||||
|
'photo_id' => $photo->id,
|
||||||
|
'variant' => 'original',
|
||||||
|
'disk' => 'public',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Bus::assertDispatched(ProcessPhotoSecurityScan::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user