feat: ship creator journey v2 and profile updates
This commit is contained in:
60
tests/Unit/ArtworkServiceContentTypeResolutionTest.php
Normal file
60
tests/Unit/ArtworkServiceContentTypeResolutionTest.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ContentType;
|
||||
use App\Models\ContentTypeSlugHistory;
|
||||
use App\Services\ArtworkService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
it('resolves historical content type slugs in artwork service content type browsing', function () {
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Digital Illustration',
|
||||
'slug' => 'digital-illustration',
|
||||
'description' => 'Digital illustration uploads',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
ContentTypeSlugHistory::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'old_slug' => 'digital-art',
|
||||
]);
|
||||
|
||||
$paginator = app(ArtworkService::class)->getArtworksByContentType('digital-art', 24);
|
||||
|
||||
expect($paginator->count())->toBe(0)
|
||||
->and($paginator->items())->toBeArray();
|
||||
});
|
||||
|
||||
it('resolves historical content type slugs in artwork service category path browsing', function () {
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Digital Illustration',
|
||||
'slug' => 'digital-illustration',
|
||||
'description' => 'Digital illustration uploads',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
ContentTypeSlugHistory::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'old_slug' => 'digital-art',
|
||||
]);
|
||||
|
||||
$rootCategory = $contentType->categories()->create([
|
||||
'parent_id' => null,
|
||||
'name' => 'Concepts',
|
||||
'slug' => 'concepts',
|
||||
'description' => null,
|
||||
'image' => null,
|
||||
'is_active' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$paginator = app(ArtworkService::class)->getArtworksByCategoryPath([
|
||||
'digital-art',
|
||||
$rootCategory->slug,
|
||||
], 24);
|
||||
|
||||
expect($paginator->count())->toBe(0)
|
||||
->and($paginator->items())->toBeArray();
|
||||
});
|
||||
37
tests/Unit/CategoriesSitemapBuilderTest.php
Normal file
37
tests/Unit/CategoriesSitemapBuilderTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Services\Sitemaps\Builders\CategoriesSitemapBuilder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
uses(Tests\TestCase::class, RefreshDatabase::class);
|
||||
|
||||
it('builds category sitemap entries from dynamic content types', function () {
|
||||
$contentTypeId = DB::table('content_types')->insertGetId([
|
||||
'name' => 'Pixel Art',
|
||||
'slug' => 'pixel-art',
|
||||
'description' => 'Pixel art uploads',
|
||||
'order' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('categories')->insert([
|
||||
'content_type_id' => $contentTypeId,
|
||||
'parent_id' => null,
|
||||
'name' => 'Characters',
|
||||
'slug' => 'characters',
|
||||
'description' => null,
|
||||
'image' => null,
|
||||
'is_active' => true,
|
||||
'sort_order' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$items = app(CategoriesSitemapBuilder::class)->items();
|
||||
$locations = array_map(static fn ($item) => $item->loc, $items);
|
||||
|
||||
expect($locations)->toContain(url('/pixel-art'))
|
||||
->and($locations)->toContain(url('/pixel-art/characters'));
|
||||
});
|
||||
@@ -8,6 +8,7 @@ use App\Models\User;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\SmartCollectionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
@@ -74,6 +75,98 @@ it('keeps artworks_count in sync while attaching and removing artworks', functio
|
||||
expect($collection->artworks()->pluck('artworks.id')->all())->toBe([$artworkB->id]);
|
||||
});
|
||||
|
||||
it('sorts manual collection artworks by stats views for popular mode', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$collection = Collection::factory()->for($user)->create([
|
||||
'sort_mode' => Collection::SORT_POPULAR,
|
||||
]);
|
||||
$lowViews = Artwork::factory()->for($user)->create();
|
||||
$highViews = Artwork::factory()->for($user)->create();
|
||||
$noStats = Artwork::factory()->for($user)->create();
|
||||
$service = app(CollectionService::class);
|
||||
|
||||
$service->attachArtworks($collection, $user, [$lowViews->id, $noStats->id, $highViews->id]);
|
||||
|
||||
DB::table('artwork_stats')->insert([
|
||||
[
|
||||
'artwork_id' => $lowViews->id,
|
||||
'views' => 25,
|
||||
'downloads' => 0,
|
||||
'favorites' => 0,
|
||||
'rating_avg' => 0,
|
||||
'rating_count' => 0,
|
||||
],
|
||||
[
|
||||
'artwork_id' => $highViews->id,
|
||||
'views' => 250,
|
||||
'downloads' => 0,
|
||||
'favorites' => 0,
|
||||
'rating_avg' => 0,
|
||||
'rating_count' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$artworks = $service->getCollectionDetailArtworks($collection->fresh(), true, 24);
|
||||
|
||||
expect($artworks->getCollection()->pluck('id')->all())->toBe([
|
||||
$highViews->id,
|
||||
$lowViews->id,
|
||||
$noStats->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts smart collection artworks by stats views for popular mode', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$collection = Collection::factory()->for($user)->create([
|
||||
'mode' => Collection::MODE_SMART,
|
||||
'smart_rules_json' => [
|
||||
'match' => 'all',
|
||||
'sort' => Collection::SORT_POPULAR,
|
||||
'rules' => [
|
||||
[
|
||||
'field' => 'created_at',
|
||||
'operator' => 'between',
|
||||
'value' => [
|
||||
'from' => now()->subDay()->toDateString(),
|
||||
'to' => now()->addDay()->toDateString(),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$lowViews = Artwork::factory()->for($user)->create();
|
||||
$highViews = Artwork::factory()->for($user)->create();
|
||||
$noStats = Artwork::factory()->for($user)->create();
|
||||
$service = app(SmartCollectionService::class);
|
||||
|
||||
DB::table('artwork_stats')->insert([
|
||||
[
|
||||
'artwork_id' => $lowViews->id,
|
||||
'views' => 12,
|
||||
'downloads' => 0,
|
||||
'favorites' => 0,
|
||||
'rating_avg' => 0,
|
||||
'rating_count' => 0,
|
||||
],
|
||||
[
|
||||
'artwork_id' => $highViews->id,
|
||||
'views' => 120,
|
||||
'downloads' => 0,
|
||||
'favorites' => 0,
|
||||
'rating_avg' => 0,
|
||||
'rating_count' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$artworks = $service->resolveArtworks($collection, true, 24);
|
||||
|
||||
expect($artworks->getCollection()->pluck('id')->all())->toBe([
|
||||
$highViews->id,
|
||||
$lowViews->id,
|
||||
$noStats->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds a human readable smart summary for medium rules', function (): void {
|
||||
$service = app(SmartCollectionService::class);
|
||||
|
||||
|
||||
@@ -33,6 +33,13 @@ test('render returns empty string for whitespace-only input', function () {
|
||||
expect(ContentSanitizer::render(' '))->toBe('');
|
||||
});
|
||||
|
||||
test('render preserves emoji characters in output', function () {
|
||||
$html = ContentSanitizer::render('Love this piece 🚀🔥');
|
||||
|
||||
expect($html)->toContain('🚀')
|
||||
->and($html)->toContain('🔥');
|
||||
});
|
||||
|
||||
// ── XSS Prevention ────────────────────────────────────────────────────────────
|
||||
|
||||
test('render strips script tags', function () {
|
||||
|
||||
36
tests/Unit/ContentTypeAssetServiceTest.php
Normal file
36
tests/Unit/ContentTypeAssetServiceTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ContentType;
|
||||
use App\Services\ContentTypeAssetService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
it('stores and deletes content type assets on the configured object storage disk', function () {
|
||||
Storage::fake('s3');
|
||||
Config::set('uploads.object_storage.disk', 's3');
|
||||
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Digital Art',
|
||||
'slug' => 'digital-art',
|
||||
'description' => 'Digital art uploads',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$path = app(ContentTypeAssetService::class)->storeUploadedAsset(
|
||||
$contentType,
|
||||
UploadedFile::fake()->image('mascot.webp', 240, 320),
|
||||
'mascot'
|
||||
);
|
||||
|
||||
expect($path)->toStartWith('content-types/' . $contentType->id . '/mascot-');
|
||||
expect(Storage::disk('s3')->exists($path))->toBeTrue();
|
||||
|
||||
app(ContentTypeAssetService::class)->deleteIfManaged($path);
|
||||
|
||||
expect(Storage::disk('s3')->exists($path))->toBeFalse();
|
||||
});
|
||||
65
tests/Unit/ContentTypeSlugResolverTest.php
Normal file
65
tests/Unit/ContentTypeSlugResolverTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ContentType;
|
||||
use App\Models\ContentTypeSlugHistory;
|
||||
use App\Services\ContentTypes\ContentTypeSlugResolver;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
uses(Tests\TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Cache::flush();
|
||||
});
|
||||
|
||||
it('resolves current, historical, and virtual content type slugs', function () {
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Digital Art',
|
||||
'slug' => 'digital-art',
|
||||
'description' => 'Digital art uploads',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
ContentTypeSlugHistory::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'old_slug' => 'concept-art',
|
||||
]);
|
||||
|
||||
$resolver = app(ContentTypeSlugResolver::class);
|
||||
|
||||
$current = $resolver->resolve('digital-art');
|
||||
$historical = $resolver->resolve('concept-art');
|
||||
$virtual = $resolver->resolve('artworks', allowVirtual: true);
|
||||
|
||||
expect($current->found())->toBeTrue()
|
||||
->and($current->requiresRedirect())->toBeFalse()
|
||||
->and($current->contentType?->slug)->toBe('digital-art')
|
||||
->and($historical->found())->toBeTrue()
|
||||
->and($historical->requiresRedirect())->toBeTrue()
|
||||
->and($historical->redirectSlug)->toBe('digital-art')
|
||||
->and($historical->contentType?->id)->toBe($contentType->id)
|
||||
->and($virtual->found())->toBeTrue()
|
||||
->and($virtual->isVirtual)->toBeTrue()
|
||||
->and($virtual->virtualType)->toBe('artworks');
|
||||
});
|
||||
|
||||
it('reports reserved and historical slug conflicts', function () {
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Photography',
|
||||
'slug' => 'photography',
|
||||
'description' => 'Photography uploads',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
ContentTypeSlugHistory::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'old_slug' => 'photos',
|
||||
]);
|
||||
|
||||
$resolver = app(ContentTypeSlugResolver::class);
|
||||
|
||||
expect($resolver->isReservedSlug('help'))->toBeTrue()
|
||||
->and($resolver->isReservedSlug('photography'))->toBeFalse()
|
||||
->and($resolver->historicalSlugExists('photos'))->toBeTrue()
|
||||
->and($resolver->historicalSlugExists('photos', $contentType->id))->toBeFalse();
|
||||
});
|
||||
33
tests/Unit/Seo/SeoDataBuilderTest.php
Normal file
33
tests/Unit/Seo/SeoDataBuilderTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Support\Seo\SeoDataBuilder;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('normalizes a single associative structured data schema', function () {
|
||||
$seo = SeoDataBuilder::fromArray([
|
||||
'title' => 'Categories',
|
||||
'structured_data' => [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => 'Categories',
|
||||
],
|
||||
])->build()->toArray();
|
||||
|
||||
expect($seo['json_ld'] ?? [])
|
||||
->toHaveCount(1)
|
||||
->and($seo['json_ld'][0]['@type'] ?? null)->toBe('CollectionPage');
|
||||
});
|
||||
|
||||
it('normalizes JSON string structured data schemas', function () {
|
||||
$seo = SeoDataBuilder::fromArray([
|
||||
'title' => 'Categories',
|
||||
'structured_data' => '{"@context":"https://schema.org","@type":"CollectionPage","name":"Categories"}',
|
||||
])->build()->toArray();
|
||||
|
||||
expect($seo['json_ld'] ?? [])
|
||||
->toHaveCount(1)
|
||||
->and($seo['json_ld'][0]['@context'] ?? null)->toBe('https://schema.org')
|
||||
->and($seo['json_ld'][0]['@type'] ?? null)->toBe('CollectionPage');
|
||||
});
|
||||
Reference in New Issue
Block a user