optimizations
This commit is contained in:
368
tests/Feature/NovaCards/NovaCardV2ApiTest.php
Normal file
368
tests/Feature/NovaCards/NovaCardV2ApiTest.php
Normal file
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\NovaCardCategory;
|
||||
use App\Models\NovaCardChallenge;
|
||||
use App\Models\NovaCardComment;
|
||||
use App\Models\NovaCardCollection;
|
||||
use App\Models\NovaCardTemplate;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
function v2Category(array $attributes = []): NovaCardCategory
|
||||
{
|
||||
return NovaCardCategory::query()->create(array_merge([
|
||||
'slug' => 'v2-category-' . Str::lower(Str::random(6)),
|
||||
'name' => 'V2 Category',
|
||||
'description' => 'Nova Cards v2 category',
|
||||
'active' => true,
|
||||
'order_num' => 0,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function v2Template(array $attributes = []): NovaCardTemplate
|
||||
{
|
||||
return NovaCardTemplate::query()->create(array_merge([
|
||||
'slug' => 'v2-template-' . Str::lower(Str::random(6)),
|
||||
'name' => 'V2 Template',
|
||||
'description' => 'Nova Cards v2 template',
|
||||
'config_json' => [
|
||||
'font_preset' => 'modern-sans',
|
||||
'gradient_preset' => 'midnight-nova',
|
||||
'layout' => 'quote_heavy',
|
||||
'text_align' => 'center',
|
||||
'text_color' => '#ffffff',
|
||||
'overlay_style' => 'dark-soft',
|
||||
],
|
||||
'supported_formats' => ['square', 'portrait'],
|
||||
'active' => true,
|
||||
'official' => true,
|
||||
'order_num' => 0,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function v2PublishedCard(User $user, array $attributes = []): NovaCard
|
||||
{
|
||||
$category = $attributes['category'] ?? v2Category();
|
||||
$template = $attributes['template'] ?? v2Template();
|
||||
|
||||
return NovaCard::query()->create(array_merge([
|
||||
'user_id' => $user->id,
|
||||
'category_id' => $category->id,
|
||||
'template_id' => $template->id,
|
||||
'title' => 'V2 Public Card',
|
||||
'slug' => 'v2-public-card-' . Str::lower(Str::random(5)),
|
||||
'quote_text' => 'A card built for Nova Cards v2 tests.',
|
||||
'quote_author' => 'Nova V2',
|
||||
'quote_source' => 'Feature suite',
|
||||
'description' => 'Published Nova Cards v2 test card.',
|
||||
'format' => NovaCard::FORMAT_SQUARE,
|
||||
'project_json' => [
|
||||
'schema_version' => 2,
|
||||
'content' => [
|
||||
'title' => 'V2 Public Card',
|
||||
'quote_text' => 'A card built for Nova Cards v2 tests.',
|
||||
'quote_author' => 'Nova V2',
|
||||
'quote_source' => 'Feature suite',
|
||||
],
|
||||
'text_blocks' => [
|
||||
['key' => 'title', 'type' => 'title', 'text' => 'V2 Public Card', 'enabled' => true],
|
||||
['key' => 'quote', 'type' => 'quote', 'text' => 'A card built for Nova Cards v2 tests.', 'enabled' => true],
|
||||
['key' => 'author', 'type' => 'author', 'text' => 'Nova V2', 'enabled' => true],
|
||||
],
|
||||
'layout' => ['layout' => 'quote_heavy', 'position' => 'center', 'alignment' => 'center', 'padding' => 'comfortable', 'max_width' => 'balanced'],
|
||||
'typography' => ['font_preset' => 'modern-sans', 'text_color' => '#ffffff', 'accent_color' => '#e0f2fe', 'quote_size' => 72, 'author_size' => 28, 'letter_spacing' => 0, 'line_height' => 1.2, 'shadow_preset' => 'soft'],
|
||||
'background' => ['type' => 'gradient', 'gradient_preset' => 'midnight-nova', 'gradient_colors' => ['#0f172a', '#1d4ed8'], 'overlay_style' => 'dark-soft', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 50],
|
||||
'decorations' => [],
|
||||
'assets' => ['pack_ids' => [], 'template_pack_ids' => [], 'items' => []],
|
||||
],
|
||||
'schema_version' => 2,
|
||||
'render_version' => 2,
|
||||
'background_type' => 'gradient',
|
||||
'visibility' => NovaCard::VISIBILITY_PUBLIC,
|
||||
'status' => NovaCard::STATUS_PUBLISHED,
|
||||
'moderation_status' => NovaCard::MOD_APPROVED,
|
||||
'allow_download' => true,
|
||||
'allow_remix' => true,
|
||||
'published_at' => now()->subHour(),
|
||||
], Arr::except($attributes, ['category', 'template'])));
|
||||
}
|
||||
|
||||
it('supports likes favorites saves and remix lineage for published cards', function (): void {
|
||||
$creator = User::factory()->create();
|
||||
$viewer = User::factory()->create();
|
||||
$card = v2PublishedCard($creator, ['slug' => 'remix-source-card']);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->postJson(route('api.cards.like', ['id' => $card->id]))
|
||||
->assertOk()
|
||||
->assertJsonPath('liked', true);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->postJson(route('api.cards.favorite', ['id' => $card->id]))
|
||||
->assertOk()
|
||||
->assertJsonPath('favorited', true);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->postJson(route('api.cards.save', ['id' => $card->id]))
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$remix = $this->actingAs($viewer)
|
||||
->postJson(route('api.cards.remix', ['id' => $card->id]))
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.lineage.original_card_id', $card->id);
|
||||
|
||||
$remixedCard = NovaCard::query()->findOrFail((int) $remix->json('data.id'));
|
||||
|
||||
expect($card->fresh()->likes_count)->toBe(1)
|
||||
->and($card->fresh()->favorites_count)->toBe(1)
|
||||
->and($card->fresh()->saves_count)->toBe(1)
|
||||
->and($card->fresh()->remixes_count)->toBe(1)
|
||||
->and($remixedCard->original_card_id)->toBe($card->id)
|
||||
->and(NovaCardCollection::query()->where('user_id', $viewer->id)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns lineage data for a published card via the api', function (): void {
|
||||
$creator = User::factory()->create();
|
||||
$root = v2PublishedCard($creator, ['slug' => 'api-lineage-root', 'title' => 'API Lineage Root']);
|
||||
$remix = v2PublishedCard($creator, [
|
||||
'slug' => 'api-lineage-remix',
|
||||
'title' => 'API Lineage Remix',
|
||||
'original_card_id' => $root->id,
|
||||
'root_card_id' => $root->id,
|
||||
]);
|
||||
|
||||
$this->getJson(route('api.cards.lineage', ['id' => $remix->id]))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.root_card.id', $root->id)
|
||||
->assertJsonPath('data.trail.0.id', $root->id)
|
||||
->assertJsonPath('data.card.id', $remix->id);
|
||||
});
|
||||
|
||||
it('supports the literal spec-style api aliases for v2 card flows', function (): void {
|
||||
$creator = User::factory()->create();
|
||||
$viewer = User::factory()->create();
|
||||
$card = v2PublishedCard($creator, ['slug' => 'alias-source-card', 'title' => 'Alias Source']);
|
||||
$challenge = NovaCardChallenge::query()->create([
|
||||
'slug' => 'alias-challenge',
|
||||
'title' => 'Alias Challenge',
|
||||
'status' => NovaCardChallenge::STATUS_ACTIVE,
|
||||
'official' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->postJson('/api/cards/' . $card->id . '/like')
|
||||
->assertOk()
|
||||
->assertJsonPath('liked', true);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->postJson('/api/cards/' . $card->id . '/save')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->postJson('/api/cards/' . $card->id . '/remix')
|
||||
->assertCreated();
|
||||
|
||||
$owned = v2PublishedCard($viewer, ['slug' => 'alias-owned-card', 'title' => 'Alias Owned']);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->postJson('/api/cards/challenges/' . $challenge->id . '/submit', ['card_id' => $owned->id])
|
||||
->assertOk()
|
||||
->assertJsonPath('entry.challenge_id', $challenge->id);
|
||||
|
||||
$this->getJson('/api/cards/' . $card->id . '/lineage')
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
it('supports collection metadata and item management via the v2 api collection endpoints', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$card = v2PublishedCard(User::factory()->create(), ['slug' => 'collection-endpoint-card']);
|
||||
|
||||
$create = $this->actingAs($user)
|
||||
->postJson(route('api.cards.collections.store'), [
|
||||
'name' => 'My Picks',
|
||||
'visibility' => 'public',
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
$collectionId = (int) $create->json('collection.id');
|
||||
|
||||
$this->actingAs($user)
|
||||
->patchJson('/api/cards/collections/' . $collectionId, [
|
||||
'name' => 'Updated Picks',
|
||||
'slug' => 'updated-picks',
|
||||
'visibility' => 'public',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('collection.slug', 'updated-picks');
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson('/api/cards/collections/' . $collectionId . '/items', [
|
||||
'card_id' => $card->id,
|
||||
'note' => 'Pinned',
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('collection.items.0.card.id', $card->id);
|
||||
|
||||
$this->actingAs($user)
|
||||
->deleteJson('/api/cards/collections/' . $collectionId . '/items/' . $card->id)
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
it('lists challenge, asset, and template resources through the v2 api', function (): void {
|
||||
$user = User::factory()->create();
|
||||
NovaCardChallenge::query()->create([
|
||||
'slug' => 'resource-check',
|
||||
'title' => 'Resource Check',
|
||||
'status' => NovaCardChallenge::STATUS_ACTIVE,
|
||||
'official' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->getJson('/api/cards/challenges')->assertOk();
|
||||
$this->actingAs($user)->getJson('/api/cards/assets')->assertOk();
|
||||
$this->actingAs($user)->getJson('/api/cards/templates')->assertOk();
|
||||
});
|
||||
|
||||
it('recomputes comments_count when card comments change', function (): void {
|
||||
$creator = User::factory()->create();
|
||||
$commenter = User::factory()->create();
|
||||
$card = v2PublishedCard($creator, ['slug' => 'comment-counter-card']);
|
||||
|
||||
NovaCardComment::query()->create([
|
||||
'card_id' => $card->id,
|
||||
'user_id' => $commenter->id,
|
||||
'body' => 'First comment',
|
||||
'rendered_body' => 'First comment',
|
||||
'status' => 'visible',
|
||||
]);
|
||||
|
||||
app(\App\Services\NovaCards\NovaCardTrendingService::class)->refreshCard($card->fresh());
|
||||
|
||||
expect($card->fresh()->comments_count)->toBe(1);
|
||||
});
|
||||
|
||||
it('upgrades legacy v1 project json into the v2 editor shape when editing', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$category = v2Category();
|
||||
$template = v2Template();
|
||||
$card = NovaCard::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'category_id' => $category->id,
|
||||
'template_id' => $template->id,
|
||||
'title' => 'Legacy Card',
|
||||
'slug' => 'legacy-card',
|
||||
'quote_text' => 'Legacy quote',
|
||||
'format' => NovaCard::FORMAT_SQUARE,
|
||||
'project_json' => [
|
||||
'schema_version' => 1,
|
||||
'content' => [
|
||||
'title' => 'Legacy Card',
|
||||
'quote_text' => 'Legacy quote',
|
||||
],
|
||||
'background' => [
|
||||
'type' => 'gradient',
|
||||
'gradient_preset' => 'midnight-nova',
|
||||
],
|
||||
],
|
||||
'schema_version' => 1,
|
||||
'background_type' => 'gradient',
|
||||
'visibility' => NovaCard::VISIBILITY_PRIVATE,
|
||||
'status' => NovaCard::STATUS_DRAFT,
|
||||
'moderation_status' => NovaCard::MOD_PENDING,
|
||||
'allow_download' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->patchJson(route('api.cards.drafts.update', ['id' => $card->id]), [
|
||||
'quote_author' => 'Legacy Author',
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
expect($response->json('data.schema_version'))->toBe(2)
|
||||
->and($response->json('data.project_json.text_blocks.0.type'))->toBe('title')
|
||||
->and(NovaCard::query()->findOrFail($card->id)->schema_version)->toBe(2);
|
||||
});
|
||||
|
||||
it('duplicates an owned card into a fresh draft without remix lineage', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$card = v2PublishedCard($user, ['slug' => 'duplicate-source-card', 'title' => 'Duplicate Source']);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->postJson(route('api.cards.duplicate', ['id' => $card->id]))
|
||||
->assertCreated();
|
||||
|
||||
$duplicate = NovaCard::query()->findOrFail((int) $response->json('data.id'));
|
||||
|
||||
expect($duplicate->user_id)->toBe($user->id)
|
||||
->and($duplicate->title)->toBe('Copy of Duplicate Source')
|
||||
->and($duplicate->status)->toBe(NovaCard::STATUS_DRAFT)
|
||||
->and($duplicate->original_card_id)->toBeNull()
|
||||
->and($duplicate->root_card_id)->toBeNull();
|
||||
});
|
||||
|
||||
it('restores an earlier draft version through the v2 versions api', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$category = v2Category();
|
||||
$template = v2Template();
|
||||
|
||||
$create = $this->actingAs($user)->postJson(route('api.cards.drafts.store'), [
|
||||
'title' => 'Versioned Card',
|
||||
'quote_text' => 'First version.',
|
||||
'category_id' => $category->id,
|
||||
'template_id' => $template->id,
|
||||
])->assertCreated();
|
||||
|
||||
$cardId = (int) $create->json('data.id');
|
||||
|
||||
$this->actingAs($user)->postJson(route('api.cards.drafts.autosave', ['id' => $cardId]), [
|
||||
'quote_text' => 'Second version.',
|
||||
])->assertOk();
|
||||
|
||||
$versions = $this->actingAs($user)
|
||||
->getJson(route('api.cards.drafts.versions', ['id' => $cardId]))
|
||||
->assertOk();
|
||||
|
||||
$firstVersionId = (int) collect($versions->json('data'))->last()['id'];
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson(route('api.cards.drafts.restore', ['id' => $cardId, 'versionId' => $firstVersionId]))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.quote_text', 'First version.');
|
||||
|
||||
expect(NovaCard::query()->findOrFail($cardId)->quote_text)->toBe('First version.');
|
||||
});
|
||||
|
||||
it('submits a published card to a challenge and renders the new public v2 routes', function (): void {
|
||||
$user = User::factory()->create(['username' => 'v2creator']);
|
||||
$card = v2PublishedCard($user, ['slug' => 'challenge-entry-card']);
|
||||
$challenge = NovaCardChallenge::query()->create([
|
||||
'slug' => 'weekly-clarity',
|
||||
'title' => 'Weekly Clarity',
|
||||
'description' => 'Make a clean editorial card about clarity.',
|
||||
'prompt' => 'Design a card that turns one strong sentence into an editorial object.',
|
||||
'status' => NovaCardChallenge::STATUS_ACTIVE,
|
||||
'official' => true,
|
||||
'featured' => true,
|
||||
'starts_at' => now()->subDay(),
|
||||
'ends_at' => now()->addWeek(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson(route('api.cards.challenges.submit', ['challengeId' => $challenge->id, 'id' => $card->id]))
|
||||
->assertOk()
|
||||
->assertJsonPath('entry.challenge_id', $challenge->id);
|
||||
|
||||
expect($card->fresh()->challenge_entries_count)->toBe(1);
|
||||
|
||||
$this->get(route('cards.popular'))->assertOk();
|
||||
$this->get(route('cards.remixed'))->assertOk();
|
||||
$this->get(route('cards.challenges'))->assertOk()->assertSee('Weekly Clarity');
|
||||
$this->get(route('cards.challenges.show', ['slug' => $challenge->slug]))->assertOk()->assertSee($card->title);
|
||||
$this->get(route('cards.templates'))->assertOk();
|
||||
$this->get(route('cards.assets'))->assertOk();
|
||||
});
|
||||
Reference in New Issue
Block a user