codes.txt

This commit is contained in:
Mekan1206
2026-06-03 21:24:00 +05:00
parent c74330cfaf
commit fa3202288f
9 changed files with 341 additions and 17 deletions

View File

@@ -2,10 +2,14 @@
namespace Tests\Feature;
use App\Exceptions\CouponPoolExhaustedException;
use App\Models\Coupon;
use App\Models\PhoneVerification;
use App\Services\CouponCodePool;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Http;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
class VerificationFlowTest extends TestCase
@@ -82,15 +86,43 @@ class VerificationFlowTest extends TestCase
->assertRedirect(route('verification.congratulations'))
->assertCookie('device_registered');
$this->assertDatabaseHas(Coupon::class, [
'phone' => self::UNMASKED_PHONE,
]);
$coupon = Coupon::query()->where('phone', self::UNMASKED_PHONE)->first();
$this->assertNotNull($coupon);
$this->assertContains(
$coupon->code,
(new CouponCodePool)->allCodes(),
);
$this->get(route('verification.congratulations'))
->assertOk()
->assertViewHas('code');
}
public function test_index_shows_promotion_ended_when_pool_exhausted(): void
{
$this->mock(CouponCodePool::class, function (MockInterface $mock): void {
$mock->shouldReceive('hasAvailable')->andReturn(false);
});
$this->get(route('verification.index'))
->assertOk()
->assertViewIs('verification.promotion-ended')
->assertSee(CouponPoolExhaustedException::MESSAGE);
}
public function test_send_otp_rejects_when_pool_exhausted(): void
{
$this->mock(CouponCodePool::class, function (MockInterface $mock): void {
$mock->shouldReceive('hasAvailable')->andReturn(false);
});
$this->from(route('verification.index'))
->post(route('verification.send'), ['phone' => self::PHONE])
->assertRedirect(route('verification.index'))
->assertSessionHasErrors(['phone' => CouponPoolExhaustedException::MESSAGE]);
}
public function test_verify_otp_rejects_wrong_code(): void
{
PhoneVerification::factory()
@@ -164,6 +196,13 @@ class VerificationFlowTest extends TestCase
->assertStatus(429);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
private function extractOtpFromLastSms(): string
{
$recorded = Http::recorded()->all();
@@ -172,7 +211,7 @@ class VerificationFlowTest extends TestCase
[$request] = $recorded[array_key_last($recorded)];
preg_match('/(\d{4})/', (string) $request->data()['code'], $matches);
preg_match('/(\d{4})/', (string) $request->data()['message'], $matches);
$this->assertNotEmpty($matches[1]);

View File

@@ -0,0 +1,73 @@
<?php
namespace Tests\Unit;
use App\Models\Coupon;
use App\Services\CouponCodePool;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Tests\TestCase;
class CouponCodePoolTest extends TestCase
{
use LazilyRefreshDatabase;
private string $codesPath;
protected function setUp(): void
{
parent::setUp();
$this->codesPath = tempnam(sys_get_temp_dir(), 'codes_');
file_put_contents($this->codesPath, "1_AAAA\n2_BBBB\n3_CCCC\n");
}
protected function tearDown(): void
{
if (is_file($this->codesPath)) {
unlink($this->codesPath);
}
parent::tearDown();
}
public function test_all_codes_loads_from_file(): void
{
$pool = new CouponCodePool($this->codesPath);
$this->assertSame(['1_AAAA', '2_BBBB', '3_CCCC'], $pool->allCodes());
}
public function test_available_codes_excludes_used_coupons(): void
{
Coupon::factory()->create(['code' => '1_AAAA', 'phone' => '61111111']);
$pool = new CouponCodePool($this->codesPath);
$this->assertEqualsCanonicalizing(['2_BBBB', '3_CCCC'], $pool->availableCodes());
}
public function test_pick_random_returns_only_unused_code(): void
{
Coupon::factory()->create(['code' => '2_BBBB', 'phone' => '61111111']);
$pool = new CouponCodePool($this->codesPath);
for ($i = 0; $i < 20; $i++) {
$picked = $pool->pickRandom();
$this->assertContains($picked, ['1_AAAA', '3_CCCC']);
}
}
public function test_has_available_is_false_when_all_codes_used(): void
{
Coupon::factory()->create(['code' => '1_AAAA', 'phone' => '61111111']);
Coupon::factory()->create(['code' => '2_BBBB', 'phone' => '62222222']);
Coupon::factory()->create(['code' => '3_CCCC', 'phone' => '63333333']);
$pool = new CouponCodePool($this->codesPath);
$this->assertFalse($pool->hasAvailable());
$this->assertNull($pool->pickRandom());
$this->assertSame([], $pool->availableCodes());
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Tests\Unit;
use App\Exceptions\CouponPoolExhaustedException;
use App\Models\Coupon;
use App\Services\CouponCodePool;
use App\Services\CouponService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
class CouponServiceTest extends TestCase
{
use LazilyRefreshDatabase;
public function test_find_or_create_assigns_code_from_pool(): void
{
$this->mock(CouponCodePool::class, function (MockInterface $mock): void {
$mock->shouldReceive('pickRandom')->once()->andReturn('1_AAAA');
});
$coupon = app(CouponService::class)->findOrCreateForPhone('61929248');
$this->assertSame('61929248', $coupon->phone);
$this->assertSame('1_AAAA', $coupon->code);
$this->assertDatabaseHas(Coupon::class, [
'phone' => '61929248',
'code' => '1_AAAA',
]);
}
public function test_find_or_create_returns_existing_coupon_without_picking(): void
{
$existing = Coupon::factory()->create(['phone' => '61929248', 'code' => '2_BBBB']);
$this->mock(CouponCodePool::class, function (MockInterface $mock): void {
$mock->shouldNotReceive('pickRandom');
});
$coupon = app(CouponService::class)->findOrCreateForPhone('61929248');
$this->assertTrue($existing->is($coupon));
}
public function test_find_or_create_throws_when_pool_exhausted(): void
{
$this->mock(CouponCodePool::class, function (MockInterface $mock): void {
$mock->shouldReceive('pickRandom')->andReturn(null);
});
$this->expectException(CouponPoolExhaustedException::class);
app(CouponService::class)->findOrCreateForPhone('61929248');
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}