feat: implement currency exchange rate scraping service and command
هذا الالتزام موجود في:
@@ -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);
|
||||
}
|
||||
}
|
||||
80
tests/Feature/Services/CurrencyServiceFeatureTest.php
Normal file
80
tests/Feature/Services/CurrencyServiceFeatureTest.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
205
tests/Unit/CurrencyServiceTest.php
Normal file
205
tests/Unit/CurrencyServiceTest.php
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
140
tests/Unit/FetchCurrencyCommandTest.php
Normal file
140
tests/Unit/FetchCurrencyCommandTest.php
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم