Files
SkinbaseNova/tests/Feature/UserStatisticsV2Test.php
2026-02-26 21:12:32 +01:00

519 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Console\Commands\RecomputeUserStatsCommand;
use App\Models\Artwork;
use App\Models\ArtworkAward;
use App\Models\ArtworkComment;
use App\Models\ArtworkFavourite;
use App\Models\ArtworkReaction;
use App\Models\User;
use App\Services\ArtworkAwardService;
use App\Services\UserStatsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
// ─── Helpers ─────────────────────────────────────────────────────────────────
function makeCreator(): User
{
return User::factory()->create(['is_active' => true]);
}
function makeArtworkFor(User $user): Artwork
{
return Artwork::factory()->create([
'user_id' => $user->id,
'is_public' => true,
'is_approved'=> true,
]);
}
function statsRow(int $userId): object
{
return DB::table('user_statistics')->where('user_id', $userId)->first();
}
// ─── 1. Schema ───────────────────────────────────────────────────────────────
test('user_statistics v2 schema has all expected columns', function () {
$columns = DB::getSchemaBuilder()->getColumnListing('user_statistics');
$expected = [
'user_id',
'uploads_count',
'downloads_received_count',
'artwork_views_received_count',
'awards_received_count',
'favorites_received_count',
'comments_received_count',
'reactions_received_count',
'profile_views_count',
'followers_count',
'following_count',
'last_upload_at',
'last_active_at',
'created_at',
'updated_at',
];
foreach ($expected as $col) {
expect(in_array($col, $columns, true))->toBeTrue("Column '{$col}' is missing from user_statistics");
}
// Old column names must NOT exist
foreach (['uploads', 'downloads', 'pageviews', 'awards', 'profile_views'] as $old) {
expect(in_array($old, $columns, true))->toBeFalse("Old column '{$old}' still present in user_statistics");
}
});
// ─── 2. UserStatsService ensureRow ─────────────────────────────────────────
test('ensureRow creates a stats row if none exists', function () {
$user = makeCreator();
DB::table('user_statistics')->where('user_id', $user->id)->delete();
app(UserStatsService::class)->ensureRow($user->id);
expect(DB::table('user_statistics')->where('user_id', $user->id)->exists())->toBeTrue();
});
test('ensureRow does not throw if row already exists', function () {
$user = makeCreator();
app(UserStatsService::class)->ensureRow($user->id);
app(UserStatsService::class)->ensureRow($user->id); // second call should not fail
expect(DB::table('user_statistics')->where('user_id', $user->id)->count())->toBe(1);
});
// ─── 3. UserStatsService increment / decrement ────────────────────────────
test('incrementUploads increments uploads_count atomically', function () {
Queue::fake(); // prevent Meilisearch reindex job
$user = makeCreator();
$svc = app(UserStatsService::class);
$svc->incrementUploads($user->id);
$svc->incrementUploads($user->id, 4);
expect((int) statsRow($user->id)->uploads_count)->toBe(5);
});
test('decrementUploads does not go below zero', function () {
Queue::fake();
$user = makeCreator();
$svc = app(UserStatsService::class);
// Ensure row exists with default 0, then try to decrement
$svc->ensureRow($user->id);
$svc->decrementUploads($user->id, 10);
expect((int) statsRow($user->id)->uploads_count)->toBe(0);
});
test('incrementFavoritesReceived increments the counter', function () {
Queue::fake();
$user = makeCreator();
$svc = app(UserStatsService::class);
$svc->incrementFavoritesReceived($user->id);
$svc->incrementFavoritesReceived($user->id);
expect((int) statsRow($user->id)->favorites_received_count)->toBe(2);
});
test('decrementFavoritesReceived does not go below zero', function () {
Queue::fake();
$user = makeCreator();
$svc = app(UserStatsService::class);
// Ensure row exists with default 0, then try to decrement
$svc->ensureRow($user->id);
$svc->decrementFavoritesReceived($user->id);
expect((int) statsRow($user->id)->favorites_received_count)->toBe(0);
});
test('incrementCommentsReceived and decrementCommentsReceived work symmetrically', function () {
Queue::fake();
$user = makeCreator();
$svc = app(UserStatsService::class);
$svc->incrementCommentsReceived($user->id);
$svc->incrementCommentsReceived($user->id);
$svc->decrementCommentsReceived($user->id);
expect((int) statsRow($user->id)->comments_received_count)->toBe(1);
});
test('incrementReactionsReceived and decrementReactionsReceived work symmetrically', function () {
Queue::fake();
$user = makeCreator();
$svc = app(UserStatsService::class);
$svc->incrementReactionsReceived($user->id, 3);
$svc->decrementReactionsReceived($user->id, 2);
expect((int) statsRow($user->id)->reactions_received_count)->toBe(1);
});
test('incrementAwardsReceived and decrementAwardsReceived work symmetrically', function () {
Queue::fake();
$user = makeCreator();
$svc = app(UserStatsService::class);
$svc->incrementAwardsReceived($user->id);
$svc->decrementAwardsReceived($user->id);
expect((int) statsRow($user->id)->awards_received_count)->toBe(0);
});
test('incrementProfileViews increments profile_views_count', function () {
Queue::fake();
$user = makeCreator();
app(UserStatsService::class)->incrementProfileViews($user->id, 5);
expect((int) statsRow($user->id)->profile_views_count)->toBe(5);
});
// ─── 4. Timestamps ───────────────────────────────────────────────────────────
test('setLastUploadAt writes the timestamp', function () {
Queue::fake();
$user = makeCreator();
$ts = now()->subHours(3);
app(UserStatsService::class)->setLastUploadAt($user->id, $ts);
$row = statsRow($user->id);
expect($row->last_upload_at)->not->toBeNull();
});
test('setLastActiveAt writes the timestamp', function () {
Queue::fake();
$user = makeCreator();
app(UserStatsService::class)->setLastActiveAt($user->id);
expect(statsRow($user->id)->last_active_at)->not->toBeNull();
});
// ─── 5. Observer wiring Artwork created ────────────────────────────────────
test('creating an artwork increments uploads_count for its owner', function () {
Queue::fake();
$creator = makeCreator();
DB::table('user_statistics')->where('user_id', $creator->id)->delete();
makeArtworkFor($creator);
expect((int) statsRow($creator->id)->uploads_count)->toBe(1);
});
test('soft-deleting an artwork decrements uploads_count', function () {
Queue::fake();
$creator = makeCreator();
$artwork = makeArtworkFor($creator);
$before = (int) statsRow($creator->id)->uploads_count;
$artwork->delete();
expect((int) statsRow($creator->id)->uploads_count)->toBe(max(0, $before - 1));
});
// ─── 6. Observer wiring Favourites ─────────────────────────────────────────
test('adding a favourite increments creator favorites_received_count', function () {
Queue::fake();
$creator = makeCreator();
$liker = makeCreator();
$artwork = makeArtworkFor($creator);
DB::table('user_statistics')->where('user_id', $creator->id)
->update(['favorites_received_count' => 0]);
ArtworkFavourite::create(['user_id' => $liker->id, 'artwork_id' => $artwork->id]);
expect((int) statsRow($creator->id)->favorites_received_count)->toBe(1);
});
test('removing a favourite decrements creator favorites_received_count', function () {
Queue::fake();
$creator = makeCreator();
$liker = makeCreator();
$artwork = makeArtworkFor($creator);
$fav = ArtworkFavourite::create(['user_id' => $liker->id, 'artwork_id' => $artwork->id]);
$after = (int) statsRow($creator->id)->favorites_received_count;
$fav->delete();
expect((int) statsRow($creator->id)->favorites_received_count)->toBe(max(0, $after - 1));
});
// ─── 7. Observer wiring Comments ───────────────────────────────────────────
test('adding a comment increments creator comments_received_count', function () {
Queue::fake();
$creator = makeCreator();
$commenter = makeCreator();
$artwork = makeArtworkFor($creator);
DB::table('user_statistics')->where('user_id', $creator->id)
->update(['comments_received_count' => 0]);
ArtworkComment::create([
'artwork_id' => $artwork->id,
'user_id' => $commenter->id,
'content' => 'Nice work!',
'is_approved'=> true,
]);
expect((int) statsRow($creator->id)->comments_received_count)->toBe(1);
});
test('soft-deleting a comment decrements creator comments_received_count', function () {
Queue::fake();
$creator = makeCreator();
$commenter = makeCreator();
$artwork = makeArtworkFor($creator);
$comment = ArtworkComment::create([
'artwork_id' => $artwork->id,
'user_id' => $commenter->id,
'content' => 'Hi',
'is_approved'=> true,
]);
$before = (int) statsRow($creator->id)->comments_received_count;
$comment->delete();
expect((int) statsRow($creator->id)->comments_received_count)->toBe(max(0, $before - 1));
});
// ─── 8. Observer wiring Reactions ──────────────────────────────────────────
test('adding a reaction increments creator reactions_received_count', function () {
Queue::fake();
$creator = makeCreator();
$reactor = makeCreator();
$artwork = makeArtworkFor($creator);
DB::table('user_statistics')->where('user_id', $creator->id)
->update(['reactions_received_count' => 0]);
ArtworkReaction::create([
'artwork_id' => $artwork->id,
'user_id' => $reactor->id,
'reaction' => 'heart',
]);
expect((int) statsRow($creator->id)->reactions_received_count)->toBe(1);
});
test('removing a reaction decrements creator reactions_received_count', function () {
Queue::fake();
$creator = makeCreator();
$reactor = makeCreator();
$artwork = makeArtworkFor($creator);
$reaction = ArtworkReaction::create([
'artwork_id' => $artwork->id,
'user_id' => $reactor->id,
'reaction' => 'thumbs_up',
]);
$before = (int) statsRow($creator->id)->reactions_received_count;
$reaction->delete();
expect((int) statsRow($creator->id)->reactions_received_count)->toBe(max(0, $before - 1));
});
// ─── 9. Observer wiring Awards ────────────────────────────────────────────
test('giving an award increments creator awards_received_count', function () {
Queue::fake();
$creator = makeCreator();
$awarder = makeCreator();
$artwork = makeArtworkFor($creator);
DB::table('user_statistics')->where('user_id', $creator->id)
->update(['awards_received_count' => 0]);
$svc = app(ArtworkAwardService::class);
$svc->award($artwork, $awarder, 'gold');
expect((int) statsRow($creator->id)->awards_received_count)->toBe(1);
});
test('removing an award decrements creator awards_received_count', function () {
Queue::fake();
$creator = makeCreator();
$awarder = makeCreator();
$artwork = makeArtworkFor($creator);
$svc = app(ArtworkAwardService::class);
$svc->award($artwork, $awarder, 'gold');
$before = (int) statsRow($creator->id)->awards_received_count;
$svc->removeAward($artwork, $awarder);
expect((int) statsRow($creator->id)->awards_received_count)->toBe(max(0, $before - 1));
});
// ─── 10. Recompute single user ─────────────────────────────────────────────
test('recomputeUser rebuilds counters from source tables', function () {
Queue::fake();
$creator = makeCreator();
$fanA = makeCreator();
$fanB = makeCreator();
$art1 = makeArtworkFor($creator);
$art2 = makeArtworkFor($creator);
// Add 2 favourites
ArtworkFavourite::create(['user_id' => $fanA->id, 'artwork_id' => $art1->id]);
ArtworkFavourite::create(['user_id' => $fanB->id, 'artwork_id' => $art2->id]);
// Add 1 comment
ArtworkComment::create([
'artwork_id' => $art1->id,
'user_id' => $fanA->id,
'content' => 'Nice',
'is_approved'=> true,
]);
// Corrupt the stored counters to simulate drift
DB::table('user_statistics')->where('user_id', $creator->id)->update([
'uploads_count' => 99,
'favorites_received_count'=> 99,
'comments_received_count' => 99,
]);
// Recompute should restore correct values
$svc = app(UserStatsService::class);
$svc->recomputeUser($creator->id);
$row = statsRow($creator->id);
expect((int) $row->uploads_count)->toBe(2)
->and((int) $row->favorites_received_count)->toBe(2)
->and((int) $row->comments_received_count)->toBe(1);
});
test('recomputeUser dry-run does not write to database', function () {
Queue::fake();
$creator = makeCreator();
makeArtworkFor($creator);
// Corrupt the counter
DB::table('user_statistics')->where('user_id', $creator->id)
->update(['uploads_count' => 99]);
$svc = app(UserStatsService::class);
$result = $svc->recomputeUser($creator->id, dryRun: true);
// Returned value should be correct
expect($result['uploads_count'])->toBe(1);
// Nothing should have been written
expect((int) statsRow($creator->id)->uploads_count)->toBe(99);
});
// ─── 11. Recompute command ────────────────────────────────────────────────────
test('recompute command dry-run does not write changes', function () {
Queue::fake();
$creator = makeCreator();
makeArtworkFor($creator);
DB::table('user_statistics')->where('user_id', $creator->id)
->update(['uploads_count' => 99]);
$this->artisan('skinbase:recompute-user-stats', [
'user_id' => $creator->id,
'--dry-run'=> true,
])->assertSuccessful();
expect((int) statsRow($creator->id)->uploads_count)->toBe(99);
});
test('recompute command live applies correct values', function () {
Queue::fake();
$creator = makeCreator();
makeArtworkFor($creator);
makeArtworkFor($creator);
DB::table('user_statistics')->where('user_id', $creator->id)
->update(['uploads_count' => 0]);
$this->artisan('skinbase:recompute-user-stats', [
'user_id' => $creator->id,
])->assertSuccessful();
expect((int) statsRow($creator->id)->uploads_count)->toBe(2);
});
test('recompute command --all processes all users', function () {
Queue::fake();
$userA = makeCreator();
$userB = makeCreator();
makeArtworkFor($userA);
DB::table('user_statistics')
->whereIn('user_id', [$userA->id, $userB->id])
->update(['uploads_count' => 0]);
$this->artisan('skinbase:recompute-user-stats', ['--all' => true])
->assertSuccessful();
expect((int) statsRow($userA->id)->uploads_count)->toBe(1)
->and((int) statsRow($userB->id)->uploads_count)->toBe(0);
});
// ─── 12. Meilisearch toSearchableArray ─────────────────────────────────────
test('User toSearchableArray contains v2 stat fields', function () {
Queue::fake();
$user = makeCreator();
// Ensure stats row exists before updating
app(UserStatsService::class)->ensureRow($user->id);
DB::table('user_statistics')->where('user_id', $user->id)->update([
'uploads_count' => 10,
'downloads_received_count' => 20,
'artwork_views_received_count' => 30,
'awards_received_count' => 4,
'favorites_received_count' => 5,
'comments_received_count' => 6,
'reactions_received_count' => 7,
'followers_count' => 100,
'following_count' => 50,
]);
$user->load('statistics');
$arr = $user->toSearchableArray();
expect($arr)->toHaveKey('uploads_count', 10)
->and($arr)->toHaveKey('downloads_received_count', 20)
->and($arr)->toHaveKey('artwork_views_received_count', 30)
->and($arr)->toHaveKey('awards_received_count', 4)
->and($arr)->toHaveKey('favorites_received_count', 5)
->and($arr)->toHaveKey('comments_received_count', 6)
->and($arr)->toHaveKey('reactions_received_count', 7)
->and($arr)->toHaveKey('followers_count', 100)
->and($arr)->toHaveKey('following_count', 50);
// Old key must not be present
expect(array_key_exists('uploads', $arr))->toBeFalse();
});