54 - Add test coverage for the counter
This commit is contained in:
parent
d482c6d3b7
commit
3a28988ae7
7 changed files with 373 additions and 16 deletions
|
|
@ -4,11 +4,16 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\TrackerFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Tracker extends Model
|
||||
{
|
||||
/** @use HasFactory<TrackerFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'asset_id',
|
||||
|
|
|
|||
30
database/factories/TrackerFactory.php
Normal file
30
database/factories/TrackerFactory.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Tracker;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Tracker>
|
||||
*/
|
||||
class TrackerFactory extends Factory
|
||||
{
|
||||
protected $model = Tracker::class;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => fn () => User::default()->id,
|
||||
'label' => 'Counter',
|
||||
'unit' => 'units',
|
||||
'count' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
153
tests/Feature/CountBackfillMigrationTest.php
Normal file
153
tests/Feature/CountBackfillMigrationTest.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CountBackfillMigrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const MIGRATION = __DIR__.'/../../database/migrations/2026_08_15_000002_add_count_to_trackers_drop_entries.php';
|
||||
|
||||
// Each require returns a fresh anonymous-class instance; there is no name to collide.
|
||||
private function migration(): object
|
||||
{
|
||||
return require self::MIGRATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the pre-migration shape: entries present, trackers without a count.
|
||||
*/
|
||||
private function revertToLedger(): void
|
||||
{
|
||||
$this->migration()->down();
|
||||
}
|
||||
|
||||
private function makeTracker(): int
|
||||
{
|
||||
$userId = DB::table('users')->insertGetId([
|
||||
'name' => 'Test',
|
||||
'email' => 'ledger@example.test',
|
||||
'password' => 'x',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return DB::table('trackers')->insertGetId([
|
||||
'user_id' => $userId,
|
||||
'label' => 'Counter',
|
||||
'unit' => 'units',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function addEntry(int $trackerId, string $quantity): void
|
||||
{
|
||||
DB::table('entries')->insert([
|
||||
'tracker_id' => $trackerId,
|
||||
'date' => now()->toDateString(),
|
||||
'quantity' => $quantity,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function countFor(int $trackerId): int
|
||||
{
|
||||
return (int) DB::table('trackers')->where('id', $trackerId)->value('count');
|
||||
}
|
||||
|
||||
public function test_backfill_sums_entries_into_count(): void
|
||||
{
|
||||
$this->revertToLedger();
|
||||
|
||||
$trackerId = $this->makeTracker();
|
||||
$this->addEntry($trackerId, '10');
|
||||
$this->addEntry($trackerId, '15');
|
||||
|
||||
$this->migration()->up();
|
||||
|
||||
$this->assertSame(25, $this->countFor($trackerId));
|
||||
}
|
||||
|
||||
public function test_backfill_rounds_fractional_totals(): void
|
||||
{
|
||||
$this->revertToLedger();
|
||||
|
||||
$trackerId = $this->makeTracker();
|
||||
$this->addEntry($trackerId, '1.4');
|
||||
$this->addEntry($trackerId, '1.2');
|
||||
|
||||
$this->migration()->up();
|
||||
|
||||
$this->assertSame(3, $this->countFor($trackerId));
|
||||
}
|
||||
|
||||
public function test_backfill_clamps_negative_totals_to_zero(): void
|
||||
{
|
||||
$this->revertToLedger();
|
||||
|
||||
$trackerId = $this->makeTracker();
|
||||
$this->addEntry($trackerId, '5');
|
||||
$this->addEntry($trackerId, '-9');
|
||||
|
||||
$this->migration()->up();
|
||||
|
||||
$this->assertSame(0, $this->countFor($trackerId));
|
||||
}
|
||||
|
||||
public function test_tracker_without_entries_defaults_to_zero(): void
|
||||
{
|
||||
$this->revertToLedger();
|
||||
|
||||
$trackerId = $this->makeTracker();
|
||||
|
||||
$this->migration()->up();
|
||||
|
||||
$this->assertSame(0, $this->countFor($trackerId));
|
||||
}
|
||||
|
||||
public function test_up_drops_the_entries_table(): void
|
||||
{
|
||||
$this->revertToLedger();
|
||||
$this->assertTrue(Schema::hasTable('entries'));
|
||||
|
||||
$this->migration()->up();
|
||||
|
||||
$this->assertFalse(Schema::hasTable('entries'));
|
||||
$this->assertTrue(Schema::hasColumn('trackers', 'count'));
|
||||
}
|
||||
|
||||
// The two down() tests start from the migrated schema, so they do not call revertToLedger().
|
||||
public function test_down_recreates_entries_from_count(): void
|
||||
{
|
||||
$trackerId = $this->makeTracker();
|
||||
DB::table('trackers')->where('id', $trackerId)->update(['count' => 7]);
|
||||
|
||||
$this->migration()->down();
|
||||
|
||||
$this->assertTrue(Schema::hasTable('entries'));
|
||||
$this->assertFalse(Schema::hasColumn('trackers', 'count'));
|
||||
|
||||
$entries = DB::table('entries')->where('tracker_id', $trackerId)->get();
|
||||
|
||||
$this->assertCount(1, $entries);
|
||||
$this->assertSame(7.0, (float) $entries->first()->quantity);
|
||||
}
|
||||
|
||||
public function test_down_writes_no_entry_for_a_zero_count(): void
|
||||
{
|
||||
$trackerId = $this->makeTracker();
|
||||
|
||||
$this->migration()->down();
|
||||
|
||||
$this->assertSame(0, DB::table('entries')->where('tracker_id', $trackerId)->count());
|
||||
}
|
||||
}
|
||||
124
tests/Feature/CounterTest.php
Normal file
124
tests/Feature/CounterTest.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Tracker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CounterTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function tracker(int $count = 0): Tracker
|
||||
{
|
||||
return Tracker::factory()->create(['count' => $count]);
|
||||
}
|
||||
|
||||
public function test_increment_adds_exactly_one(): void
|
||||
{
|
||||
$tracker = $this->tracker(5);
|
||||
|
||||
$response = $this->postJson('/increment');
|
||||
|
||||
$response->assertOk()->assertJson(['count' => 6]);
|
||||
$this->assertSame(6, $tracker->refresh()->count);
|
||||
}
|
||||
|
||||
public function test_increments_accumulate(): void
|
||||
{
|
||||
$tracker = $this->tracker();
|
||||
|
||||
$this->postJson('/increment');
|
||||
$this->postJson('/increment');
|
||||
$this->postJson('/increment');
|
||||
|
||||
$this->assertSame(3, $tracker->refresh()->count);
|
||||
}
|
||||
|
||||
public function test_increment_returns_404_without_a_tracker(): void
|
||||
{
|
||||
$this->postJson('/increment')->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_count_can_be_set_to_an_absolute_value(): void
|
||||
{
|
||||
$tracker = $this->tracker(3);
|
||||
|
||||
$response = $this->patchJson('/count', ['count' => 250]);
|
||||
|
||||
$response->assertOk()->assertJson(['count' => 250]);
|
||||
$this->assertSame(250, $tracker->refresh()->count);
|
||||
}
|
||||
|
||||
public function test_count_can_be_set_to_zero(): void
|
||||
{
|
||||
$tracker = $this->tracker(42);
|
||||
|
||||
$this->patchJson('/count', ['count' => 0])->assertOk();
|
||||
|
||||
$this->assertSame(0, $tracker->refresh()->count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{mixed}>
|
||||
*/
|
||||
public static function invalidCounts(): array
|
||||
{
|
||||
return [
|
||||
'negative' => [-1],
|
||||
'non-numeric string' => ['abc'],
|
||||
'fractional' => [1.5],
|
||||
'above unsigned int ceiling' => [4294967296],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('invalidCounts')]
|
||||
public function test_count_rejects_invalid_values(mixed $value): void
|
||||
{
|
||||
$tracker = $this->tracker(7);
|
||||
|
||||
$this->patchJson('/count', ['count' => $value])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('count');
|
||||
|
||||
$this->assertSame(7, $tracker->refresh()->count);
|
||||
}
|
||||
|
||||
public function test_count_requires_a_value(): void
|
||||
{
|
||||
$this->tracker();
|
||||
|
||||
$this->patchJson('/count', [])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors('count');
|
||||
}
|
||||
|
||||
public function test_count_returns_404_without_a_tracker(): void
|
||||
{
|
||||
$this->patchJson('/count', ['count' => 5])->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_counter_endpoints_return_json_not_redirects(): void
|
||||
{
|
||||
$this->tracker();
|
||||
|
||||
$this->postJson('/increment')
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'application/json');
|
||||
|
||||
$this->patchJson('/count', ['count' => 1])
|
||||
->assertOk()
|
||||
->assertHeader('content-type', 'application/json');
|
||||
}
|
||||
|
||||
public function test_count_is_cast_to_an_integer(): void
|
||||
{
|
||||
$this->tracker(12);
|
||||
|
||||
$this->assertSame(12, Tracker::first()->count);
|
||||
}
|
||||
}
|
||||
61
tests/Feature/TrackerTest.php
Normal file
61
tests/Feature/TrackerTest.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Tracker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TrackerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_show_reports_no_tracker_on_a_fresh_install(): void
|
||||
{
|
||||
$this->getJson('/tracker')
|
||||
->assertOk()
|
||||
->assertJson(['exists' => false]);
|
||||
}
|
||||
|
||||
public function test_show_returns_the_tracker_once_created(): void
|
||||
{
|
||||
Tracker::factory()->create(['count' => 9]);
|
||||
|
||||
$this->getJson('/tracker')
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'exists' => true,
|
||||
'tracker' => ['count' => 9],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_a_tracker_can_be_created_without_a_label_or_unit(): void
|
||||
{
|
||||
$this->postJson('/tracker', [])->assertCreated();
|
||||
|
||||
$tracker = Tracker::first();
|
||||
|
||||
$this->assertSame('Counter', $tracker->label);
|
||||
$this->assertSame('units', $tracker->unit);
|
||||
$this->assertSame(0, $tracker->count);
|
||||
}
|
||||
|
||||
public function test_a_new_counter_starts_at_zero(): void
|
||||
{
|
||||
$this->postJson('/tracker', []);
|
||||
|
||||
$this->getJson('/tracker')
|
||||
->assertOk()
|
||||
->assertJson(['tracker' => ['count' => 0]]);
|
||||
}
|
||||
|
||||
public function test_creating_a_second_tracker_conflicts(): void
|
||||
{
|
||||
$this->postJson('/tracker', [])->assertCreated();
|
||||
$this->postJson('/tracker', [])->assertStatus(409);
|
||||
|
||||
$this->assertSame(1, Tracker::count());
|
||||
}
|
||||
}
|
||||
0
tests/Unit/.gitkeep
Normal file
0
tests/Unit/.gitkeep
Normal file
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_that_true_is_true()
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue