tests/Feature/CurrencyApiTest.php
Copy Code
<?php
use App\Models\Currency;
use App\Models\Tokens;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
beforeEach(function () {
// Refresh database and cache for each test
Currency::truncate();
Tokens::truncate();
User::truncate();
Cache::flush();
});
test('api middleware blocks request without token', function () {
$response = $this->getJson('/api/v1');
$response->assertStatus(401)
->assertJsonPath('status', false)
->assertJsonPath('message', 'Sorry, But you forgot to provide a token.');
});
test('api middleware blocks request with invalid token', function () {
$response = $this->withToken('invalid_token_123')->getJson('/api/v1');
$response->assertStatus(401)
->assertJsonPath('status', false)
->assertJsonPath('message', 'Sorry, But your token is not valid or expired. Please request a new token or check your token.');
});
test('api middleware allows request with valid token and returns rates relative to default currency', function () {
$user = User::factory()->create();
$token = Tokens::create([
'user_id' => $user->id,
'name' => 'Test Token',
'token' => 'oxr_test_token_123',
'default_currency' => 'EUR',
'active' => true,
]);
// Seed some currency rates
Currency::create(['code' => 'USD', 'name' => 'United States Dollar', 'rate' => 1.0, 'active' => true, 'flag' => '🇺🇸', 'flag_png' => 'https://flagcdn.com/w320/us.png', 'flag_svg' => 'https://flagcdn.com/us.svg']);
Currency::create(['code' => 'EUR', 'name' => 'Euro', 'rate' => 0.92, 'active' => true, 'flag' => '🇪🇺', 'flag_png' => 'https://flagcdn.com/w320/eu.png', 'flag_svg' => 'https://flagcdn.com/eu.svg']);
Currency::create(['code' => 'INR', 'name' => 'Indian Rupee', 'rate' => 83.2, 'active' => true, 'flag' => '🇮🇳', 'flag_png' => 'https://flagcdn.com/w320/in.png', 'flag_svg' => 'https://flagcdn.com/in.svg']);
// Force refresh cache
Cache::put('currencies:rates', [
'USD' => 1.0,
'EUR' => 0.92,
'INR' => 83.2,
], 3600);
$response = $this->withToken('oxr_test_token_123')->getJson('/api/v1');
$response->assertStatus(200)
->assertJsonPath('base', 'EUR')
->assertJsonStructure(['disclaimer', 'license', 'timestamp', 'base', 'rates']);
// Check rate relative to EUR
// EUR relative to EUR should be 1.0
// USD relative to EUR should be 1.0 / 0.92 = 1.08695652
$rates = $response->json('rates');
expect($rates['EUR']['rate'])->toEqual(1.0);
expect($rates['USD']['rate'])->toEqual(round(1.0 / 0.92, 8));
expect($rates['INR']['rate'])->toEqual(round(83.2 / 0.92, 8));
expect($rates['EUR']['flag'])->toEqual('🇪🇺');
expect($rates['EUR']['flag_png'])->toEqual('https://flagcdn.com/w320/eu.png');
expect($rates['EUR']['flag_svg'])->toEqual('https://flagcdn.com/eu.svg');
});
test('api allows request with custom base currency code', function () {
$user = User::factory()->create();
$token = Tokens::create([
'user_id' => $user->id,
'name' => 'Test Token',
'token' => 'oxr_test_token_123',
'default_currency' => 'USD',
'active' => true,
]);
Currency::create(['code' => 'USD', 'name' => 'United States Dollar', 'rate' => 1.0, 'active' => true]);
Currency::create(['code' => 'INR', 'name' => 'Indian Rupee', 'rate' => 83.2, 'active' => true]);
Cache::put('currencies:rates', [
'USD' => 1.0,
'INR' => 83.2,
], 3600);
// Request with custom base INR
$response = $this->withToken('oxr_test_token_123')->getJson('/api/v1/INR');
$response->assertStatus(200)
->assertJsonPath('base', 'INR');
// USD relative to INR should be 1.0 / 83.2 = 0.01201923
$rates = $response->json('rates');
expect($rates['USD']['rate'])->toEqual(round(1.0 / 83.2, 8));
expect($rates['INR']['rate'])->toEqual(1.0);
});
test('api returns 400 error for unsupported custom base currency', function () {
$user = User::factory()->create();
Tokens::create([
'user_id' => $user->id,
'name' => 'Test Token',
'token' => 'oxr_test_token_123',
'default_currency' => 'USD',
'active' => true,
]);
Currency::create(['code' => 'USD', 'name' => 'United States Dollar', 'rate' => 1.0, 'active' => true]);
Cache::put('currencies:rates', ['USD' => 1.0], 3600);
$response = $this->withToken('oxr_test_token_123')->getJson('/api/v1/XYZ');
$response->assertStatus(400)
->assertJsonPath('status', false)
->assertJsonPath('message', "Currency code 'XYZ' is not supported or active.");
});
test('api converts currency values correctly', function () {
$user = User::factory()->create();
Tokens::create([
'user_id' => $user->id,
'name' => 'Test Token',
'token' => 'oxr_test_token_123',
'default_currency' => 'USD',
'active' => true,
]);
Currency::create(['code' => 'USD', 'name' => 'United States Dollar', 'rate' => 1.0, 'active' => true]);
Currency::create(['code' => 'EUR', 'name' => 'Euro', 'rate' => 0.92, 'active' => true]);
Currency::create(['code' => 'INR', 'name' => 'Indian Rupee', 'rate' => 83.2, 'active' => true]);
Cache::put('currencies:rates', [
'USD' => 1.0,
'EUR' => 0.92,
'INR' => 83.2,
], 3600);
// Convert 100 EUR to INR
// 100 EUR = 100 / 0.92 USD = 108.6956 USD
// 108.6956 USD * 83.2 = 9043.4782 INR
$response = $this->withToken('oxr_test_token_123')
->postJson('/api/v1/EUR/INR', ['amount' => 100]);
$response->assertStatus(200)
->assertJsonPath('status', true)
->assertJsonPath('from', 'EUR')
->assertJsonPath('to', 'INR')
->assertJsonPath('amount', 100)
->assertJsonPath('converted', round((100 / 0.92) * 83.2, 4));
});
test('sync command fetches real rates from openexchangerates and saves/caches them', function () {
// Fake the external HTTP requests
Http::fake([
'openexchangerates.org/api/currencies.json' => Http::response([
'USD' => 'United States Dollar',
'EUR' => 'Euro',
'INR' => 'Indian Rupee',
'BTC' => 'Bitcoin',
], 200),
'openexchangerates.org/api/latest.json*' => Http::response([
'rates' => [
'USD' => 1.0,
'EUR' => 0.915,
'INR' => 83.45,
'BTC' => 0.000015,
],
], 200),
'restcountries.com/v3.1/all*' => Http::response([
[
'cca2' => 'US',
'name' => ['common' => 'United States'],
'currencies' => ['USD' => ['name' => 'United States dollar', 'symbol' => '$']],
'flags' => ['png' => 'https://flagcdn.com/w320/us.png', 'svg' => 'https://flagcdn.com/us.svg'],
'flag' => '🇺🇸',
],
[
'cca2' => 'IN',
'name' => ['common' => 'India'],
'currencies' => ['INR' => ['name' => 'Indian rupee', 'symbol' => '₹']],
'flags' => ['png' => 'https://flagcdn.com/w320/in.png', 'svg' => 'https://flagcdn.com/in.svg'],
'flag' => '🇮🇳',
],
[
'cca2' => 'BT',
'name' => ['common' => 'Bhutan'],
'currencies' => [
'BTN' => ['name' => 'Bhutanese ngultrum', 'symbol' => 'Nu.'],
'INR' => ['name' => 'Indian rupee', 'symbol' => '₹'],
],
'flags' => ['png' => 'https://flagcdn.com/w320/bt.png', 'svg' => 'https://flagcdn.com/bt.svg'],
'flag' => '🇧🇹',
],
], 200),
]);
$exitCode = Artisan::call('app:sync-currencies');
expect($exitCode)->toEqual(0);
// Verify database was updated
$this->assertDatabaseHas('currencies', [
'code' => 'EUR',
'rate' => 0.915,
'name' => 'Euro',
'country' => 'Eurozone (Austria, Belgium, Cyprus, Estonia, Finland, France, Germany, Greece, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Portugal, Slovakia, Slovenia, Spain)',
'flag' => '🇪🇺',
'flag_png' => 'https://flagcdn.com/w320/eu.png',
'flag_svg' => 'https://flagcdn.com/eu.svg',
]);
$this->assertDatabaseHas('currencies', [
'code' => 'INR',
'rate' => 83.45,
'name' => 'Indian Rupee',
'country' => 'India, Bhutan',
'flag' => '🇮🇳',
'flag_png' => 'https://flagcdn.com/w320/in.png',
'flag_svg' => 'https://flagcdn.com/in.svg',
]);
// Verify that BTC (which has no flag) was skipped and is missing from database
$this->assertDatabaseMissing('currencies', [
'code' => 'BTC',
]);
// Verify cache was populated
$cachedRates = Cache::get('currencies:rates');
expect($cachedRates)->toBeArray();
expect($cachedRates['EUR'])->toEqual(0.915);
expect($cachedRates['INR'])->toEqual(83.45);
expect(isset($cachedRates['BTC']))->toBeFalse();
});
test('api returns rates relative to system default INR if token default currency is null', function () {
$user = User::factory()->create();
Tokens::create([
'user_id' => $user->id,
'name' => 'Test Token',
'token' => 'oxr_test_token_123',
'default_currency' => null,
'active' => true,
]);
Currency::create(['code' => 'USD', 'name' => 'United States Dollar', 'rate' => 1.0, 'active' => true]);
Currency::create(['code' => 'INR', 'name' => 'Indian Rupee', 'rate' => 83.2, 'active' => true]);
Cache::put('currencies:rates', [
'USD' => 1.0,
'INR' => 83.2,
], 3600);
$response = $this->withToken('oxr_test_token_123')->getJson('/api/v1');
$response->assertStatus(200)
->assertJsonPath('base', 'INR');
$rates = $response->json('rates');
expect($rates['INR']['rate'])->toEqual(1.0);
expect($rates['USD']['rate'])->toEqual(round(1.0 / 83.2, 8));
});
test('api geo endpoints return cached lists and merged data with valid token', function () {
$user = User::factory()->create();
Tokens::create([
'user_id' => $user->id,
'name' => 'Test Token',
'token' => 'oxr_test_token_123',
'default_currency' => 'INR',
'active' => true,
]);
// Seed database to populate relationships and prevent self-healing sync trigger in test
DB::table('countries')->insert([
'id' => 101,
'name' => 'India',
'sortname' => 'IN',
'currency_code' => 'INR',
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('states')->insert([
'id' => 2,
'name' => 'Andhra Pradesh',
'country_id' => 101,
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('cities')->insert([
'id' => 5,
'name' => 'Hyderabad',
'state_id' => 2,
'created_at' => now(),
'updated_at' => now(),
]);
Currency::create([
'code' => 'INR',
'name' => 'Indian Rupee',
'rate' => 83.2,
'active' => true,
'flag' => '🇮🇳',
'flag_png' => 'https://flagcdn.com/w320/in.png',
'flag_svg' => 'https://flagcdn.com/in.svg',
]);
// 1. Seed Cache with Mock Geo Data
Cache::put('geo:countries', [
['id' => 101, 'name' => 'India', 'sortname' => 'IN', 'currency_code' => 'INR'],
], 3600);
Cache::put('geo:states_list', [
['id' => 2, 'name' => 'Andhra Pradesh', 'country_id' => 101],
], 3600);
Cache::put('geo:cities_list', [
['id' => 5, 'name' => 'Hyderabad', 'state_id' => 2],
], 3600);
Cache::put('geo:all_merged', [
'india' => [
'currency' => [
'name' => 'Indian Rupee',
'code' => 'INR',
'symbol' => '₹',
'flag' => '🇮🇳',
'flag_png' => 'https://flagcdn.com/w320/in.png',
'flag_svg' => 'https://flagcdn.com/in.svg',
'rate' => 83.2,
'country' => 'India, Bhutan',
'active' => true,
],
'states' => [
'Andhra Pradesh' => [
'Hyderabad',
],
],
],
], 3600);
// Call countries endpoint with token
$countriesResponse = $this->withToken('oxr_test_token_123')->getJson('/api/v1/countries');
$countriesResponse->assertStatus(200)
->assertJsonPath('status', true)
->assertJsonStructure(['status', 'countries']);
$countries = $countriesResponse->json('countries');
expect($countries[0])->toHaveKey('name');
expect($countries[0])->toHaveKey('iso_code');
expect($countries[0])->toHaveKey('currency');
expect($countries[0]['currency']['code'])->toEqual('INR');
expect($countries[0])->not->toHaveKey('id');
expect($countries[0])->not->toHaveKey('created_at');
expect($countries[0]['currency'])->not->toHaveKey('id');
// Call states endpoint with token
$statesResponse = $this->withToken('oxr_test_token_123')->getJson('/api/v1/countries/IN/states');
$statesResponse->assertStatus(200)
->assertJsonPath('status', true)
->assertJsonPath('country_code', 'IN')
->assertJsonStructure(['status', 'country_code', 'states']);
$states = $statesResponse->json('states');
expect($states[0])->toHaveKey('name');
expect($states[0])->toHaveKey('country');
expect($states[0]['country']['name'])->toEqual('India');
expect($states[0]['country']['iso_code'])->toEqual('IN');
expect($states[0]['country']['currency']['code'])->toEqual('INR');
expect($states[0])->not->toHaveKey('id');
expect($states[0])->not->toHaveKey('country_id');
// Call cities endpoint with token
$citiesResponse = $this->withToken('oxr_test_token_123')->getJson('/api/v1/states/Andhra Pradesh/cities');
$citiesResponse->assertStatus(200)
->assertJsonPath('status', true)
->assertJsonPath('state_name', 'Andhra Pradesh')
->assertJsonStructure(['status', 'state_name', 'cities']);
$cities = $citiesResponse->json('cities');
expect($cities[0])->toHaveKey('name');
expect($cities[0])->toHaveKey('state');
expect($cities[0]['state']['name'])->toEqual('Andhra Pradesh');
expect($cities[0]['state']['country']['name'])->toEqual('India');
expect($cities[0]['state']['country']['currency']['code'])->toEqual('INR');
expect($cities[0])->not->toHaveKey('id');
expect($cities[0])->not->toHaveKey('state_id');
// Call allMerged endpoint with token
$allMergedResponse = $this->withToken('oxr_test_token_123')->getJson('/api/v1/all');
$allMergedResponse->assertStatus(200)
->assertJsonStructure(['india' => ['currency', 'states']]);
});
test('api geo endpoints require authorization token', function () {
$this->getJson('/api/v1/countries')->assertStatus(401);
$this->getJson('/api/v1/countries/IN/states')->assertStatus(401);
$this->getJson('/api/v1/states/Andhra Pradesh/cities')->assertStatus(401);
$this->getJson('/api/v1/all')->assertStatus(401);
});
test('api geo endpoints trigger self-healing sync when database is empty', function () {
$user = User::factory()->create();
Tokens::create([
'user_id' => $user->id,
'name' => 'Test Token',
'token' => 'oxr_test_token_123',
'default_currency' => 'INR',
'active' => true,
]);
// Truncate tables to make sure database is completely empty
DB::table('cities')->delete();
DB::table('states')->delete();
DB::table('countries')->delete();
Currency::truncate();
Cache::flush();
$geoPath = storage_path('app/geo');
if (! is_dir($geoPath)) {
mkdir($geoPath, 0755, true);
}
file_put_contents($geoPath.'/countries.json', json_encode([
'countries' => [
['id' => 101, 'name' => 'India', 'sortname' => 'IN', 'phoneCode' => '91'],
],
]));
file_put_contents($geoPath.'/states.json', json_encode([
'states' => [
['id' => 2, 'name' => 'Andhra Pradesh', 'country_id' => 101],
],
]));
file_put_contents($geoPath.'/cities.json', json_encode([
'cities' => [
['id' => 5, 'name' => 'Hyderabad', 'state_id' => 2],
],
]));
// Fake HTTP requests that the sync command will make
Http::fake([
'openexchangerates.org/api/currencies.json' => Http::response([
'INR' => 'Indian Rupee',
], 200),
'openexchangerates.org/api/latest.json*' => Http::response([
'rates' => [
'INR' => 83.45,
],
], 200),
'restcountries.com/v3.1/all*' => Http::response([
[
'cca2' => 'IN',
'name' => ['common' => 'India'],
'currencies' => ['INR' => ['name' => 'Indian rupee', 'symbol' => '₹']],
'flags' => ['png' => 'https://flagcdn.com/w320/in.png', 'svg' => 'https://flagcdn.com/in.svg'],
'flag' => '🇮🇳',
],
], 200),
]);
// Request countries endpoint with token. This should trigger the sync and succeed
$response = $this->withToken('oxr_test_token_123')->getJson('/api/v1/countries');
$response->assertStatus(200)
->assertJsonPath('status', true);
// Verify database was populated
expect(Currency::count())->toBeGreaterThan(0);
expect(DB::table('countries')->count())->toBeGreaterThan(0);
});