358 lines
15 KiB
PHP
358 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\NovaCard;
|
|
use App\Models\NovaCardCategory;
|
|
use App\Models\NovaCardChallenge;
|
|
use App\Models\NovaCardChallengeEntry;
|
|
use App\Models\NovaCardComment;
|
|
use App\Models\NovaCardTemplate;
|
|
use App\Models\Report;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
|
|
function reportCardCategory(array $attributes = []): NovaCardCategory
|
|
{
|
|
return NovaCardCategory::query()->create(array_merge([
|
|
'slug' => 'report-category-' . Str::lower(Str::random(6)),
|
|
'name' => 'Report Category',
|
|
'description' => 'Report category',
|
|
'active' => true,
|
|
'order_num' => 0,
|
|
], $attributes));
|
|
}
|
|
|
|
function reportCardTemplate(array $attributes = []): NovaCardTemplate
|
|
{
|
|
return NovaCardTemplate::query()->create(array_merge([
|
|
'slug' => 'report-template-' . Str::lower(Str::random(6)),
|
|
'name' => 'Report Template',
|
|
'description' => 'Report 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'],
|
|
'active' => true,
|
|
'official' => true,
|
|
'order_num' => 0,
|
|
], $attributes));
|
|
}
|
|
|
|
function reportableCard(User $user, array $attributes = []): NovaCard
|
|
{
|
|
$category = $attributes['category'] ?? reportCardCategory();
|
|
$template = $attributes['template'] ?? reportCardTemplate();
|
|
|
|
return NovaCard::query()->create(array_merge([
|
|
'user_id' => $user->id,
|
|
'category_id' => $category->id,
|
|
'template_id' => $template->id,
|
|
'title' => 'Reportable Card',
|
|
'slug' => 'reportable-card-' . Str::lower(Str::random(6)),
|
|
'quote_text' => 'Card that can be reported.',
|
|
'format' => NovaCard::FORMAT_SQUARE,
|
|
'project_json' => [
|
|
'content' => ['title' => 'Reportable Card', 'quote_text' => 'Card that can be reported.'],
|
|
'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' => [],
|
|
],
|
|
'render_version' => 2,
|
|
'schema_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()->subMinutes(5),
|
|
], Arr::except($attributes, ['category', 'template'])));
|
|
}
|
|
|
|
it('accepts nova card, challenge, and challenge entry reports through the shared intake endpoint', function (): void {
|
|
$reporter = User::factory()->create(['username' => 'reporter']);
|
|
$creator = User::factory()->create(['username' => 'creator']);
|
|
$card = reportableCard($creator);
|
|
$challenge = NovaCardChallenge::query()->create([
|
|
'user_id' => $creator->id,
|
|
'slug' => 'reporting-challenge',
|
|
'title' => 'Reporting Challenge',
|
|
'description' => 'Challenge description',
|
|
'prompt' => 'Challenge prompt',
|
|
'status' => NovaCardChallenge::STATUS_ACTIVE,
|
|
'official' => true,
|
|
'featured' => false,
|
|
'starts_at' => now()->subDay(),
|
|
'ends_at' => now()->addWeek(),
|
|
]);
|
|
$entry = NovaCardChallengeEntry::query()->create([
|
|
'challenge_id' => $challenge->id,
|
|
'card_id' => $card->id,
|
|
'user_id' => $creator->id,
|
|
'status' => NovaCardChallengeEntry::STATUS_SUBMITTED,
|
|
'note' => 'Challenge entry note',
|
|
]);
|
|
|
|
$this->actingAs($reporter)
|
|
->postJson(route('api.reports.store'), [
|
|
'target_type' => 'nova_card',
|
|
'target_id' => $card->id,
|
|
'reason' => 'Misleading card',
|
|
])
|
|
->assertCreated();
|
|
|
|
$this->actingAs($reporter)
|
|
->postJson(route('api.reports.store'), [
|
|
'target_type' => 'nova_card_challenge',
|
|
'target_id' => $challenge->id,
|
|
'reason' => 'Bad challenge brief',
|
|
])
|
|
->assertCreated();
|
|
|
|
$this->actingAs($reporter)
|
|
->postJson(route('api.reports.store'), [
|
|
'target_type' => 'nova_card_challenge_entry',
|
|
'target_id' => $entry->id,
|
|
'reason' => 'Spam challenge entry',
|
|
'details' => 'This entry is unrelated to the prompt.',
|
|
])
|
|
->assertCreated();
|
|
|
|
expect(Report::query()->where('reporter_id', $reporter->id)->count())->toBe(3);
|
|
});
|
|
|
|
it('accepts nova card comment reports through the shared intake endpoint', function (): void {
|
|
$reporter = User::factory()->create(['username' => 'commentreporter']);
|
|
$creator = User::factory()->create(['username' => 'commentreportcreator']);
|
|
$card = reportableCard($creator, ['title' => 'Comment Report Card']);
|
|
$comment = NovaCardComment::query()->create([
|
|
'card_id' => $card->id,
|
|
'user_id' => $creator->id,
|
|
'body' => 'Questionable comment body.',
|
|
'rendered_body' => 'Questionable comment body.',
|
|
'status' => 'visible',
|
|
]);
|
|
|
|
$this->actingAs($reporter)
|
|
->postJson(route('api.reports.store'), [
|
|
'target_type' => 'nova_card_comment',
|
|
'target_id' => $comment->id,
|
|
'reason' => 'Abusive comment',
|
|
])
|
|
->assertCreated();
|
|
|
|
expect(Report::query()->where('target_type', 'nova_card_comment')->where('target_id', $comment->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
it('filters nova card reports in the moderation queue and allows status transitions', function (): void {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$reporter = User::factory()->create(['username' => 'queuereporter']);
|
|
$creator = User::factory()->create(['username' => 'queuecreator']);
|
|
$card = reportableCard($creator, ['title' => 'Queue Card']);
|
|
$challenge = NovaCardChallenge::query()->create([
|
|
'user_id' => $creator->id,
|
|
'slug' => 'queue-challenge',
|
|
'title' => 'Queue Challenge',
|
|
'prompt' => 'Queue prompt',
|
|
'status' => NovaCardChallenge::STATUS_ACTIVE,
|
|
'official' => true,
|
|
'featured' => false,
|
|
'starts_at' => now()->subDay(),
|
|
'ends_at' => now()->addWeek(),
|
|
]);
|
|
|
|
$cardReport = Report::query()->create([
|
|
'reporter_id' => $reporter->id,
|
|
'target_type' => 'nova_card',
|
|
'target_id' => $card->id,
|
|
'reason' => 'Card spam',
|
|
'status' => 'open',
|
|
]);
|
|
|
|
Report::query()->create([
|
|
'reporter_id' => $reporter->id,
|
|
'target_type' => 'nova_card_challenge',
|
|
'target_id' => $challenge->id,
|
|
'reason' => 'Challenge abuse',
|
|
'status' => 'open',
|
|
]);
|
|
|
|
Report::query()->create([
|
|
'reporter_id' => $reporter->id,
|
|
'target_type' => 'user',
|
|
'target_id' => $creator->id,
|
|
'reason' => 'Unrelated user report',
|
|
'status' => 'open',
|
|
]);
|
|
|
|
$queue = $this->actingAs($admin)
|
|
->getJson(route('api.admin.reports.queue', ['group' => 'nova_cards', 'status' => 'open']))
|
|
->assertOk();
|
|
|
|
expect($queue->json('meta.total'))->toBe(2)
|
|
->and(collect($queue->json('data'))->pluck('target.label')->all())->toContain('Queue Card', 'Queue Challenge');
|
|
|
|
$this->actingAs($admin)
|
|
->patchJson(route('api.admin.reports.update', ['report' => $cardReport->id]), [
|
|
'status' => 'reviewing',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('report.status', 'reviewing')
|
|
->assertJsonPath('report.target.label', 'Queue Card');
|
|
|
|
$reviewing = $this->actingAs($admin)
|
|
->getJson(route('api.admin.reports.queue', ['group' => 'nova_cards', 'status' => 'reviewing']))
|
|
->assertOk();
|
|
|
|
expect($reviewing->json('meta.total'))->toBe(1)
|
|
->and($cardReport->fresh()->status)->toBe('reviewing');
|
|
});
|
|
|
|
it('records moderator notes and report history entries from the moderation queue', function (): void {
|
|
$admin = User::factory()->create(['role' => 'admin', 'username' => 'auditadmin']);
|
|
$reporter = User::factory()->create(['username' => 'auditreporter']);
|
|
$creator = User::factory()->create(['username' => 'auditcreator']);
|
|
$card = reportableCard($creator, ['title' => 'Audit Queue Card']);
|
|
|
|
$report = Report::query()->create([
|
|
'reporter_id' => $reporter->id,
|
|
'target_type' => 'nova_card',
|
|
'target_id' => $card->id,
|
|
'reason' => 'Needs closer review',
|
|
'status' => 'open',
|
|
]);
|
|
|
|
$response = $this->actingAs($admin)
|
|
->patchJson(route('api.admin.reports.update', ['report' => $report->id]), [
|
|
'status' => 'reviewing',
|
|
'moderator_note' => 'Escalated to card moderation while we verify the prompt source.',
|
|
])
|
|
->assertOk();
|
|
|
|
$report->refresh();
|
|
|
|
expect($report->status)->toBe('reviewing')
|
|
->and($report->moderator_note)->toBe('Escalated to card moderation while we verify the prompt source.')
|
|
->and($report->last_moderated_by_id)->toBe($admin->id)
|
|
->and($report->historyEntries()->count())->toBe(1)
|
|
->and($response->json('report.history.0.summary'))->toContain('Status open -> reviewing')
|
|
->and($response->json('report.history.0.actor.username'))->toBe('auditadmin');
|
|
});
|
|
|
|
it('allows moderators to update the underlying nova card from a report row', function (): void {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$reporter = User::factory()->create(['username' => 'targetreporter']);
|
|
$creator = User::factory()->create(['username' => 'targetcreator']);
|
|
$card = reportableCard($creator, [
|
|
'title' => 'Flaggable Queue Card',
|
|
'moderation_status' => NovaCard::MOD_PENDING,
|
|
'project_json' => [
|
|
'content' => ['title' => 'Flaggable Queue Card', 'quote_text' => 'Report queue card'],
|
|
'moderation' => [
|
|
'source' => 'publish_heuristics',
|
|
'flagged' => true,
|
|
'reasons' => ['self_remix_loop'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$report = Report::query()->create([
|
|
'reporter_id' => $reporter->id,
|
|
'target_type' => 'nova_card',
|
|
'target_id' => $card->id,
|
|
'reason' => 'Suspicious engagement bait',
|
|
'status' => 'reviewing',
|
|
]);
|
|
|
|
$this->actingAs($admin)
|
|
->postJson(route('api.admin.reports.moderate-target', ['report' => $report->id]), [
|
|
'action' => 'flag_card',
|
|
'disposition' => 'rights_review_required',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('report.target.moderation_target.card_id', $card->id)
|
|
->assertJsonPath('report.target.moderation_target.moderation_status', NovaCard::MOD_FLAGGED)
|
|
->assertJsonPath('report.target.moderation_target.moderation_reasons.0', 'self_remix_loop')
|
|
->assertJsonPath('report.target.moderation_target.moderation_reason_labels.0', 'Self-remix loop')
|
|
->assertJsonPath('report.target.moderation_target.moderation_override.source', 'report_queue')
|
|
->assertJsonPath('report.target.moderation_target.moderation_override.disposition', 'rights_review_required')
|
|
->assertJsonPath('report.target.moderation_target.moderation_override.disposition_label', 'Rights review required')
|
|
->assertJsonPath('report.target.moderation_target.moderation_override_history.0.disposition_label', 'Rights review required')
|
|
->assertJsonPath('report.target.moderation_target.moderation_override.report_id', $report->id)
|
|
->assertJsonPath('report.history.0.action_type', 'target_moderated');
|
|
|
|
expect($card->fresh()->moderation_status)->toBe(NovaCard::MOD_FLAGGED)
|
|
->and($card->fresh()->project_json['moderation']['override']['source'] ?? null)->toBe('report_queue')
|
|
->and($card->fresh()->project_json['moderation']['override']['disposition'] ?? null)->toBe('rights_review_required')
|
|
->and($report->fresh()->historyEntries()->count())->toBe(1)
|
|
->and($report->fresh()->last_moderated_by_id)->toBe($admin->id);
|
|
});
|
|
|
|
it('includes nova card comment reports in the nova cards moderation queue', function (): void {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$reporter = User::factory()->create(['username' => 'commentqueuereporter']);
|
|
$creator = User::factory()->create(['username' => 'commentqueuecreator']);
|
|
$card = reportableCard($creator, ['title' => 'Queue Comment Card']);
|
|
$comment = NovaCardComment::query()->create([
|
|
'card_id' => $card->id,
|
|
'user_id' => $creator->id,
|
|
'body' => 'Queue this comment too.',
|
|
'rendered_body' => 'Queue this comment too.',
|
|
'status' => 'visible',
|
|
]);
|
|
|
|
Report::query()->create([
|
|
'reporter_id' => $reporter->id,
|
|
'target_type' => 'nova_card_comment',
|
|
'target_id' => $comment->id,
|
|
'reason' => 'Comment harassment',
|
|
'status' => 'open',
|
|
]);
|
|
|
|
$queue = $this->actingAs($admin)
|
|
->getJson(route('api.admin.reports.queue', ['group' => 'nova_cards', 'status' => 'open']))
|
|
->assertOk();
|
|
|
|
expect(collect($queue->json('data'))->pluck('target.type')->all())->toContain('nova_card_comment');
|
|
});
|
|
|
|
it('renders challenge reporting controls for authenticated viewers', function (): void {
|
|
$viewer = User::factory()->create(['username' => 'challengeviewer']);
|
|
$creator = User::factory()->create(['username' => 'challengecreator']);
|
|
$card = reportableCard($creator, ['title' => 'Challenge Card']);
|
|
$challenge = NovaCardChallenge::query()->create([
|
|
'user_id' => $creator->id,
|
|
'slug' => 'challenge-report-page',
|
|
'title' => 'Challenge Report Page',
|
|
'description' => 'Challenge description',
|
|
'prompt' => 'Challenge prompt',
|
|
'status' => NovaCardChallenge::STATUS_ACTIVE,
|
|
'official' => true,
|
|
'featured' => true,
|
|
'starts_at' => now()->subDay(),
|
|
'ends_at' => now()->addWeek(),
|
|
]);
|
|
|
|
NovaCardChallengeEntry::query()->create([
|
|
'challenge_id' => $challenge->id,
|
|
'card_id' => $card->id,
|
|
'user_id' => $creator->id,
|
|
'status' => NovaCardChallengeEntry::STATUS_SUBMITTED,
|
|
]);
|
|
|
|
$response = $this->actingAs($viewer)
|
|
->get(route('cards.challenges.show', ['slug' => $challenge->slug]))
|
|
->assertOk();
|
|
|
|
expect($response->getContent())
|
|
->toContain('data-report-target-type="nova_card_challenge"')
|
|
->toContain('data-report-target-type="nova_card_challenge_entry"');
|
|
}); |