messages implemented
This commit is contained in:
518
tests/Feature/UserStatisticsV2Test.php
Normal file
518
tests/Feature/UserStatisticsV2Test.php
Normal file
@@ -0,0 +1,518 @@
|
||||
<?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();
|
||||
});
|
||||
Reference in New Issue
Block a user