Save workspace changes
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ContentType;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('view public artwork by slug', function () {
|
||||
$art = Artwork::factory()->create();
|
||||
|
||||
$this->getJson('/api/v1/artworks/' . $art->slug)
|
||||
->assertStatus(200)
|
||||
->assertJsonPath('slug', $art->slug)
|
||||
->assertJsonStructure(['slug', 'title', 'description', 'file', 'published_at']);
|
||||
});
|
||||
|
||||
test('cannot view unapproved artwork', function () {
|
||||
$art = Artwork::factory()->unapproved()->create();
|
||||
|
||||
$this->getJson('/api/v1/artworks/' . $art->slug)
|
||||
->assertStatus(404);
|
||||
});
|
||||
|
||||
test('soft-deleted artwork returns 404', function () {
|
||||
$art = Artwork::factory()->create();
|
||||
$art->delete();
|
||||
|
||||
$this->getJson('/api/v1/artworks/' . $art->slug)
|
||||
->assertStatus(404);
|
||||
});
|
||||
|
||||
test('category browsing returns artworks for the category only', function () {
|
||||
$contentType = ContentType::create(['name' => 'Photography', 'slug' => 'photography', 'description' => '']);
|
||||
$category = Category::create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'parent_id' => null,
|
||||
'name' => 'Abstract',
|
||||
'slug' => 'abstract',
|
||||
'description' => '',
|
||||
'is_active' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$inCat = Artwork::factory()->create();
|
||||
$outCat = Artwork::factory()->create();
|
||||
|
||||
$inCat->categories()->attach($category->id);
|
||||
|
||||
$this->getJson('/api/v1/categories/' . $category->slug . '/artworks')
|
||||
->assertStatus(200)
|
||||
->assertJsonStructure(['data', 'links', 'meta'])
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.slug', $inCat->slug);
|
||||
});
|
||||
|
||||
test('unauthorized or private access is blocked (private artwork)', function () {
|
||||
$art = Artwork::factory()->private()->create();
|
||||
|
||||
$this->getJson('/api/v1/artworks/' . $art->slug)
|
||||
->assertStatus(404);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use function Pest\Laravel\actingAs;
|
||||
use function Pest\Laravel\postJson;
|
||||
|
||||
it('creates upload drafts as private artworks', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
actingAs($user);
|
||||
|
||||
$response = postJson('/api/artworks', [
|
||||
'title' => 'Upload draft test',
|
||||
'description' => '<p>Draft body</p>',
|
||||
'is_mature' => false,
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('status', 'draft');
|
||||
|
||||
$artworkId = (int) $response->json('artwork_id');
|
||||
$artwork = Artwork::query()->findOrFail($artworkId);
|
||||
|
||||
expect($artwork->visibility)->toBe(Artwork::VISIBILITY_PRIVATE)
|
||||
->and($artwork->is_public)->toBeFalse()
|
||||
->and($artwork->artwork_status)->toBe('draft')
|
||||
->and($artwork->published_at)->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ReactionType;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
// ── Comment CRUD ──────────────────────────────────────────────────────────────
|
||||
|
||||
test('authenticated user can post a comment', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/artworks/{$artwork->id}/comments", [
|
||||
'content' => 'Great work! Really love the **colours**.',
|
||||
])
|
||||
->assertStatus(201)
|
||||
->assertJsonPath('data.user.id', $user->id)
|
||||
->assertJsonStructure(['data' => ['id', 'raw_content', 'rendered_content', 'user']]);
|
||||
});
|
||||
|
||||
test('guest cannot post a comment', function () {
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$this->postJson("/api/artworks/{$artwork->id}/comments", ['content' => 'Nice!'])
|
||||
->assertStatus(401);
|
||||
});
|
||||
|
||||
test('comment with raw HTML is rejected via validation', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/artworks/{$artwork->id}/comments", [
|
||||
'content' => '<script>alert("xss")</script>',
|
||||
])
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
test('user can view comments on public artwork', function () {
|
||||
$artwork = Artwork::factory()->create();
|
||||
$comment = ArtworkComment::factory()->create(['artwork_id' => $artwork->id]);
|
||||
|
||||
$this->getJson("/api/artworks/{$artwork->id}/comments")
|
||||
->assertStatus(200)
|
||||
->assertJsonStructure(['data', 'meta'])
|
||||
->assertJsonCount(1, 'data');
|
||||
});
|
||||
|
||||
// ── Reactions ─────────────────────────────────────────────────────────────────
|
||||
|
||||
test('authenticated user can add an artwork reaction', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/artworks/{$artwork->id}/reactions", [
|
||||
'reaction' => ReactionType::Heart->value,
|
||||
])
|
||||
->assertStatus(200)
|
||||
->assertJsonPath('reaction', ReactionType::Heart->value)
|
||||
->assertJsonPath('active', true);
|
||||
});
|
||||
|
||||
test('reaction is toggled off when posted twice', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
// First toggle — on
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/artworks/{$artwork->id}/reactions", [
|
||||
'reaction' => ReactionType::ThumbsUp->value,
|
||||
])
|
||||
->assertJsonPath('active', true);
|
||||
|
||||
// Second toggle — off
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/artworks/{$artwork->id}/reactions", [
|
||||
'reaction' => ReactionType::ThumbsUp->value,
|
||||
])
|
||||
->assertJsonPath('active', false);
|
||||
});
|
||||
|
||||
test('guest cannot add a reaction', function () {
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$this->postJson("/api/artworks/{$artwork->id}/reactions", [
|
||||
'reaction' => ReactionType::Fire->value,
|
||||
])->assertStatus(401);
|
||||
});
|
||||
|
||||
test('invalid reaction slug is rejected', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/artworks/{$artwork->id}/reactions", [
|
||||
'reaction' => 'not_valid_slug',
|
||||
])
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
test('reaction totals are returned for public artworks', function () {
|
||||
$artwork = Artwork::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
// Insert a reaction directly
|
||||
\Illuminate\Support\Facades\DB::table('artwork_reactions')->insert([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $user->id,
|
||||
'reaction' => ReactionType::Clap->value,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$this->getJson("/api/artworks/{$artwork->id}/reactions")
|
||||
->assertStatus(200)
|
||||
->assertJsonPath('totals.' . ReactionType::Clap->value . '.count', 1)
|
||||
->assertJsonPath('totals.' . ReactionType::Clap->value . '.emoji', '👏');
|
||||
});
|
||||
|
||||
test('user can react to a comment', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
$comment = ArtworkComment::factory()->create(['artwork_id' => $artwork->id]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/comments/{$comment->id}/reactions", [
|
||||
'reaction' => ReactionType::Laugh->value,
|
||||
])
|
||||
->assertStatus(200)
|
||||
->assertJsonPath('active', true)
|
||||
->assertJsonPath('entity_type', 'comment');
|
||||
});
|
||||
|
||||
test('reaction uniqueness per user per slug', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$slug = ReactionType::Wow->value;
|
||||
|
||||
// Toggle on
|
||||
$this->actingAs($user)->postJson("/api/artworks/{$artwork->id}/reactions", ['reaction' => $slug]);
|
||||
|
||||
// DB should have exactly 1 row
|
||||
$this->assertDatabaseCount('artwork_reactions', 1);
|
||||
|
||||
// Toggle off
|
||||
$this->actingAs($user)->postJson("/api/artworks/{$artwork->id}/reactions", ['reaction' => $slug]);
|
||||
|
||||
// DB should have 0 rows
|
||||
$this->assertDatabaseCount('artwork_reactions', 0);
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\Story;
|
||||
use App\Models\User;
|
||||
|
||||
test('authenticated user can like artwork through generic social endpoint and owner is notified', function () {
|
||||
$owner = User::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create(['user_id' => $owner->id]);
|
||||
|
||||
$this->actingAs($actor)
|
||||
->postJson('/api/like', [
|
||||
'entity_type' => 'artwork',
|
||||
'entity_id' => $artwork->id,
|
||||
'state' => true,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('is_liked', true)
|
||||
->assertJsonPath('stats.likes', 1);
|
||||
|
||||
$this->assertDatabaseHas('artwork_likes', [
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $actor->id,
|
||||
]);
|
||||
|
||||
$notification = $owner->fresh()
|
||||
->notifications()
|
||||
->where('type', 'artwork_liked')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
expect($notification)->not->toBeNull();
|
||||
expect($notification->data['type'] ?? null)->toBe('artwork_liked');
|
||||
expect($notification->data['actor_id'] ?? null)->toBe($actor->id);
|
||||
});
|
||||
|
||||
test('authenticated user can comment on artwork through generic social endpoint and send owner and mention notifications', function () {
|
||||
$owner = User::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$mentioned = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create(['user_id' => $owner->id]);
|
||||
|
||||
$this->actingAs($actor)
|
||||
->postJson('/api/comments', [
|
||||
'entity_type' => 'artwork',
|
||||
'entity_id' => $artwork->id,
|
||||
'content' => 'Great work @' . $mentioned->username,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.user.id', $actor->id);
|
||||
|
||||
$comment = ArtworkComment::query()->latest('id')->first();
|
||||
|
||||
expect($comment)->not->toBeNull();
|
||||
expect($comment->artwork_id)->toBe($artwork->id);
|
||||
|
||||
$ownerNotification = $owner->fresh()
|
||||
->notifications()
|
||||
->where('type', 'artwork_commented')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
$mentionedNotification = $mentioned->fresh()
|
||||
->notifications()
|
||||
->where('type', 'artwork_mentioned')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
expect($ownerNotification)->not->toBeNull();
|
||||
expect($ownerNotification->data['type'] ?? null)->toBe('artwork_commented');
|
||||
expect($mentionedNotification)->not->toBeNull();
|
||||
expect($mentionedNotification->data['type'] ?? null)->toBe('artwork_mentioned');
|
||||
|
||||
$this->assertDatabaseHas('user_mentions', [
|
||||
'comment_id' => $comment->id,
|
||||
'mentioned_user_id' => $mentioned->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('generic comments endpoint lists artwork comments', function () {
|
||||
$artwork = Artwork::factory()->create();
|
||||
$comment = ArtworkComment::factory()->create(['artwork_id' => $artwork->id]);
|
||||
|
||||
$this->getJson('/api/comments?entity_type=artwork&entity_id=' . $artwork->id)
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', $comment->id);
|
||||
});
|
||||
|
||||
test('authenticated user can bookmark artwork through generic endpoint and see it in bookmarks list', function () {
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson('/api/bookmark', [
|
||||
'entity_type' => 'artwork',
|
||||
'entity_id' => $artwork->id,
|
||||
'state' => true,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('is_bookmarked', true)
|
||||
->assertJsonPath('stats.bookmarks', 1);
|
||||
|
||||
$this->assertDatabaseHas('artwork_bookmarks', [
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->getJson('/api/bookmarks?entity_type=artwork')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.type', 'artwork')
|
||||
->assertJsonPath('data.0.id', $artwork->id);
|
||||
});
|
||||
|
||||
test('authenticated user can like and bookmark a story through generic social endpoints', function () {
|
||||
$owner = User::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$story = Story::query()->create([
|
||||
'creator_id' => $owner->id,
|
||||
'title' => 'Published Story',
|
||||
'slug' => 'published-story-' . strtolower((string) \Illuminate\Support\Str::random(6)),
|
||||
'content' => '<p>Story body</p>',
|
||||
'story_type' => 'creator_story',
|
||||
'status' => 'published',
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->actingAs($actor)
|
||||
->postJson('/api/like', [
|
||||
'entity_type' => 'story',
|
||||
'entity_id' => $story->id,
|
||||
'state' => true,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('is_liked', true)
|
||||
->assertJsonPath('stats.likes', 1);
|
||||
|
||||
$this->assertDatabaseHas('story_likes', [
|
||||
'story_id' => $story->id,
|
||||
'user_id' => $actor->id,
|
||||
]);
|
||||
|
||||
$likeNotification = $owner->fresh()
|
||||
->notifications()
|
||||
->where('type', 'story_liked')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
expect($likeNotification)->not->toBeNull();
|
||||
expect($likeNotification->data['type'] ?? null)->toBe('story_liked');
|
||||
|
||||
$this->actingAs($actor)
|
||||
->postJson('/api/bookmark', [
|
||||
'entity_type' => 'story',
|
||||
'entity_id' => $story->id,
|
||||
'state' => true,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('is_bookmarked', true)
|
||||
->assertJsonPath('stats.bookmarks', 1);
|
||||
|
||||
$this->assertDatabaseHas('story_bookmarks', [
|
||||
'story_id' => $story->id,
|
||||
'user_id' => $actor->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($actor)
|
||||
->getJson('/api/bookmarks?entity_type=story')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.type', 'story')
|
||||
->assertJsonPath('data.0.id', $story->id);
|
||||
});
|
||||
|
||||
test('authenticated user can comment on a story through generic social endpoint and send owner and mention notifications', function () {
|
||||
$owner = User::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$mentioned = User::factory()->create();
|
||||
$story = Story::query()->create([
|
||||
'creator_id' => $owner->id,
|
||||
'title' => 'Commentable Story',
|
||||
'slug' => 'commentable-story-' . strtolower((string) \Illuminate\Support\Str::random(6)),
|
||||
'content' => '<p>Story body</p>',
|
||||
'story_type' => 'creator_story',
|
||||
'status' => 'published',
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->actingAs($actor)
|
||||
->postJson('/api/comments', [
|
||||
'entity_type' => 'story',
|
||||
'entity_id' => $story->id,
|
||||
'content' => 'Great story @' . $mentioned->username,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.user.id', $actor->id);
|
||||
|
||||
$this->assertDatabaseHas('story_comments', [
|
||||
'story_id' => $story->id,
|
||||
'user_id' => $actor->id,
|
||||
]);
|
||||
|
||||
$ownerNotification = $owner->fresh()
|
||||
->notifications()
|
||||
->where('type', 'story_commented')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
$mentionedNotification = $mentioned->fresh()
|
||||
->notifications()
|
||||
->where('type', 'story_mentioned')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
expect($ownerNotification)->not->toBeNull();
|
||||
expect($ownerNotification->data['type'] ?? null)->toBe('story_commented');
|
||||
expect($mentionedNotification)->not->toBeNull();
|
||||
expect($mentionedNotification->data['type'] ?? null)->toBe('story_mentioned');
|
||||
|
||||
$this->actingAs($actor)
|
||||
->getJson('/api/comments?entity_type=story&entity_id=' . $story->id)
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.user.id', $actor->id);
|
||||
});
|
||||
Reference in New Issue
Block a user