Save workspace changes
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('passes when every artwork has a matching user', function (): void {
|
||||
Artwork::factory()->count(3)->create();
|
||||
|
||||
$code = Artisan::call('artworks:check-user-refs', [
|
||||
'--chunk' => 2,
|
||||
'--show-missing' => 5,
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
file_put_contents(storage_path('logs/check-artwork-user-refs-test-output.log'), $output);
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and($output)->toContain('Checked 3 artworks: 3 valid, 0 missing user references, 0 null user_id values.')
|
||||
->and($output)->toContain('No missing user references found in artworks.user_id.');
|
||||
});
|
||||
|
||||
it('fails and reports orphaned artwork user references', function (): void {
|
||||
$validArtwork = Artwork::factory()->create();
|
||||
$orphanedArtwork = Artwork::factory()->create();
|
||||
|
||||
DB::table('artworks')
|
||||
->where('id', $orphanedArtwork->id)
|
||||
->update(['user_id' => 999999]);
|
||||
|
||||
$code = Artisan::call('artworks:check-user-refs', [
|
||||
'--chunk' => 1,
|
||||
'--show-missing' => 5,
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
|
||||
expect($validArtwork->fresh())->not->toBeNull()
|
||||
->and($code)->toBe(1)
|
||||
->and($output)->toContain('Checked 2 artworks: 1 valid, 1 missing user references, 0 null user_id values.')
|
||||
->and($output)->toContain('Found artworks with missing user references.')
|
||||
->and($output)->toContain((string) $orphanedArtwork->id)
|
||||
->and($output)->toContain('999999');
|
||||
});
|
||||
|
||||
it('can copy missing referenced users from the legacy users table by the same id', function (): void {
|
||||
$legacyUserId = 777;
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
DB::table('artworks')
|
||||
->where('id', $artwork->id)
|
||||
->update(['user_id' => $legacyUserId]);
|
||||
|
||||
config()->set('database.connections.legacy', config('database.connections.' . config('database.default')));
|
||||
DB::purge('legacy');
|
||||
|
||||
Schema::connection('legacy')->dropIfExists('legacy_users');
|
||||
Schema::connection('legacy')->create('legacy_users', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
$table->string('uname')->nullable();
|
||||
$table->string('real_name')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->unsignedTinyInteger('active')->default(1);
|
||||
$table->timestamp('joinDate')->nullable();
|
||||
$table->timestamp('LastVisit')->nullable();
|
||||
$table->string('country')->nullable();
|
||||
$table->string('country_code', 2)->nullable();
|
||||
$table->string('web')->nullable();
|
||||
$table->text('about_me')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->string('gender', 16)->nullable();
|
||||
});
|
||||
|
||||
DB::connection('legacy')->table('legacy_users')->insert([
|
||||
'user_id' => $legacyUserId,
|
||||
'uname' => 'legacy_artist',
|
||||
'real_name' => 'Legacy Artist',
|
||||
'email' => 'legacy.artist@example.test',
|
||||
'active' => 1,
|
||||
'joinDate' => '2020-01-02 03:04:05',
|
||||
'LastVisit' => '2025-01-05 06:07:08',
|
||||
'country' => 'Finland',
|
||||
'country_code' => 'FI',
|
||||
'web' => 'legacy.example.test',
|
||||
'about_me' => 'Imported from legacy.',
|
||||
'gender' => 'F',
|
||||
]);
|
||||
|
||||
$code = Artisan::call('artworks:check-user-refs', [
|
||||
'--chunk' => 1,
|
||||
'--show-missing' => 5,
|
||||
'--copy-missing-from-legacy' => true,
|
||||
'--legacy-users-table' => 'legacy_users',
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
file_put_contents(storage_path('logs/check-artwork-user-refs-copy-test-output.log'), $output);
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and(DB::table('users')->where('id', $legacyUserId)->exists())->toBeTrue()
|
||||
->and(DB::table('user_profiles')->where('user_id', $legacyUserId)->value('country_code'))->toBe('FI')
|
||||
->and($output)->toContain('[copied] imported legacy user #777 username=@legacy_artist name="Legacy Artist" email=<legacy.artist@example.test>')
|
||||
->and($output)->toContain('Checked 1 artworks: 1 valid, 0 missing user references, 0 null user_id values.')
|
||||
->and($output)->toContain('Legacy copy summary: requested 1 users, copied 1, would copy 0, conflicts 0, not found in legacy 0, errors 0.')
|
||||
->and($output)->toContain('Copied or would-copy user ids: 777');
|
||||
|
||||
Schema::connection('legacy')->dropIfExists('legacy_users');
|
||||
});
|
||||
|
||||
it('shows dry run copy details for legacy imports', function (): void {
|
||||
$legacyUserId = 778;
|
||||
$artwork = Artwork::factory()->create();
|
||||
|
||||
DB::table('artworks')
|
||||
->where('id', $artwork->id)
|
||||
->update(['user_id' => $legacyUserId]);
|
||||
|
||||
config()->set('database.connections.legacy', config('database.connections.' . config('database.default')));
|
||||
DB::purge('legacy');
|
||||
|
||||
Schema::connection('legacy')->dropIfExists('legacy_users');
|
||||
Schema::connection('legacy')->create('legacy_users', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
$table->string('uname')->nullable();
|
||||
$table->string('real_name')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->unsignedTinyInteger('active')->default(1);
|
||||
$table->timestamp('joinDate')->nullable();
|
||||
$table->timestamp('LastVisit')->nullable();
|
||||
$table->string('country')->nullable();
|
||||
$table->string('country_code', 2)->nullable();
|
||||
$table->string('web')->nullable();
|
||||
$table->text('about_me')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->string('gender', 16)->nullable();
|
||||
});
|
||||
|
||||
DB::connection('legacy')->table('legacy_users')->insert([
|
||||
'user_id' => $legacyUserId,
|
||||
'uname' => 'legacy_preview',
|
||||
'real_name' => 'Legacy Preview',
|
||||
'email' => 'legacy.preview@example.test',
|
||||
'active' => 1,
|
||||
]);
|
||||
|
||||
$code = Artisan::call('artworks:check-user-refs', [
|
||||
'--chunk' => 1,
|
||||
'--show-missing' => 5,
|
||||
'--copy-missing-from-legacy' => true,
|
||||
'--dry-run-copy' => true,
|
||||
'--legacy-users-table' => 'legacy_users',
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
|
||||
expect($code)->toBe(1)
|
||||
->and(DB::table('users')->where('id', $legacyUserId)->exists())->toBeFalse()
|
||||
->and($output)->toContain('[dry-run] would import legacy user #778 username=@legacy_preview name="Legacy Preview" email=<legacy.preview@example.test>')
|
||||
->and($output)->toContain('Legacy copy summary: requested 1 users, copied 0, would copy 1, conflicts 0, not found in legacy 0, errors 0.');
|
||||
|
||||
Schema::connection('legacy')->dropIfExists('legacy_users');
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use App\Services\Vision\AiArtworkVectorSearchService;
|
||||
use App\Services\Vision\ArtworkVectorIndexService;
|
||||
use App\Services\Vision\ArtworkVectorMetadataService;
|
||||
use App\Services\Vision\ArtworkVisionImageUrl;
|
||||
use App\Services\Vision\VectorGatewayClient;
|
||||
use App\Services\Vision\VectorService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function bindVectorService(): void
|
||||
{
|
||||
$imageUrl = new ArtworkVisionImageUrl();
|
||||
|
||||
app()->instance(VectorService::class, new VectorService(
|
||||
new AiArtworkVectorSearchService(new VectorGatewayClient(), $imageUrl),
|
||||
new ArtworkVectorIndexService(new VectorGatewayClient(), $imageUrl, new ArtworkVectorMetadataService()),
|
||||
));
|
||||
}
|
||||
|
||||
it('indexes artworks by latest updated_at descending by default', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$oldest = Artwork::factory()->for($user)->create([
|
||||
'title' => 'Oldest artwork',
|
||||
'slug' => 'oldest-artwork',
|
||||
'hash' => str_repeat('a', 32),
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subDays(3),
|
||||
'updated_at' => now()->subDays(3),
|
||||
]);
|
||||
|
||||
$middle = Artwork::factory()->for($user)->create([
|
||||
'title' => 'Middle artwork',
|
||||
'slug' => 'middle-artwork',
|
||||
'hash' => str_repeat('b', 32),
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subDays(2),
|
||||
'updated_at' => now()->subDays(2),
|
||||
]);
|
||||
|
||||
$latest = Artwork::factory()->for($user)->create([
|
||||
'title' => 'Latest artwork',
|
||||
'slug' => 'latest-artwork',
|
||||
'hash' => str_repeat('c', 32),
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
'updated_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
bindVectorService();
|
||||
|
||||
$code = Artisan::call('artworks:vectors-index', [
|
||||
'--dry-run' => true,
|
||||
'--public-only' => true,
|
||||
'--limit' => 3,
|
||||
'--batch' => 3,
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
|
||||
expect($code)->toBe(0);
|
||||
|
||||
$latestPos = strpos($output, 'Processing artwork=' . $latest->id);
|
||||
$middlePos = strpos($output, 'Processing artwork=' . $middle->id);
|
||||
$oldestPos = strpos($output, 'Processing artwork=' . $oldest->id);
|
||||
|
||||
expect($latestPos)->not->toBeFalse()
|
||||
->and($middlePos)->not->toBeFalse()
|
||||
->and($oldestPos)->not->toBeFalse()
|
||||
->and($latestPos)->toBeLessThan($middlePos)
|
||||
->and($middlePos)->toBeLessThan($oldestPos);
|
||||
});
|
||||
|
||||
it('supports legacy id ascending order when explicitly requested', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$first = Artwork::factory()->for($user)->create([
|
||||
'title' => 'First artwork',
|
||||
'slug' => 'first-artwork-' . Str::lower(Str::random(4)),
|
||||
'hash' => str_repeat('d', 32),
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subDay(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$second = Artwork::factory()->for($user)->create([
|
||||
'title' => 'Second artwork',
|
||||
'slug' => 'second-artwork-' . Str::lower(Str::random(4)),
|
||||
'hash' => str_repeat('e', 32),
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subDays(2),
|
||||
'updated_at' => now()->subDays(2),
|
||||
]);
|
||||
|
||||
bindVectorService();
|
||||
|
||||
$code = Artisan::call('artworks:vectors-index', [
|
||||
'--dry-run' => true,
|
||||
'--public-only' => true,
|
||||
'--limit' => 2,
|
||||
'--batch' => 2,
|
||||
'--order' => 'id-asc',
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
|
||||
expect($code)->toBe(0);
|
||||
|
||||
$firstPos = strpos($output, 'Processing artwork=' . $first->id);
|
||||
$secondPos = strpos($output, 'Processing artwork=' . $second->id);
|
||||
|
||||
expect($firstPos)->not->toBeFalse()
|
||||
->and($secondPos)->not->toBeFalse()
|
||||
->and($firstPos)->toBeLessThan($secondPos);
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use cPad\Plugins\News\Models\NewsArticle;
|
||||
use cPad\Plugins\News\Models\NewsCategory;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('publishes scheduled news articles whose publish time has passed', function (): void {
|
||||
$author = User::factory()->create();
|
||||
$category = NewsCategory::query()->create([
|
||||
'name' => 'Announcements',
|
||||
'slug' => 'announcements',
|
||||
'description' => 'Announcement category',
|
||||
'position' => 0,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$dueArticle = NewsArticle::query()->create([
|
||||
'title' => 'Due scheduled article',
|
||||
'slug' => 'due-scheduled-article',
|
||||
'excerpt' => 'Due now.',
|
||||
'content' => 'Body',
|
||||
'author_id' => $author->id,
|
||||
'category_id' => $category->id,
|
||||
'type' => NewsArticle::TYPE_ANNOUNCEMENT,
|
||||
'status' => 'scheduled',
|
||||
'editorial_status' => NewsArticle::EDITORIAL_STATUS_SCHEDULED,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$futureArticle = NewsArticle::query()->create([
|
||||
'title' => 'Future scheduled article',
|
||||
'slug' => 'future-scheduled-article',
|
||||
'excerpt' => 'Not due yet.',
|
||||
'content' => 'Body',
|
||||
'author_id' => $author->id,
|
||||
'category_id' => $category->id,
|
||||
'type' => NewsArticle::TYPE_ANNOUNCEMENT,
|
||||
'status' => 'scheduled',
|
||||
'editorial_status' => NewsArticle::EDITORIAL_STATUS_SCHEDULED,
|
||||
'published_at' => now()->addHour(),
|
||||
]);
|
||||
|
||||
$this->artisan('news:publish-scheduled')
|
||||
->expectsOutput(sprintf('Published News article #%d: "%s"', $dueArticle->id, $dueArticle->title))
|
||||
->expectsOutput('Done. Published: 1, Errors: 0.')
|
||||
->assertSuccessful();
|
||||
|
||||
expect($dueArticle->fresh())
|
||||
->editorial_status->toBe(NewsArticle::EDITORIAL_STATUS_PUBLISHED)
|
||||
->status->toBe('published')
|
||||
->and($futureArticle->fresh())
|
||||
->editorial_status->toBe(NewsArticle::EDITORIAL_STATUS_SCHEDULED)
|
||||
->status->toBe('scheduled');
|
||||
});
|
||||
|
||||
it('supports dry run for scheduled news publishing', function (): void {
|
||||
$author = User::factory()->create();
|
||||
$category = NewsCategory::query()->create([
|
||||
'name' => 'Platform',
|
||||
'slug' => 'platform',
|
||||
'description' => 'Platform category',
|
||||
'position' => 0,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$article = NewsArticle::query()->create([
|
||||
'title' => 'Dry run article',
|
||||
'slug' => 'dry-run-article',
|
||||
'excerpt' => 'Due but not published in dry run.',
|
||||
'content' => 'Body',
|
||||
'author_id' => $author->id,
|
||||
'category_id' => $category->id,
|
||||
'type' => NewsArticle::TYPE_PLATFORM_UPDATE,
|
||||
'status' => 'scheduled',
|
||||
'editorial_status' => NewsArticle::EDITORIAL_STATUS_SCHEDULED,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->artisan('news:publish-scheduled', ['--dry-run' => true])
|
||||
->expectsOutput(sprintf('[dry-run] Would publish News article #%d: "%s"', $article->id, $article->title))
|
||||
->assertSuccessful();
|
||||
|
||||
expect($article->fresh())
|
||||
->editorial_status->toBe(NewsArticle::EDITORIAL_STATUS_SCHEDULED)
|
||||
->status->toBe('scheduled');
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\ModerationContentType;
|
||||
use App\Models\ContentModerationActionLog;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\ContentModerationFinding;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Log\Events\MessageLogged;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('scans artwork comments and descriptions and stores suspicious findings', function (): void {
|
||||
$artwork = Artwork::factory()->create([
|
||||
'description' => 'Buy followers at https://promo.pornsite.com and win a crypto giveaway now',
|
||||
]);
|
||||
|
||||
ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $artwork->user_id,
|
||||
'content' => 'Visit my site at https://promo.pornsite.com now',
|
||||
'raw_content' => 'Visit my site at https://promo.pornsite.com now',
|
||||
]);
|
||||
|
||||
$code = Artisan::call('skinbase:scan-content-moderation');
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and(ContentModerationFinding::query()->count())->toBe(2)
|
||||
->and(ContentModerationFinding::query()->where('content_type', ModerationContentType::ArtworkComment)->exists())->toBeTrue()
|
||||
->and(ContentModerationFinding::query()->where('content_type', ModerationContentType::ArtworkDescription)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not create duplicate findings for unchanged content', function (): void {
|
||||
$artwork = Artwork::factory()->create([
|
||||
'description' => 'Buy followers at https://promo.pornsite.com and win a crypto giveaway now',
|
||||
]);
|
||||
|
||||
Artisan::call('skinbase:scan-content-moderation', ['--only' => 'descriptions']);
|
||||
Artisan::call('skinbase:scan-content-moderation', ['--only' => 'descriptions']);
|
||||
|
||||
expect(ContentModerationFinding::query()->where('content_type', ModerationContentType::ArtworkDescription)->count())->toBe(1)
|
||||
->and(ContentModerationFinding::query()->first()?->content_id)->toBe($artwork->id);
|
||||
});
|
||||
|
||||
it('supports dry runs without persisting findings', function (): void {
|
||||
Artwork::factory()->create([
|
||||
'description' => 'Buy followers at https://promo.pornsite.com and win a crypto giveaway now',
|
||||
]);
|
||||
|
||||
$code = Artisan::call('skinbase:scan-content-moderation', ['--dry-run' => true]);
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and(ContentModerationFinding::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('logs a command summary after scanning', function (): void {
|
||||
Event::fake([MessageLogged::class]);
|
||||
|
||||
Artwork::factory()->create([
|
||||
'description' => 'Buy followers at https://promo.pornsite.com and win a crypto giveaway now',
|
||||
]);
|
||||
|
||||
$code = Artisan::call('skinbase:scan-content-moderation', ['--only' => 'descriptions']);
|
||||
|
||||
expect($code)->toBe(0);
|
||||
|
||||
Event::assertDispatched(MessageLogged::class, function (MessageLogged $event): bool {
|
||||
return $event->level === 'info'
|
||||
&& $event->message === 'Content moderation scan complete.'
|
||||
&& ($event->context['targets'] ?? []) === ['artwork_description']
|
||||
&& ($event->context['counts']['scanned'] ?? 0) === 1
|
||||
&& ($event->context['counts']['flagged'] ?? 0) === 1;
|
||||
});
|
||||
});
|
||||
|
||||
it('shows target progress and verbose finding details while scanning', function (): void {
|
||||
Artwork::factory()->create([
|
||||
'description' => 'Buy followers at https://promo.pornsite.com and win a crypto giveaway now',
|
||||
]);
|
||||
|
||||
$code = Artisan::call('skinbase:scan-content-moderation', [
|
||||
'--only' => 'descriptions',
|
||||
'--verbose' => true,
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and($output)->toContain('Starting content moderation scan...')
|
||||
->and($output)->toContain('Scanning Artwork Description entries...')
|
||||
->and($output)->toContain('[artwork_description #')
|
||||
->and($output)->toContain('flagged')
|
||||
->and($output)->toContain('Finished Artwork Description: scanned=1, flagged=1');
|
||||
});
|
||||
|
||||
it('auto hides critical comment spam while keeping the finding', function (): void {
|
||||
$artwork = Artwork::factory()->create();
|
||||
$comment = ArtworkComment::factory()->create([
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $artwork->user_id,
|
||||
'content' => 'Buy followers now at https://promo.pornsite.com and claim your crypto giveaway',
|
||||
'raw_content' => 'Buy followers now at https://promo.pornsite.com and claim your crypto giveaway',
|
||||
'is_approved' => true,
|
||||
]);
|
||||
|
||||
$code = Artisan::call('skinbase:scan-content-moderation', ['--only' => 'comments']);
|
||||
|
||||
$finding = ContentModerationFinding::query()->where('content_type', ModerationContentType::ArtworkComment)->first();
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and($finding)->not->toBeNull()
|
||||
->and($finding?->is_auto_hidden)->toBeTrue()
|
||||
->and($finding?->action_taken)->toBe('auto_hide_comment')
|
||||
->and($comment->fresh()->is_approved)->toBeFalse();
|
||||
});
|
||||
|
||||
it('rescans existing findings with the latest rules', function (): void {
|
||||
$artwork = Artwork::factory()->create([
|
||||
'description' => 'Buy followers at https://promo.pornsite.com and win a crypto giveaway now',
|
||||
]);
|
||||
|
||||
$finding = ContentModerationFinding::query()->create([
|
||||
'content_type' => ModerationContentType::ArtworkDescription->value,
|
||||
'content_id' => $artwork->id,
|
||||
'artwork_id' => $artwork->id,
|
||||
'user_id' => $artwork->user_id,
|
||||
'status' => 'pending',
|
||||
'severity' => 'high',
|
||||
'score' => 90,
|
||||
'content_hash' => hash('sha256', 'old-hash'),
|
||||
'scanner_version' => '1.0',
|
||||
'content_snapshot' => 'old snapshot',
|
||||
]);
|
||||
|
||||
$code = Artisan::call('skinbase:rescan-content-moderation', ['--only' => 'descriptions']);
|
||||
$scannerVersion = (string) config('content_moderation.scanner_version');
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and(ContentModerationFinding::query()->where('content_type', ModerationContentType::ArtworkDescription)->where('content_id', $artwork->id)->where('scanner_version', $scannerVersion)->exists())->toBeTrue()
|
||||
->and(ContentModerationActionLog::query()->where('action_type', 'rescan')->exists())->toBeTrue();
|
||||
});
|
||||
Reference in New Issue
Block a user