Featured artworks thumbnails
This commit is contained in:
48
tests/Feature/Community/LatestCommentsTest.php
Normal file
48
tests/Feature/Community/LatestCommentsTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('renders the latest comments page', function (): void {
|
||||
$author = User::factory()->create();
|
||||
$artwork = Artwork::factory()->for($author)->create();
|
||||
|
||||
ArtworkComment::factory()->for($artwork)->for($author)->create([
|
||||
'content' => 'Latest comments page regression comment',
|
||||
'raw_content' => 'Latest comments page regression comment',
|
||||
'rendered_content' => '<p>Latest comments page regression comment</p>',
|
||||
'created_at' => now()->subMinutes(5),
|
||||
]);
|
||||
|
||||
$this->get(route('legacy.latest_comments'))
|
||||
->assertOk()
|
||||
->assertSee('Latest Comments');
|
||||
});
|
||||
|
||||
it('returns latest comments api data', function (): void {
|
||||
$author = User::factory()->create();
|
||||
$artwork = Artwork::factory()->for($author)->create([
|
||||
'title' => 'Latest Comments Artwork',
|
||||
'slug' => 'latest-comments-artwork',
|
||||
]);
|
||||
|
||||
$comment = ArtworkComment::factory()->for($artwork)->for($author)->create([
|
||||
'content' => 'Latest comments api regression comment',
|
||||
'raw_content' => 'Latest comments api regression comment',
|
||||
'rendered_content' => '<p>Latest comments api regression comment</p>',
|
||||
'created_at' => now()->subMinutes(10),
|
||||
]);
|
||||
|
||||
$this->getJson(route('api.comments.latest'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.comment_id', $comment->id)
|
||||
->assertJsonPath('data.0.commenter.id', $author->id)
|
||||
->assertJsonPath('data.0.artwork.id', $artwork->id)
|
||||
->assertJsonPath('meta.total', 1);
|
||||
});
|
||||
@@ -341,14 +341,14 @@ test('homepage renders featured hero picture and preload from dedicated featured
|
||||
|
||||
$desktopUrl = $paths->url($artwork, 'desktop');
|
||||
$desktopXlUrl = $paths->url($artwork, 'desktop_xl');
|
||||
$xsUrl = $paths->url($artwork, 'xs');
|
||||
$mobileXsUrl = $paths->url($artwork, 'mobile_xs');
|
||||
$mobileUrl = $paths->url($artwork, 'mobile');
|
||||
|
||||
$this->get(route('index'))
|
||||
->assertOk()
|
||||
->assertSee($desktopUrl, false)
|
||||
->assertSee($desktopXlUrl, false)
|
||||
->assertSee($xsUrl, false)
|
||||
->assertSee($mobileXsUrl, false)
|
||||
->assertSee($mobileUrl, false)
|
||||
->assertSee('rel="preload"', false)
|
||||
->assertSee('type="image/webp"', false)
|
||||
|
||||
105
tests/Feature/Moderation/OnlineVisitorsModerationTest.php
Normal file
105
tests/Feature/Moderation/OnlineVisitorsModerationTest.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Moderation;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Traffic\OnlineVisitorRepository;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class OnlineVisitorsModerationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Mockery::close();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_moderation_online_page_requires_staff_access(): void
|
||||
{
|
||||
$user = User::factory()->create(['role' => 'user']);
|
||||
|
||||
$this->get('/moderation/traffic/online')
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/moderation/traffic/online')
|
||||
->assertRedirect(route('index'));
|
||||
|
||||
$this->actingAs($user)
|
||||
->getJson('/moderation/traffic/online/data')
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_staff_can_open_online_page_and_json_endpoint(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$repository = Mockery::mock(OnlineVisitorRepository::class);
|
||||
$repository->shouldReceive('summary')->twice()->andReturn([
|
||||
'total' => 3,
|
||||
'logged' => 1,
|
||||
'guests' => 1,
|
||||
'bots' => 1,
|
||||
'search_bots' => 1,
|
||||
'ai_bots' => 0,
|
||||
'social_bots' => 0,
|
||||
'seo_bots' => 0,
|
||||
'suspicious_bots' => 0,
|
||||
]);
|
||||
$repository->shouldReceive('all')->twice()->andReturn([
|
||||
[
|
||||
'visitor_key' => 'user:1',
|
||||
'type' => 'human_logged',
|
||||
'user_name' => 'Gregor',
|
||||
'ip_masked' => '188.230.xxx.xxx',
|
||||
'browser' => 'Chrome',
|
||||
'platform' => 'Windows',
|
||||
'current_url' => '/wallpapers',
|
||||
'route_name' => 'wallpapers.index',
|
||||
'referer' => null,
|
||||
'first_seen_at' => now()->subMinute()->toIso8601String(),
|
||||
'last_seen_at' => now()->toIso8601String(),
|
||||
'hits' => 2,
|
||||
],
|
||||
]);
|
||||
$repository->shouldReceive('activePages')->twice()->andReturn([
|
||||
['url' => '/wallpapers', 'visitors' => 3],
|
||||
]);
|
||||
$repository->shouldReceive('track')->andReturnNull();
|
||||
$this->app->instance(OnlineVisitorRepository::class, $repository);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get('/moderation/traffic/online')
|
||||
->assertOk()
|
||||
->assertSee('Online Visitors')
|
||||
->assertSee('Live view of logged users, guests, crawlers, AI bots, and suspicious traffic.');
|
||||
|
||||
$this->actingAs($admin)
|
||||
->getJson('/moderation/traffic/online/data')
|
||||
->assertOk()
|
||||
->assertJsonStructure([
|
||||
'summary' => ['total', 'logged', 'guests', 'bots', 'search_bots', 'ai_bots', 'social_bots', 'seo_bots', 'suspicious_bots'],
|
||||
'visitors',
|
||||
'active_pages',
|
||||
'generated_at',
|
||||
])
|
||||
->assertJsonPath('summary.total', 3)
|
||||
->assertJsonPath('active_pages.0.url', '/wallpapers');
|
||||
}
|
||||
|
||||
public function test_redis_failure_does_not_break_public_request(): void
|
||||
{
|
||||
$repository = Mockery::mock(OnlineVisitorRepository::class);
|
||||
$repository->shouldReceive('track')->andThrow(new \RuntimeException('Redis unavailable'));
|
||||
$this->app->instance(OnlineVisitorRepository::class, $repository);
|
||||
|
||||
$this->get('/robots.txt')
|
||||
->assertOk();
|
||||
}
|
||||
}
|
||||
167
tests/Feature/Traffic/OnlineVisitorTrackingTest.php
Normal file
167
tests/Feature/Traffic/OnlineVisitorTrackingTest.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Traffic;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Traffic\BotClassifier;
|
||||
use App\Services\Traffic\OnlineVisitorRepository;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class OnlineVisitorTrackingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_googlebot_is_classified_as_search_bot(): void
|
||||
{
|
||||
$classifier = app(BotClassifier::class);
|
||||
$request = Request::create('/wallpapers', 'GET', server: ['HTTP_USER_AGENT' => 'Googlebot/2.1']);
|
||||
|
||||
$result = $classifier->classify($request);
|
||||
|
||||
self::assertTrue($result['is_bot']);
|
||||
self::assertSame('search_bot', $result['type']);
|
||||
self::assertSame('Googlebot', $result['family']);
|
||||
}
|
||||
|
||||
public function test_gptbot_is_classified_as_ai_bot(): void
|
||||
{
|
||||
$classifier = app(BotClassifier::class);
|
||||
$request = Request::create('/art/1/test', 'GET', server: ['HTTP_USER_AGENT' => 'GPTBot']);
|
||||
|
||||
$result = $classifier->classify($request);
|
||||
|
||||
self::assertTrue($result['is_bot']);
|
||||
self::assertSame('ai_bot', $result['type']);
|
||||
self::assertSame('GPTBot', $result['family']);
|
||||
}
|
||||
|
||||
public function test_suspicious_user_agent_is_classified_as_suspicious_bot(): void
|
||||
{
|
||||
$classifier = app(BotClassifier::class);
|
||||
$request = Request::create('/rate.php', 'GET', server: ['HTTP_USER_AGENT' => 'python-requests/2.31']);
|
||||
|
||||
$result = $classifier->classify($request);
|
||||
|
||||
self::assertTrue($result['is_bot']);
|
||||
self::assertSame('suspicious_bot', $result['type']);
|
||||
self::assertSame('python-requests', $result['family']);
|
||||
}
|
||||
|
||||
public function test_logged_in_user_is_tracked_with_ttl_and_hit_counter(): void
|
||||
{
|
||||
$user = User::factory()->create(['role' => 'admin', 'name' => 'Gregor']);
|
||||
$repository = new InMemoryOnlineVisitorRepository(app(BotClassifier::class));
|
||||
|
||||
$request = Request::create('/wallpapers', 'GET', server: [
|
||||
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0',
|
||||
'HTTP_CF_CONNECTING_IP' => '188.230.12.14',
|
||||
]);
|
||||
$request->setUserResolver(static fn (): User => $user);
|
||||
|
||||
$repository->track($request);
|
||||
$repository->track($request);
|
||||
|
||||
$records = $repository->all();
|
||||
|
||||
self::assertCount(1, $records);
|
||||
self::assertSame('human_logged', $records[0]['type']);
|
||||
self::assertSame(2, $records[0]['hits']);
|
||||
self::assertSame('188.230.xxx.xxx', $records[0]['ip_masked']);
|
||||
self::assertSame(OnlineVisitorRepository::TTL_SECONDS, $repository->lastStoredTtl);
|
||||
}
|
||||
|
||||
public function test_guest_tracking_cleans_expired_records_from_index(): void
|
||||
{
|
||||
$repository = new InMemoryOnlineVisitorRepository(app(BotClassifier::class));
|
||||
|
||||
$request = Request::create('/news/test', 'GET', server: [
|
||||
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) Safari/605.1.15',
|
||||
'REMOTE_ADDR' => '192.168.10.22',
|
||||
]);
|
||||
$request->cookies->set((string) config('session.cookie'), 'guest-session-cookie');
|
||||
|
||||
$repository->track($request);
|
||||
$repository->seedIndexOnly('guest:expired');
|
||||
|
||||
$records = $repository->all();
|
||||
$summary = $repository->summary();
|
||||
$pages = $repository->activePages();
|
||||
|
||||
self::assertCount(1, $records);
|
||||
self::assertSame('human_guest', $records[0]['type']);
|
||||
self::assertSame(1, $summary['guests']);
|
||||
self::assertSame('/news/test', $pages[0]['url']);
|
||||
self::assertSame(['guest:expired'], $repository->removedFromIndex);
|
||||
}
|
||||
}
|
||||
|
||||
final class InMemoryOnlineVisitorRepository extends OnlineVisitorRepository
|
||||
{
|
||||
/** @var array<string, array<string, mixed>> */
|
||||
private array $records = [];
|
||||
|
||||
/** @var array<int, string> */
|
||||
private array $index = [];
|
||||
|
||||
/** @var array<int, string> */
|
||||
public array $removedFromIndex = [];
|
||||
|
||||
public ?int $lastStoredTtl = null;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function readIndexMembers(): array
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function readRecord(string $visitorKey): ?array
|
||||
{
|
||||
return $this->records[$visitorKey] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $record
|
||||
*/
|
||||
protected function storeRecord(string $visitorKey, array $record, int $ttlSeconds): void
|
||||
{
|
||||
$this->records[$visitorKey] = $record;
|
||||
$this->lastStoredTtl = $ttlSeconds;
|
||||
}
|
||||
|
||||
protected function addIndexMember(string $visitorKey): void
|
||||
{
|
||||
if (! in_array($visitorKey, $this->index, true)) {
|
||||
$this->index[] = $visitorKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $visitorKeys
|
||||
*/
|
||||
protected function removeIndexMembers(array $visitorKeys): void
|
||||
{
|
||||
foreach ($visitorKeys as $visitorKey) {
|
||||
$this->removedFromIndex[] = $visitorKey;
|
||||
$this->index = array_values(array_filter($this->index, static fn (string $indexedKey): bool => $indexedKey !== $visitorKey));
|
||||
}
|
||||
}
|
||||
|
||||
protected function deleteRecord(string $visitorKey): void
|
||||
{
|
||||
unset($this->records[$visitorKey]);
|
||||
}
|
||||
|
||||
public function seedIndexOnly(string $visitorKey): void
|
||||
{
|
||||
$this->index[] = $visitorKey;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user