166 lines
5.6 KiB
PHP
166 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\User;
|
|
use App\Services\ArtworkStatsService;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
beforeEach(function () {
|
|
// Disable Meilisearch and Redis during tests
|
|
config(['scout.driver' => 'null']);
|
|
});
|
|
|
|
// ── ArtworkViewController (POST /api/art/{id}/view) ──────────────────────────
|
|
|
|
it('returns 404 for a non-existent artwork on view', function () {
|
|
$this->postJson('/api/art/99999/view')->assertStatus(404);
|
|
});
|
|
|
|
it('returns 404 for a private artwork on view', function () {
|
|
$artwork = Artwork::factory()->create(['is_public' => false]);
|
|
$this->postJson("/api/art/{$artwork->id}/view")->assertStatus(404);
|
|
});
|
|
|
|
it('returns 404 for an unapproved artwork on view', function () {
|
|
$artwork = Artwork::factory()->create(['is_approved' => false]);
|
|
$this->postJson("/api/art/{$artwork->id}/view")->assertStatus(404);
|
|
});
|
|
|
|
it('records a view and returns ok=true on first call', function () {
|
|
$artwork = Artwork::factory()->create([
|
|
'is_public' => true,
|
|
'is_approved' => true,
|
|
'published_at' => now()->subDay(),
|
|
]);
|
|
|
|
// Ensure a stats row exists with 0 views
|
|
DB::table('artwork_stats')->insertOrIgnore([
|
|
'artwork_id' => $artwork->id,
|
|
'views' => 0,
|
|
'downloads' => 0,
|
|
'favorites' => 0,
|
|
'rating_avg' => 0,
|
|
'rating_count' => 0,
|
|
]);
|
|
|
|
$mock = $this->mock(ArtworkStatsService::class);
|
|
$mock->shouldReceive('logViewEvent')
|
|
->once()
|
|
->with($artwork->id, null); // null = guest (unauthenticated request)
|
|
$mock->shouldReceive('incrementViews')
|
|
->once()
|
|
->with($artwork->id, 1, true);
|
|
|
|
$response = $this->postJson("/api/art/{$artwork->id}/view");
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('ok', true)
|
|
->assertJsonPath('counted', true);
|
|
});
|
|
|
|
it('skips DB increment and returns counted=false if artwork was already viewed this session', function () {
|
|
$artwork = Artwork::factory()->create([
|
|
'is_public' => true,
|
|
'is_approved' => true,
|
|
'published_at' => now()->subDay(),
|
|
]);
|
|
|
|
// Mark as already viewed in the session
|
|
session()->put("art_viewed.{$artwork->id}", true);
|
|
|
|
$mock = $this->mock(ArtworkStatsService::class);
|
|
$mock->shouldReceive('incrementViews')->never();
|
|
|
|
$response = $this->postJson("/api/art/{$artwork->id}/view");
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('ok', true)
|
|
->assertJsonPath('counted', false);
|
|
});
|
|
|
|
// ── ArtworkDownloadController (POST /api/art/{id}/download) ──────────────────
|
|
|
|
it('returns 404 for a non-existent artwork on download', function () {
|
|
$this->postJson('/api/art/99999/download')->assertStatus(404);
|
|
});
|
|
|
|
it('returns 404 for a private artwork on download', function () {
|
|
$artwork = Artwork::factory()->create(['is_public' => false]);
|
|
$this->postJson("/api/art/{$artwork->id}/download")->assertStatus(404);
|
|
});
|
|
|
|
it('records a download and returns ok=true with a url', function () {
|
|
$artwork = Artwork::factory()->create([
|
|
'is_public' => true,
|
|
'is_approved' => true,
|
|
'published_at' => now()->subDay(),
|
|
]);
|
|
|
|
$mock = $this->mock(ArtworkStatsService::class);
|
|
$mock->shouldReceive('incrementDownloads')
|
|
->once()
|
|
->with($artwork->id, 1, true);
|
|
|
|
$response = $this->postJson("/api/art/{$artwork->id}/download");
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('ok', true)
|
|
->assertJsonStructure(['ok', 'url']);
|
|
});
|
|
|
|
it('inserts a row in artwork_downloads on valid download', function () {
|
|
$user = User::factory()->create();
|
|
$artwork = Artwork::factory()->create([
|
|
'is_public' => true,
|
|
'is_approved' => true,
|
|
'published_at' => now()->subDay(),
|
|
]);
|
|
|
|
// Stub the stats service so we don't need Redis
|
|
$mock = $this->mock(ArtworkStatsService::class);
|
|
$mock->shouldReceive('incrementDownloads')->once();
|
|
|
|
$this->actingAs($user)->postJson("/api/art/{$artwork->id}/download");
|
|
|
|
$this->assertDatabaseHas('artwork_downloads', [
|
|
'artwork_id' => $artwork->id,
|
|
'user_id' => $user->id,
|
|
]);
|
|
});
|
|
|
|
it('records download as guest (no user_id) when unauthenticated', function () {
|
|
$artwork = Artwork::factory()->create([
|
|
'is_public' => true,
|
|
'is_approved' => true,
|
|
'published_at' => now()->subDay(),
|
|
]);
|
|
|
|
$mock = $this->mock(ArtworkStatsService::class);
|
|
$mock->shouldReceive('incrementDownloads')->once();
|
|
|
|
$this->postJson("/api/art/{$artwork->id}/download");
|
|
|
|
$this->assertDatabaseHas('artwork_downloads', [
|
|
'artwork_id' => $artwork->id,
|
|
'user_id' => null,
|
|
]);
|
|
});
|
|
|
|
// ── Route names ───────────────────────────────────────────────────────────────
|
|
|
|
it('view endpoint route is named api.art.view', function () {
|
|
$artwork = Artwork::factory()->create([
|
|
'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay(),
|
|
]);
|
|
expect(route('api.art.view', ['id' => $artwork->id]))->toContain("/api/art/{$artwork->id}/view");
|
|
});
|
|
|
|
it('download endpoint route is named api.art.download', function () {
|
|
$artwork = Artwork::factory()->create([
|
|
'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay(),
|
|
]);
|
|
expect(route('api.art.download', ['id' => $artwork->id]))->toContain("/api/art/{$artwork->id}/download");
|
|
});
|