update
This commit is contained in:
229
tests/Feature/Api/SocialCompatibilityEndpointsTest.php
Normal file
229
tests/Feature/Api/SocialCompatibilityEndpointsTest.php
Normal file
@@ -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);
|
||||
});
|
||||
@@ -46,7 +46,8 @@ it('allows complete onboarding user to access profile and upload', function () {
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/profile')
|
||||
->assertOk();
|
||||
->assertRedirect('/dashboard/profile')
|
||||
->assertStatus(301);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/upload')
|
||||
|
||||
28
tests/Feature/Countries/CountryMigrationTest.php
Normal file
28
tests/Feature/Countries/CountryMigrationTest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
it('creates the countries table and user country relation column', function (): void {
|
||||
expect(Schema::hasTable('countries'))->toBeTrue();
|
||||
expect(Schema::hasColumns('countries', [
|
||||
'id',
|
||||
'iso2',
|
||||
'iso3',
|
||||
'numeric_code',
|
||||
'name_common',
|
||||
'name_official',
|
||||
'region',
|
||||
'subregion',
|
||||
'flag_svg_url',
|
||||
'flag_png_url',
|
||||
'flag_emoji',
|
||||
'active',
|
||||
'sort_order',
|
||||
'is_featured',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]))->toBeTrue();
|
||||
expect(Schema::hasColumn('users', 'country_id'))->toBeTrue();
|
||||
});
|
||||
90
tests/Feature/Countries/CountrySyncServiceTest.php
Normal file
90
tests/Feature/Countries/CountrySyncServiceTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\User;
|
||||
use App\Services\Countries\CountryCatalogService;
|
||||
use App\Services\Countries\CountryRemoteProviderInterface;
|
||||
use App\Services\Countries\CountrySyncService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
it('syncs countries updates cache and backfills users from legacy country codes', function (): void {
|
||||
config()->set('skinbase-countries.deactivate_missing', true);
|
||||
|
||||
Country::query()->where('iso2', 'SI')->update([
|
||||
'iso' => 'SI',
|
||||
'iso3' => 'SVN',
|
||||
'name' => 'Old Slovenia',
|
||||
'name_common' => 'Old Slovenia',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
DB::table('user_profiles')->insert([
|
||||
'user_id' => $user->id,
|
||||
'country_code' => 'SI',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$catalog = app(CountryCatalogService::class);
|
||||
expect(collect($catalog->profileSelectOptions())->firstWhere('iso2', 'SI')['name'])->toBe('Old Slovenia');
|
||||
|
||||
app()->instance(CountryRemoteProviderInterface::class, new class implements CountryRemoteProviderInterface {
|
||||
public function fetchAll(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'iso2' => 'SI',
|
||||
'iso3' => 'SVN',
|
||||
'numeric_code' => '705',
|
||||
'name_common' => 'Slovenia',
|
||||
'name_official' => 'Republic of Slovenia',
|
||||
'region' => 'Europe',
|
||||
'subregion' => 'Central Europe',
|
||||
'flag_svg_url' => 'https://flags.test/si.svg',
|
||||
'flag_png_url' => 'https://flags.test/si.png',
|
||||
'flag_emoji' => '🇸🇮',
|
||||
],
|
||||
[
|
||||
'iso2' => '',
|
||||
'name_common' => 'Invalid',
|
||||
],
|
||||
[
|
||||
'iso2' => 'SI',
|
||||
'name_common' => 'Duplicate Slovenia',
|
||||
],
|
||||
[
|
||||
'iso2' => 'ZZ',
|
||||
'iso3' => 'ZZZ',
|
||||
'numeric_code' => '999',
|
||||
'name_common' => 'Zedland',
|
||||
'name_official' => 'Republic of Zedland',
|
||||
'region' => 'Europe',
|
||||
'subregion' => 'Nowhere',
|
||||
'flag_svg_url' => 'https://flags.test/zz.svg',
|
||||
'flag_png_url' => 'https://flags.test/zz.png',
|
||||
'flag_emoji' => '🏳️',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function normalizePayload(array $payload): array
|
||||
{
|
||||
return $payload;
|
||||
}
|
||||
});
|
||||
|
||||
$summary = app(CountrySyncService::class)->sync(allowFallback: false, deactivateMissing: true);
|
||||
|
||||
expect($summary['updated'])->toBe(1)
|
||||
->and($summary['inserted'])->toBe(1)
|
||||
->and($summary['invalid'])->toBe(1)
|
||||
->and($summary['skipped'])->toBe(1)
|
||||
->and($summary['backfilled_users'])->toBe(1);
|
||||
|
||||
expect(collect($catalog->profileSelectOptions())->firstWhere('iso2', 'SI')['name'])->toBe('Slovenia');
|
||||
expect($user->fresh()->country_id)->toBe(Country::query()->where('iso2', 'SI')->value('id'));
|
||||
});
|
||||
86
tests/Feature/Countries/ProfileCountryPersistenceTest.php
Normal file
86
tests/Feature/Countries/ProfileCountryPersistenceTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Http\Middleware\ForumBotProtectionMiddleware;
|
||||
use App\Models\Country;
|
||||
use App\Models\User;
|
||||
|
||||
it('stores a selected country on the personal settings endpoint', function (): void {
|
||||
$this->withoutMiddleware(ForumBotProtectionMiddleware::class);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$country = Country::query()->where('iso2', 'SI')->firstOrFail();
|
||||
$country->update([
|
||||
'iso' => 'SI',
|
||||
'iso3' => 'SVN',
|
||||
'name' => 'Slovenia',
|
||||
'name_common' => 'Slovenia',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/settings/personal/update', [
|
||||
'birthday' => '1990-01-02',
|
||||
'gender' => 'm',
|
||||
'country_id' => $country->id,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$user->refresh();
|
||||
|
||||
expect($user->country_id)->toBe($country->id);
|
||||
expect(optional($user->profile)->country_code)->toBe('SI');
|
||||
});
|
||||
|
||||
it('rejects invalid country identifiers on the personal settings endpoint', function (): void {
|
||||
$this->withoutMiddleware(ForumBotProtectionMiddleware::class);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/settings/personal/update', [
|
||||
'country_id' => 999999,
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['country_id']);
|
||||
});
|
||||
|
||||
it('loads countries on the dashboard profile settings page', function (): void {
|
||||
$this->withoutMiddleware(ForumBotProtectionMiddleware::class);
|
||||
|
||||
$user = User::factory()->create();
|
||||
Country::query()->where('iso2', 'SI')->update([
|
||||
'iso' => 'SI',
|
||||
'iso3' => 'SVN',
|
||||
'name' => 'Slovenia',
|
||||
'name_common' => 'Slovenia',
|
||||
'flag_emoji' => '🇸🇮',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/dashboard/profile');
|
||||
|
||||
$response->assertOk()->assertSee('Slovenia');
|
||||
});
|
||||
|
||||
it('supports country persistence through the legacy profile update endpoint', function (): void {
|
||||
$user = User::factory()->create();
|
||||
$country = Country::query()->where('iso2', 'DE')->firstOrFail();
|
||||
$country->update([
|
||||
'iso' => 'DE',
|
||||
'iso3' => 'DEU',
|
||||
'name' => 'Germany',
|
||||
'name_common' => 'Germany',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->from('/profile/edit')
|
||||
->patch('/profile', [
|
||||
'name' => 'Updated User',
|
||||
'email' => $user->email,
|
||||
'country_id' => $country->id,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
expect($user->fresh()->country_id)->toBe($country->id);
|
||||
expect(optional($user->fresh()->profile)->country_code)->toBe('DE');
|
||||
});
|
||||
56
tests/Feature/Countries/SyncCountriesCommandTest.php
Normal file
56
tests/Feature/Countries/SyncCountriesCommandTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Country;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
it('sync command imports countries from the configured remote source', function (): void {
|
||||
config()->set('skinbase-countries.endpoint', 'https://countries.test/all');
|
||||
config()->set('skinbase-countries.fallback_seed_enabled', false);
|
||||
|
||||
Http::fake([
|
||||
'https://countries.test/all' => Http::response([
|
||||
[
|
||||
'cca2' => 'SI',
|
||||
'cca3' => 'SVN',
|
||||
'ccn3' => '705',
|
||||
'name' => ['common' => 'Slovenia', 'official' => 'Republic of Slovenia'],
|
||||
'region' => 'Europe',
|
||||
'subregion' => 'Central Europe',
|
||||
'flags' => ['svg' => 'https://flags.test/si.svg', 'png' => 'https://flags.test/si.png'],
|
||||
'flag' => '🇸🇮',
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$this->artisan('skinbase:sync-countries')->assertSuccessful();
|
||||
|
||||
expect(Country::query()->where('iso2', 'SI')->value('name_common'))->toBe('Slovenia');
|
||||
});
|
||||
|
||||
it('sync command fails when the remote source errors and fallback is disabled', function (): void {
|
||||
config()->set('skinbase-countries.endpoint', 'https://countries.test/all');
|
||||
config()->set('skinbase-countries.fallback_seed_enabled', false);
|
||||
|
||||
Http::fake([
|
||||
'https://countries.test/all' => Http::response(['message' => 'server error'], 500),
|
||||
]);
|
||||
|
||||
$this->artisan('skinbase:sync-countries')
|
||||
->assertExitCode(1);
|
||||
});
|
||||
|
||||
it('sync command fails gracefully when the payload contains no valid country records', function (): void {
|
||||
config()->set('skinbase-countries.endpoint', 'https://countries.test/all');
|
||||
config()->set('skinbase-countries.fallback_seed_enabled', false);
|
||||
|
||||
Http::fake([
|
||||
'https://countries.test/all' => Http::response([
|
||||
['bad' => 'payload'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$this->artisan('skinbase:sync-countries')
|
||||
->assertExitCode(1);
|
||||
});
|
||||
111
tests/Feature/Dashboard/DashboardOverviewTest.php
Normal file
111
tests/Feature/Dashboard/DashboardOverviewTest.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\DashboardPreference;
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
it('embeds dashboard overview counts for the authenticated user', function () {
|
||||
$user = User::factory()->create([
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$follower = User::factory()->create();
|
||||
$followed = User::factory()->create();
|
||||
$commenter = User::factory()->create();
|
||||
|
||||
$artwork = Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
DB::table('user_followers')->insert([
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'follower_id' => $follower->id,
|
||||
'created_at' => now(),
|
||||
],
|
||||
[
|
||||
'user_id' => $followed->id,
|
||||
'follower_id' => $user->id,
|
||||
'created_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('artwork_favourites')->insert([
|
||||
'user_id' => $user->id,
|
||||
'artwork_id' => $artwork->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
Notification::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => 'comment',
|
||||
'data' => [
|
||||
'message' => 'Unread dashboard notification',
|
||||
'url' => '/dashboard/notifications',
|
||||
],
|
||||
'read_at' => null,
|
||||
]);
|
||||
|
||||
ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $commenter->id,
|
||||
'content' => 'Unread dashboard comment',
|
||||
'raw_content' => 'Unread dashboard comment',
|
||||
'rendered_content' => '<p>Unread dashboard comment</p>',
|
||||
'is_approved' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/dashboard');
|
||||
|
||||
$response->assertOk()->assertSee('data-overview=', false);
|
||||
|
||||
$overview = $response->viewData('dashboard_overview');
|
||||
$preferences = $response->viewData('dashboard_preferences');
|
||||
|
||||
expect($overview)->toMatchArray([
|
||||
'artworks' => 1,
|
||||
'stories' => 0,
|
||||
'followers' => 1,
|
||||
'following' => 1,
|
||||
'favorites' => 1,
|
||||
'notifications' => $user->unreadNotifications()->count(),
|
||||
'received_comments' => 1,
|
||||
]);
|
||||
|
||||
expect($preferences)->toMatchArray([
|
||||
'pinned_spaces' => [],
|
||||
]);
|
||||
});
|
||||
|
||||
it('embeds saved pinned dashboard spaces for the authenticated user', function () {
|
||||
$user = User::factory()->create([
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
DashboardPreference::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'pinned_spaces' => [
|
||||
'/dashboard/notifications',
|
||||
'/studio',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/dashboard');
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
expect($response->viewData('dashboard_preferences'))->toMatchArray([
|
||||
'pinned_spaces' => [
|
||||
'/dashboard/notifications',
|
||||
'/studio',
|
||||
],
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\DashboardPreference;
|
||||
use App\Models\User;
|
||||
|
||||
it('persists sanitized pinned dashboard spaces for the authenticated user', function () {
|
||||
$user = User::factory()->create([
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->putJson('/api/dashboard/preferences/shortcuts', [
|
||||
'pinned_spaces' => [
|
||||
'/dashboard/notifications',
|
||||
'/dashboard/notifications',
|
||||
'/dashboard/comments/received',
|
||||
'/not-allowed',
|
||||
'/studio',
|
||||
],
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'data' => [
|
||||
'pinned_spaces' => [
|
||||
'/dashboard/notifications',
|
||||
'/dashboard/comments/received',
|
||||
'/studio',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
expect(DashboardPreference::query()->find($user->id)?->pinned_spaces)->toBe([
|
||||
'/dashboard/notifications',
|
||||
'/dashboard/comments/received',
|
||||
'/studio',
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows clearing all pinned dashboard spaces for the authenticated user', function () {
|
||||
$user = User::factory()->create([
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
DashboardPreference::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'pinned_spaces' => [
|
||||
'/dashboard/notifications',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->putJson('/api/dashboard/preferences/shortcuts', [
|
||||
'pinned_spaces' => [],
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'data' => [
|
||||
'pinned_spaces' => [],
|
||||
],
|
||||
]);
|
||||
|
||||
expect(DashboardPreference::query()->find($user->id)?->pinned_spaces)->toBe([]);
|
||||
});
|
||||
29
tests/Feature/Dashboard/NotificationsPageTest.php
Normal file
29
tests/Feature/Dashboard/NotificationsPageTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
|
||||
it('renders the dashboard notifications page for an authenticated user', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Notification::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => 'comment',
|
||||
'data' => [
|
||||
'type' => 'comment',
|
||||
'message' => 'Someone commented on your artwork',
|
||||
'url' => '/dashboard/comments/received',
|
||||
],
|
||||
'read_at' => null,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/dashboard/notifications');
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('Notifications', false)
|
||||
->assertSee('Someone commented on your artwork', false)
|
||||
->assertSee('Unread', false);
|
||||
});
|
||||
66
tests/Feature/Dashboard/ReceivedCommentsUnreadStateTest.php
Normal file
66
tests/Feature/Dashboard/ReceivedCommentsUnreadStateTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\User;
|
||||
use App\Services\ReceivedCommentsInboxService;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
it('tracks unread received comments and clears them when the inbox is opened', function () {
|
||||
Carbon::setTestNow('2026-03-19 12:00:00');
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$commenter = User::factory()->create();
|
||||
|
||||
$artwork = Artwork::factory()->create([
|
||||
'user_id' => $owner->id,
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$firstComment = ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $commenter->id,
|
||||
'content' => 'First unread comment',
|
||||
'raw_content' => 'First unread comment',
|
||||
'rendered_content' => '<p>First unread comment</p>',
|
||||
'is_approved' => true,
|
||||
'created_at' => now()->subMinute(),
|
||||
'updated_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$service = app(ReceivedCommentsInboxService::class);
|
||||
|
||||
expect($service->unreadCountForUser($owner))->toBe(1);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->get('/dashboard/comments/received')
|
||||
->assertOk()
|
||||
->assertSee('Marked 1 new comment as read', false);
|
||||
|
||||
$this->assertDatabaseHas('user_received_comment_reads', [
|
||||
'user_id' => $owner->id,
|
||||
'artwork_comment_id' => $firstComment->id,
|
||||
]);
|
||||
expect($service->unreadCountForUser($owner))->toBe(0);
|
||||
|
||||
Carbon::setTestNow('2026-03-19 12:05:00');
|
||||
|
||||
ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $commenter->id,
|
||||
'content' => 'Second unread comment',
|
||||
'raw_content' => 'Second unread comment',
|
||||
'rendered_content' => '<p>Second unread comment</p>',
|
||||
'is_approved' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
expect($service->unreadCountForUser($owner))->toBe(1);
|
||||
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
@@ -105,7 +105,7 @@ it('following tab returns 200 for users with no follows', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/community/activity?type=following')
|
||||
->get('/community/activity?filter=following')
|
||||
->assertStatus(200);
|
||||
});
|
||||
|
||||
@@ -127,10 +127,12 @@ it('following tab shows only events from followed users', function () {
|
||||
// Event from non-followed user (should not appear)
|
||||
ActivityEvent::record($other->id, ActivityEvent::TYPE_UPLOAD, ActivityEvent::TARGET_ARTWORK, $artwork->id);
|
||||
|
||||
$response = $this->actingAs($user)->get('/community/activity?type=following');
|
||||
$response = $this->actingAs($user)->get('/community/activity?filter=following');
|
||||
$response->assertStatus(200);
|
||||
|
||||
$events = $response->original->gatherData()['events'];
|
||||
expect($events->total())->toBe(1);
|
||||
expect($events->first()->actor_id)->toBe($creator->id);
|
||||
$props = $response->viewData('props');
|
||||
$events = collect($props['initialActivities'] ?? []);
|
||||
|
||||
expect($events)->toHaveCount(1);
|
||||
expect(data_get($events->first(), 'user.id'))->toBe($creator->id);
|
||||
});
|
||||
|
||||
50
tests/Feature/Legacy/ReceivedCommentsPageTest.php
Normal file
50
tests/Feature/Legacy/ReceivedCommentsPageTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\User;
|
||||
|
||||
it('redirects legacy received comments urls to the canonical dashboard page', function () {
|
||||
$owner = User::factory()->create();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->get('/recieved-comments')
|
||||
->assertRedirect('/dashboard/comments/received')
|
||||
->assertStatus(301);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->get('/received-comments')
|
||||
->assertRedirect('/dashboard/comments/received')
|
||||
->assertStatus(301);
|
||||
});
|
||||
|
||||
it('renders the canonical received comments dashboard page for an authenticated user', function () {
|
||||
$owner = User::factory()->create();
|
||||
$commenter = User::factory()->create();
|
||||
|
||||
$artwork = Artwork::factory()->create([
|
||||
'user_id' => $owner->id,
|
||||
'is_approved' => true,
|
||||
'is_public' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $commenter->id,
|
||||
'content' => 'Legacy comment regression test',
|
||||
'raw_content' => 'Legacy comment regression test',
|
||||
'rendered_content' => '<p>Legacy comment regression test</p>',
|
||||
'is_approved' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($owner)->get('/dashboard/comments/received');
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('Received Comments', false)
|
||||
->assertSee('Total comments', false)
|
||||
->assertSee('Legacy comment regression test', false);
|
||||
});
|
||||
65
tests/Feature/LegacyProfileSubdomainRedirectTest.php
Normal file
65
tests/Feature/LegacyProfileSubdomainRedirectTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
beforeEach(function (): void {
|
||||
config()->set('app.url', 'http://skinbase26.test');
|
||||
});
|
||||
|
||||
it('redirects a username subdomain root to the canonical profile URL', function () {
|
||||
User::factory()->create([
|
||||
'username' => 'gregor',
|
||||
]);
|
||||
|
||||
$response = app(Kernel::class)->handle(
|
||||
Request::create('/', 'GET', ['tab' => 'favourites'], [], [], ['HTTP_HOST' => 'gregor.skinbase26.test'])
|
||||
);
|
||||
|
||||
expect($response->getStatusCode())->toBe(301);
|
||||
expect($response->headers->get('location'))->toBe('http://skinbase26.test/@gregor?tab=favourites');
|
||||
});
|
||||
|
||||
it('redirects the legacy username subdomain gallery path to the canonical profile gallery URL', function () {
|
||||
User::factory()->create([
|
||||
'username' => 'gregor',
|
||||
]);
|
||||
|
||||
$response = app(Kernel::class)->handle(
|
||||
Request::create('/gallery', 'GET', [], [], [], ['HTTP_HOST' => 'gregor.skinbase26.test'])
|
||||
);
|
||||
|
||||
expect($response->getStatusCode())->toBe(301);
|
||||
expect($response->headers->get('location'))->toBe('http://skinbase26.test/@gregor/gallery');
|
||||
});
|
||||
|
||||
it('redirects an old username subdomain to the canonical profile URL for the renamed user', function () {
|
||||
$user = User::factory()->create([
|
||||
'username' => 'gregor',
|
||||
]);
|
||||
|
||||
DB::table('username_redirects')->insert([
|
||||
'old_username' => 'oldgregor',
|
||||
'new_username' => 'gregor',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$response = app(Kernel::class)->handle(
|
||||
Request::create('/', 'GET', [], [], [], ['HTTP_HOST' => 'oldgregor.skinbase26.test'])
|
||||
);
|
||||
|
||||
expect($response->getStatusCode())->toBe(301);
|
||||
expect($response->headers->get('location'))->toBe('http://skinbase26.test/@gregor');
|
||||
});
|
||||
|
||||
it('does not treat reserved subdomains as profile hosts', function () {
|
||||
$this->call('GET', '/sections', [], [], [], ['HTTP_HOST' => 'www.skinbase26.test'])
|
||||
->assertRedirect('/categories')
|
||||
->assertStatus(301);
|
||||
});
|
||||
45
tests/Feature/ProfileGalleryPageTest.php
Normal file
45
tests/Feature/ProfileGalleryPageTest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
|
||||
it('renders the canonical profile gallery page', function () {
|
||||
$user = User::factory()->create([
|
||||
'username' => 'gregor',
|
||||
]);
|
||||
|
||||
Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Gallery Artwork',
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$this->get('/@gregor/gallery')
|
||||
->assertOk()
|
||||
->assertSee('http://skinbase26.test/@gregor/gallery', false)
|
||||
->assertSee('Profile\\/ProfileGallery', false);
|
||||
});
|
||||
|
||||
it('redirects the legacy gallery route to the canonical profile gallery', function () {
|
||||
$user = User::factory()->create([
|
||||
'username' => 'gregor',
|
||||
]);
|
||||
|
||||
$this->get('/gallery/' . $user->id . '/Gregor%20Klev%C5%BEe')
|
||||
->assertRedirect('/@gregor/gallery')
|
||||
->assertStatus(301);
|
||||
});
|
||||
|
||||
it('redirects mixed-case profile gallery usernames to the lowercase canonical route', function () {
|
||||
User::factory()->create([
|
||||
'username' => 'gregor',
|
||||
]);
|
||||
|
||||
$this->get('/@Gregor/gallery')
|
||||
->assertRedirect('/@gregor/gallery')
|
||||
->assertStatus(301);
|
||||
});
|
||||
@@ -7,6 +7,9 @@ declare(strict_types=1);
|
||||
* Routing Unification spec (§3.2 Explore, §4 Blog/Pages, §6.1 redirects).
|
||||
*/
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\ContentType;
|
||||
|
||||
// ── /explore routes ──────────────────────────────────────────────────────────
|
||||
|
||||
it('GET /explore returns 200', function () {
|
||||
@@ -78,6 +81,116 @@ it('GET /discover redirects to /discover/trending with 301', function () {
|
||||
$this->get('/discover')->assertRedirect('/discover/trending')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /sections redirects to /categories with 301', function () {
|
||||
$this->get('/sections')->assertRedirect('/categories')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /browse-categories redirects to /categories with 301', function () {
|
||||
$this->get('/browse-categories')->assertRedirect('/categories')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('legacy mixed-case category route redirects to the canonical category URL with 301', function () {
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Skins',
|
||||
'slug' => 'skins',
|
||||
]);
|
||||
|
||||
$category = Category::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'name' => 'BrowserBob',
|
||||
'slug' => 'browserbob',
|
||||
]);
|
||||
|
||||
$this->get('/Skins/BrowserBob/' . $category->id . '?ref=legacy')
|
||||
->assertRedirect('/skins/browserbob?ref=legacy')
|
||||
->assertStatus(301);
|
||||
});
|
||||
|
||||
it('legacy nested category route redirects to the canonical nested category URL with 301', function () {
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Skins',
|
||||
'slug' => 'skins',
|
||||
]);
|
||||
|
||||
$parent = Category::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'name' => 'BrowserBob',
|
||||
'slug' => 'browserbob',
|
||||
]);
|
||||
|
||||
$child = Category::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'parent_id' => $parent->id,
|
||||
'name' => 'sdsdsdsd',
|
||||
'slug' => 'sdsdsdsd',
|
||||
]);
|
||||
|
||||
$this->get('/Skins/BrowserBob/sdsdsdsd/' . $child->id)
|
||||
->assertRedirect('/skins/browserbob/sdsdsdsd')
|
||||
->assertStatus(301);
|
||||
});
|
||||
|
||||
it('legacy category route falls back to /categories and preserves query string when no category matches', function () {
|
||||
$this->get('/Skins/does-not-exist/999999?source=old-site')
|
||||
->assertRedirect('/categories?source=old-site')
|
||||
->assertStatus(301);
|
||||
});
|
||||
|
||||
it('legacy /category route redirects to the canonical category URL with 301', function () {
|
||||
$contentType = ContentType::query()->create([
|
||||
'name' => 'Skins',
|
||||
'slug' => 'skins',
|
||||
]);
|
||||
|
||||
$category = Category::query()->create([
|
||||
'content_type_id' => $contentType->id,
|
||||
'name' => 'BrowserBob',
|
||||
'slug' => 'browserbob',
|
||||
]);
|
||||
|
||||
$this->get('/category/skins/browserbob/' . $category->id . '?ref=legacy-category')
|
||||
->assertRedirect('/skins/browserbob?ref=legacy-category')
|
||||
->assertStatus(301);
|
||||
});
|
||||
|
||||
it('legacy /category route falls back to /categories and preserves query string when no category matches', function () {
|
||||
$this->get('/category/skins/does-not-exist/999999?source=legacy-category')
|
||||
->assertRedirect('/categories?source=legacy-category')
|
||||
->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /today-in-history redirects to /discover/on-this-day with 301', function () {
|
||||
$this->get('/today-in-history')->assertRedirect('/discover/on-this-day')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /members redirects to /creators/top with 301', function () {
|
||||
$this->get('/members')->assertRedirect('/creators/top')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /explore/members redirects to /creators/top with 301', function () {
|
||||
$this->get('/explore/members')->assertRedirect('/creators/top')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /latest-artworks redirects to /discover/fresh with 301', function () {
|
||||
$this->get('/latest-artworks')->assertRedirect('/discover/fresh')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /today-downloads redirects to /downloads/today with 301', function () {
|
||||
$this->get('/today-downloads')->assertRedirect('/downloads/today')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /monthly-commentators redirects to /comments/monthly with 301', function () {
|
||||
$this->get('/monthly-commentators')->assertRedirect('/comments/monthly')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /top-favourites redirects to /discover/top-rated with 301', function () {
|
||||
$this->get('/top-favourites')->assertRedirect('/discover/top-rated')->assertStatus(301);
|
||||
});
|
||||
|
||||
it('GET /downloads/today returns 200', function () {
|
||||
$this->get('/downloads/today')->assertOk();
|
||||
});
|
||||
|
||||
// ── /blog routes ─────────────────────────────────────────────────────────────
|
||||
|
||||
it('GET /blog returns 200', function () {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\Story;
|
||||
use App\Models\User;
|
||||
use App\Notifications\StoryStatusNotification;
|
||||
@@ -74,3 +75,21 @@ it('moderator can reject a pending story with reason and notify creator', functi
|
||||
|
||||
Notification::assertSentTo($creator, StoryStatusNotification::class);
|
||||
});
|
||||
|
||||
it('admin approval records a story publish activity event', function () {
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$creator = User::factory()->create();
|
||||
|
||||
$story = createPendingReviewStory($creator);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->post(route('admin.stories.approve', ['story' => $story->id]))
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('activity_events', [
|
||||
'actor_id' => $creator->id,
|
||||
'type' => ActivityEvent::TYPE_UPLOAD,
|
||||
'target_type' => ActivityEvent::TARGET_STORY,
|
||||
'target_id' => $story->id,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Story;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -94,3 +96,135 @@ it('creator can submit draft for review', function () {
|
||||
expect($story->status)->toBe('pending_review');
|
||||
expect($story->submitted_for_review_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('creator publish records story activity and stores a story_published notification', function () {
|
||||
$creator = User::factory()->create();
|
||||
|
||||
$story = Story::query()->create([
|
||||
'creator_id' => $creator->id,
|
||||
'title' => 'Publish Me',
|
||||
'slug' => 'publish-me-' . Str::lower(Str::random(6)),
|
||||
'content' => '<p>Publish content</p>',
|
||||
'story_type' => 'creator_story',
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($creator)
|
||||
->post(route('creator.stories.publish-now', ['story' => $story->id]));
|
||||
|
||||
$response->assertRedirect(route('stories.show', ['slug' => $story->slug]));
|
||||
|
||||
$story->refresh();
|
||||
|
||||
expect($story->status)->toBe('published');
|
||||
expect($story->published_at)->not->toBeNull();
|
||||
|
||||
$this->assertDatabaseHas('activity_events', [
|
||||
'actor_id' => $creator->id,
|
||||
'type' => ActivityEvent::TYPE_UPLOAD,
|
||||
'target_type' => ActivityEvent::TARGET_STORY,
|
||||
'target_id' => $story->id,
|
||||
]);
|
||||
|
||||
$notification = $creator->fresh()
|
||||
->notifications()
|
||||
->where('type', 'story_published')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
expect($notification)->toBeInstanceOf(Notification::class);
|
||||
expect($notification->data['type'] ?? null)->toBe('story_published');
|
||||
});
|
||||
|
||||
it('published story page renders successfully', function () {
|
||||
$creator = User::factory()->create();
|
||||
|
||||
$story = Story::query()->create([
|
||||
'creator_id' => $creator->id,
|
||||
'title' => 'Renderable Story',
|
||||
'slug' => 'renderable-story-' . Str::lower(Str::random(6)),
|
||||
'excerpt' => 'Renderable excerpt',
|
||||
'content' => json_encode([
|
||||
'type' => 'doc',
|
||||
'content' => [
|
||||
['type' => 'paragraph', 'content' => [['type' => 'text', 'text' => 'Renderable story content.']]],
|
||||
['type' => 'codeBlock', 'attrs' => ['language' => 'bash'], 'content' => [['type' => 'text', 'text' => 'git clone https://github.com/klevze/sqlBackup.git']]],
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
|
||||
'story_type' => 'creator_story',
|
||||
'status' => 'published',
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->get(route('stories.show', ['slug' => $story->slug]))
|
||||
->assertOk()
|
||||
->assertSee('Renderable Story', false)
|
||||
->assertSee('language-bash', false)
|
||||
->assertDontSee('{"type":"doc"', false);
|
||||
});
|
||||
|
||||
it('creator can publish through story editor api create endpoint', function () {
|
||||
$creator = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($creator)
|
||||
->postJson(route('api.stories.create'), [
|
||||
'title' => 'API Publish Story',
|
||||
'story_type' => 'creator_story',
|
||||
'content' => [
|
||||
'type' => 'doc',
|
||||
'content' => [
|
||||
['type' => 'paragraph', 'content' => [['type' => 'text', 'text' => 'Story body for API publishing.']]],
|
||||
],
|
||||
],
|
||||
'submit_action' => 'publish_now',
|
||||
'status' => 'published',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', 'published')
|
||||
->assertJsonPath('public_url', fn (string $url) => str_contains($url, '/stories/'));
|
||||
|
||||
$storyId = (int) $response->json('story_id');
|
||||
$story = Story::query()->findOrFail($storyId);
|
||||
|
||||
expect($story->status)->toBe('published');
|
||||
expect($story->published_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('creator can publish through story editor api update endpoint', function () {
|
||||
$creator = User::factory()->create();
|
||||
|
||||
$story = Story::query()->create([
|
||||
'creator_id' => $creator->id,
|
||||
'title' => 'API Draft Story',
|
||||
'slug' => 'api-draft-story-' . Str::lower(Str::random(6)),
|
||||
'content' => '<p>Draft content</p>',
|
||||
'story_type' => 'creator_story',
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($creator)
|
||||
->putJson(route('api.stories.update'), [
|
||||
'story_id' => $story->id,
|
||||
'title' => 'API Published Story',
|
||||
'story_type' => 'creator_story',
|
||||
'content' => [
|
||||
'type' => 'doc',
|
||||
'content' => [
|
||||
['type' => 'paragraph', 'content' => [['type' => 'text', 'text' => 'Updated story content for publication.']]],
|
||||
],
|
||||
],
|
||||
'submit_action' => 'publish_now',
|
||||
'status' => 'published',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', 'published')
|
||||
->assertJsonPath('preview_url', fn (string $url) => str_contains($url, '/preview'));
|
||||
|
||||
$story->refresh();
|
||||
|
||||
expect($story->title)->toBe('API Published Story');
|
||||
expect($story->status)->toBe('published');
|
||||
expect($story->published_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user