Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use App\Services\Cdn\ArtworkCdnPurgeService;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class);
it('purges canonical artwork urls through the Cloudflare api', function (): void {
Http::fake([
'https://api.cloudflare.com/client/v4/zones/test-zone/purge_cache' => Http::response(['success' => true], 200),
]);
config()->set('cdn.files_url', 'https://cdn.skinbase.org');
config()->set('cdn.cloudflare.zone_id', 'test-zone');
config()->set('cdn.cloudflare.api_token', 'test-token');
$result = app(ArtworkCdnPurgeService::class)->purgeArtworkObjectPaths([
'artworks/sq/61/83/6183c98975512ee6bff4657043067953a33769c7.webp',
], ['reason' => 'test']);
expect($result)->toBeTrue();
Http::assertSent(function ($request): bool {
return $request->url() === 'https://api.cloudflare.com/client/v4/zones/test-zone/purge_cache'
&& $request->hasHeader('Authorization', 'Bearer test-token')
&& $request['files'] === [
'https://cdn.skinbase.org/artworks/sq/61/83/6183c98975512ee6bff4657043067953a33769c7.webp',
];
});
});
it('falls back to the legacy purge webhook when Cloudflare credentials are absent', function (): void {
Http::fake([
'https://purge.internal.example/purge' => Http::response(['ok' => true], 200),
]);
config()->set('cdn.files_url', 'https://cdn.skinbase.org');
config()->set('cdn.cloudflare.zone_id', null);
config()->set('cdn.cloudflare.api_token', null);
config()->set('cdn.purge_url', 'https://purge.internal.example/purge');
$result = app(ArtworkCdnPurgeService::class)->purgeArtworkObjectPaths([
'artworks/sq/61/83/6183c98975512ee6bff4657043067953a33769c7.webp',
], ['reason' => 'test']);
expect($result)->toBeTrue();
Http::assertSent(function ($request): bool {
return $request->url() === 'https://purge.internal.example/purge'
&& $request['paths'] === [
'/artworks/sq/61/83/6183c98975512ee6bff4657043067953a33769c7.webp',
];
});
});

View File

@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\User;
use App\Services\CollectionHealthService;
use App\Services\CollectionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
it('flags public collections as stale when freshness falls to zero', function (): void {
$user = User::factory()->create();
$service = app(CollectionHealthService::class);
$collection = Collection::factory()->make([
'user_id' => $user->id,
'title' => 'Stale Health Candidate',
'slug' => 'stale-health-candidate',
'mode' => Collection::MODE_SMART,
'artworks_count' => 6,
'visibility' => Collection::VISIBILITY_PUBLIC,
'moderation_status' => Collection::MODERATION_ACTIVE,
'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
'summary' => 'A focused summary for stale health coverage.',
'description' => 'A longer description that gives the stale collection enough metadata depth for the health service.',
'likes_count' => 45,
'followers_count' => 20,
'saves_count' => 24,
'comments_count' => 6,
'shares_count' => 4,
'views_count' => 1000,
'last_activity_at' => now()->subDays(60),
'updated_at' => now()->subDays(60),
'published_at' => now()->subDays(60),
'workflow_state' => Collection::WORKFLOW_APPROVED,
]);
$collection->cover_artwork_id = 999;
$collection->setRelation('coverArtwork', Artwork::factory()->for($user)->make([
'width' => 1400,
'height' => 900,
'published_at' => now()->subDays(61),
]));
$flagsMethod = new ReflectionMethod(CollectionHealthService::class, 'flags');
$flagsMethod->setAccessible(true);
$flags = $flagsMethod->invoke($service, $collection, 80.0, 0.0, 80.0, 80.0);
expect($flags)->toContain(Collection::HEALTH_STALE);
});
it('flags collections with fewer than six artworks as low content', function (): void {
$user = User::factory()->create();
$service = app(CollectionHealthService::class);
$collection = seededHealthyCollection($user, 'Thin Collection Candidate', 5);
$payload = $service->evaluate($collection->fresh(['coverArtwork']));
expect($payload['health_flags_json'])->toContain(Collection::HEALTH_LOW_CONTENT);
});
it('flags broken items when too many attached artworks are not publicly visible', function (): void {
$user = User::factory()->create();
$service = app(CollectionHealthService::class);
$collection = Collection::factory()->for($user)->create([
'title' => 'Broken Items Candidate',
'slug' => 'broken-items-candidate',
'visibility' => Collection::VISIBILITY_PUBLIC,
'moderation_status' => Collection::MODERATION_ACTIVE,
'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
'summary' => 'A collection used to validate broken item detection.',
'description' => 'A detailed curatorial description for broken item health coverage.',
'likes_count' => 40,
'followers_count' => 20,
'saves_count' => 15,
'views_count' => 900,
'published_at' => now()->subDays(10),
'updated_at' => now()->subDay(),
'last_activity_at' => now()->subDay(),
]);
$visible = Artwork::factory()->for($user)->create(['width' => 1280, 'height' => 720]);
$private = Artwork::factory()->for($user)->private()->create();
$unapproved = Artwork::factory()->for($user)->unapproved()->create();
$unpublished = Artwork::factory()->for($user)->unpublished()->create();
app(CollectionService::class)->attachArtworks($collection, $user, [$visible->id, $private->id, $unapproved->id, $unpublished->id]);
$collection->forceFill([
'cover_artwork_id' => $visible->id,
'updated_at' => now()->subDay(),
'last_activity_at' => now()->subDay(),
])->save();
$payload = $service->evaluate($collection->fresh(['coverArtwork']));
expect($payload['health_flags_json'])->toContain(Collection::HEALTH_BROKEN_ITEMS)
->and($payload['placement_eligibility'])->toBeFalse();
});
it('flags collections without an explicit cover as weak cover', function (): void {
$user = User::factory()->create();
$service = app(CollectionHealthService::class);
$collection = seededHealthyCollection($user, 'Weak Cover Candidate');
$collection->forceFill(['cover_artwork_id' => null])->save();
$payload = $service->evaluate($collection->fresh(['coverArtwork']));
expect($payload['health_flags_json'])->toContain(Collection::HEALTH_WEAK_COVER);
});
it('keeps strong active collections healthy and placement eligible', function (): void {
$user = User::factory()->create();
$service = app(CollectionHealthService::class);
$collection = seededHealthyCollection($user, 'Healthy Cover Candidate');
$payload = $service->evaluate($collection->fresh(['coverArtwork']));
expect($payload['health_state'])->toBe(Collection::HEALTH_HEALTHY)
->and($payload['health_flags_json'])->toBe([])
->and($payload['placement_eligibility'])->toBeTrue();
});
function seededHealthyCollection(User $user, string $title, int $artworksCount = 6): Collection
{
$collection = Collection::factory()->for($user)->create([
'title' => $title,
'slug' => str($title)->slug()->value(),
'visibility' => Collection::VISIBILITY_PUBLIC,
'moderation_status' => Collection::MODERATION_ACTIVE,
'lifecycle_state' => Collection::LIFECYCLE_PUBLISHED,
'summary' => 'A focused summary for health scoring coverage.',
'description' => 'A longer description that provides enough metadata depth for health scoring and editorial readiness calculations.',
'likes_count' => 45,
'followers_count' => 18,
'saves_count' => 24,
'comments_count' => 8,
'shares_count' => 5,
'views_count' => 1400,
'published_at' => now()->subDays(5),
'updated_at' => now()->subDay(),
'last_activity_at' => now()->subDay(),
'workflow_state' => Collection::WORKFLOW_APPROVED,
]);
$artworkIds = Artwork::factory()->count($artworksCount)->for($user)->create([
'width' => 1400,
'height' => 900,
])->pluck('id')->all();
app(CollectionService::class)->attachArtworks($collection, $user, $artworkIds);
$collection->forceFill([
'cover_artwork_id' => $artworkIds[0] ?? null,
'updated_at' => now()->subDay(),
'last_activity_at' => now()->subDay(),
])->save();
return $collection->fresh(['coverArtwork']);
}