feat: implement currency exchange rate scraping service and command

هذا الالتزام موجود في:
2025-05-24 01:37:27 +03:00
الأصل 76c2ab12c8
التزام 72b4eff3ee
10 ملفات معدلة مع 1087 إضافات و40 حذوفات

عرض الملف

@@ -1,19 +0,0 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}

عرض الملف

@@ -0,0 +1,80 @@
<?php
namespace Tests\Feature\Services;
use App\Services\CurrencyService;
use Carbon\Carbon;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Symfony\Component\DomCrawler\Crawler;
use Tests\TestCase;
/**
* @group real test - requires internet connection and maybe slow
*/
class CurrencyServiceFeatureTest extends TestCase
{
protected $currencyService;
protected function setUp(): void
{
parent::setUp();
$this->currencyService = new CurrencyService();
}
/**
* Test getting last twenty days of currency data
*/
public function testGetLastTwentyDays()
{
$result = $this->currencyService->getLastTwentyDays();
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$firstItem = $result[0];
$this->assertArrayHasKey('currency', $firstItem);
$this->assertArrayHasKey('city', $firstItem);
$this->assertArrayHasKey('price_buy', $firstItem);
$this->assertArrayHasKey('price_sell', $firstItem);
$this->assertArrayHasKey('date', $firstItem);
$this->assertArrayHasKey('day', $firstItem);
$this->assertContains('Sanaa', array_column($result, 'city'));
$this->assertContains('Aden', array_column($result, 'city'));
$this->assertContains('USD', array_column($result, 'currency'));
$this->assertContains('SAR', array_column($result, 'currency'));
}
/**
* Test getting today's currencies
*/
public function testGetTodayCurrencies()
{
$result = $this->currencyService->getTodayCurrencies();
// Assertions
$this->assertIsArray($result);
$this->assertNotEmpty($result);
// Should only return today's data for Sanaa and Aden
// In our sample, there are 4 entries for today (2 cities x 2 currencies)
$todayDate = Carbon::today()->format('Y-m-d');
$todayItems = array_filter($result, function ($item) use ($todayDate) {
return $item['date'] === $todayDate;
});
// Check that we have both cities
$cities = array_column($todayItems, 'city');
$this->assertContains('Sanaa', $cities);
$this->assertContains('Aden', $cities);
// Check that we have both currencies
$currencies = array_column($todayItems, 'currency');
$this->assertContains('USD', $currencies);
$this->assertContains('SAR', $currencies);
}
}

عرض الملف

@@ -0,0 +1,205 @@
<?php
namespace Tests\Unit;
use App\Services\CurrencyService;
use Illuminate\Support\Carbon;
use Mockery;
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\DomCrawler\Crawler;
use Tests\TestCase;
class CurrencyServiceTest extends TestCase
{
protected $currencyService;
protected $mockHttpBrowser;
protected function setUp(): void
{
parent::setUp();
// Create a mock for HttpBrowser
$this->mockHttpBrowser = Mockery::mock(HttpBrowser::class);
// Create the service with mocked dependencies
$this->currencyService = Mockery::mock(CurrencyService::class)
->makePartial()
->shouldAllowMockingProtectedMethods();
// Configure the service to return our mock
$this->currencyService->shouldAllowMockingProtectedMethods();
$this->currencyService->shouldReceive('createHttpBrowser')
->andReturn($this->mockHttpBrowser);
}
public function testGetTodayCurrenciesFiltersForTodayAndCities()
{
// Sample data that includes today and past dates
$today = Carbon::today()->format('Y-m-d');
$yesterday = Carbon::yesterday()->format('Y-m-d');
$testData = [
[
'currency' => 'USD',
'city' => 'Sanaa',
'price_buy' => 535.0,
'price_sell' => 537.0,
'date' => $today,
'day' => Carbon::today()->format('l')
],
[
'currency' => 'SAR',
'city' => 'Sanaa',
'price_buy' => 139.8,
'price_sell' => 140.2,
'date' => $today,
'day' => Carbon::today()->format('l')
],
[
'currency' => 'USD',
'city' => 'Aden',
'price_buy' => 2541.0,
'price_sell' => 2556.0,
'date' => $today,
'day' => Carbon::today()->format('l')
],
[
'currency' => 'SAR',
'city' => 'Aden',
'price_buy' => 668.0,
'price_sell' => 670.0,
'date' => $today,
'day' => Carbon::today()->format('l')
],
[
'currency' => 'USD',
'city' => 'Sanaa',
'price_buy' => 534.0,
'price_sell' => 536.0,
'date' => $yesterday,
'day' => Carbon::yesterday()->format('l')
],
[
'currency' => 'USD',
'city' => 'OtherCity',
'price_buy' => 540.0,
'price_sell' => 542.0,
'date' => $today,
'day' => Carbon::today()->format('l')
]
];
// Create a new instance for this test
$currencyService = Mockery::mock(CurrencyService::class)->makePartial();
$currencyService->shouldReceive('getLastTwentyDays')
->once()
->andReturn($testData);
// Test the method
$result = $currencyService->getTodayCurrencies();
// Should only include today's data for Sanaa and Aden
$this->assertCount(4, $result);
// Check that only Sanaa and Aden cities are included
$cities = array_column($result, 'city');
$this->assertContains('Sanaa', $cities);
$this->assertContains('Aden', $cities);
$this->assertNotContains('OtherCity', $cities);
// Check that only today's data is included
$dates = array_unique(array_column($result, 'date'));
$this->assertCount(1, $dates);
$this->assertEquals($today, $dates[0]);
}
public function testGetTodayCurrenciesHandlesExceptions()
{
// Create a subclass of CurrencyService for testing
$currencyService = new class extends CurrencyService {
// Override getLastTwentyDays to throw an exception
public function getLastTwentyDays(): array
{
throw new \Exception('Test exception');
}
};
// Test the method - this will call the real getTodayCurrencies which calls our overridden getLastTwentyDays
$result = $currencyService->getTodayCurrencies();
// Should return an empty array on exception
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetTodayCurrenciesUsesRecentDataWhenTodayNotAvailable()
{
// Sample data that includes only past dates
$yesterday = Carbon::yesterday()->format('Y-m-d');
$twoDaysAgo = Carbon::yesterday()->subDay()->format('Y-m-d');
$testData = [
[
'currency' => 'USD',
'city' => 'Sanaa',
'price_buy' => 534.0,
'price_sell' => 536.0,
'date' => $yesterday,
'day' => Carbon::yesterday()->format('l')
],
[
'currency' => 'SAR',
'city' => 'Sanaa',
'price_buy' => 139.0,
'price_sell' => 140.0,
'date' => $yesterday,
'day' => Carbon::yesterday()->format('l')
],
[
'currency' => 'USD',
'city' => 'Aden',
'price_buy' => 2540.0,
'price_sell' => 2555.0,
'date' => $yesterday,
'day' => Carbon::yesterday()->format('l')
],
[
'currency' => 'USD',
'city' => 'Sanaa',
'price_buy' => 533.0,
'price_sell' => 535.0,
'date' => $twoDaysAgo,
'day' => Carbon::yesterday()->subDay()->format('l')
]
];
// Create a new instance for this test
$currencyService = Mockery::mock(CurrencyService::class)->makePartial();
$currencyService->shouldReceive('getLastTwentyDays')
->once()
->andReturn($testData);
// Test the method
$result = $currencyService->getTodayCurrencies();
// Should include yesterday's data for Sanaa and Aden
$this->assertCount(3, $result);
// Check that only Sanaa and Aden cities are included
$cities = array_column($result, 'city');
$this->assertContains('Sanaa', $cities);
$this->assertContains('Aden', $cities);
// Check that only yesterday's data is included (most recent)
$dates = array_unique(array_column($result, 'date'));
$this->assertCount(1, $dates);
$this->assertEquals($yesterday, $dates[0]);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}

عرض الملف

@@ -1,16 +0,0 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}

عرض الملف

@@ -0,0 +1,140 @@
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Console\Commands\FetchCurrencyCommand;
use App\Services\CurrencyService;
use Mockery;
class FetchCurrencyCommandTest extends TestCase
{
protected $command;
protected $currencyService;
protected function setUp(): void
{
parent::setUp();
// Create a mock for the CurrencyService
$this->currencyService = Mockery::mock(CurrencyService::class);
$this->app->instance(CurrencyService::class, $this->currencyService);
// Create the command
$this->command = new FetchCurrencyCommand();
}
public function testHandleWithTodayOption()
{
// Sample data for today's currencies
$todayCurrencies = [
[
'currency' => 'USD',
'city' => 'Sanaa',
'price_buy' => 535.0,
'price_sell' => 537.0,
'date' => '2025-05-19',
'day' => 'Monday'
],
[
'currency' => 'SAR',
'city' => 'Sanaa',
'price_buy' => 139.8,
'price_sell' => 140.2,
'date' => '2025-05-19',
'day' => 'Monday'
]
];
// Configure the mock to return our sample data
$this->currencyService->shouldReceive('getTodayCurrencies')
->once()
->andReturn($todayCurrencies);
// We don't expect getLastTwentyDays to be called
$this->currencyService->shouldReceive('getLastTwentyDays')
->never();
// Create a mock for the command to capture output
$this->artisan('currency:fetch', ['--today' => true])
->expectsOutput('Fetching currency data...')
->expectsOutput('Today\'s Currency Data:')
->expectsOutput('Currency data fetched successfully!')
->assertExitCode(0);
}
public function testHandleWithoutTodayOption()
{
// Sample data for historical currencies
$historicalCurrencies = [
[
'currency' => 'USD',
'city' => 'Sanaa',
'price_buy' => 535.0,
'price_sell' => 537.0,
'date' => '2025-05-19',
'day' => 'Monday'
],
[
'currency' => 'USD',
'city' => 'Sanaa',
'price_buy' => 534.0,
'price_sell' => 536.0,
'date' => '2025-05-18',
'day' => 'Sunday'
]
];
// Configure the mock to return our sample data
$this->currencyService->shouldReceive('getLastTwentyDays')
->once()
->andReturn($historicalCurrencies);
// We don't expect getTodayCurrencies to be called
$this->currencyService->shouldReceive('getTodayCurrencies')
->never();
// Create a mock for the command to capture output
$this->artisan('currency:fetch')
->expectsOutput('Fetching currency data...')
->expectsOutput('Last 20 Days Currency Data:')
->expectsOutput('Currency data fetched successfully!')
->assertExitCode(0);
}
public function testHandleWithEmptyData()
{
// Configure the mock to return empty data
$this->currencyService->shouldReceive('getLastTwentyDays')
->once()
->andReturn([]);
// Create a mock for the command to capture output
$this->artisan('currency:fetch')
->expectsOutput('Fetching currency data...')
->expectsOutput('No historical currency data available.')
->expectsOutput('Currency data fetched successfully!')
->assertExitCode(0);
}
public function testHandleWithException()
{
// Configure the mock to throw an exception
$this->currencyService->shouldReceive('getLastTwentyDays')
->once()
->andThrow(new \Exception('Test exception'));
// Create a mock for the command to capture output
$this->artisan('currency:fetch')
->expectsOutput('Fetching currency data...')
->expectsOutput('Failed to fetch currency data: Test exception')
->assertExitCode(1);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}